@kyo-so/cli 0.9.1 → 0.11.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/index.js CHANGED
@@ -169500,6 +169500,9 @@ function defineConfig(config) {
169500
169500
  // src/core/runReview.ts
169501
169501
  import { resolve as resolve6 } from "node:path";
169502
169502
 
169503
+ // src/config/schema.ts
169504
+ import { isAbsolute } from "node:path";
169505
+
169503
169506
  // node_modules/zod/v4/classic/external.js
169504
169507
  var exports_external = {};
169505
169508
  __export(exports_external, {
@@ -183776,8 +183779,19 @@ function date4(params) {
183776
183779
 
183777
183780
  // node_modules/zod/v4/classic/external.js
183778
183781
  config(en_default());
183782
+ // src/core/constants.ts
183783
+ var DEFAULT_AGENT_TIMEOUT_MS = 120000;
183784
+ var MAX_AGENT_OUTPUT_BYTES = 1048576;
183785
+ var JUDGE_MAX_OUTPUT_TOKENS = 4096;
183786
+ var RAW_OUTPUT_MAX_CHARS = 16384;
183787
+ var TRACE_DIR = ".kyoso/traces";
183788
+ var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
183789
+
183779
183790
  // src/config/schema.ts
183780
- var agentSchema = exports_external.object({
183791
+ var CODEX_OPENROUTER_PROVIDER = "openrouter";
183792
+ var CODEX_DEFAULT_PROVIDER = "default";
183793
+ var CODEX_OPENROUTER_MODEL_REQUIRED_ISSUE = "codex_openrouter_model_required";
183794
+ var baseAgentSchema = exports_external.object({
183781
183795
  enabled: exports_external.boolean().default(true),
183782
183796
  type: exports_external.literal("acp").default("acp"),
183783
183797
  command: exports_external.string(),
@@ -183799,6 +183813,31 @@ var agentSchema = exports_external.object({
183799
183813
  envWhitelist: exports_external.array(exports_external.string())
183800
183814
  })
183801
183815
  });
183816
+ var codexAgentSchema = baseAgentSchema.extend({
183817
+ provider: exports_external.enum([CODEX_OPENROUTER_PROVIDER, CODEX_DEFAULT_PROVIDER]).optional(),
183818
+ allowProjectProvider: exports_external.array(exports_external.string().min(1).refine(isAbsolute, {
183819
+ message: "must contain only absolute project directory paths for exact matching"
183820
+ })).default([])
183821
+ }).superRefine((agent, context) => {
183822
+ if (agent.provider !== CODEX_OPENROUTER_PROVIDER || (agent.model?.trim().length ?? 0) > 0) {
183823
+ return;
183824
+ }
183825
+ context.addIssue({
183826
+ code: exports_external.ZodIssueCode.custom,
183827
+ path: ["model"],
183828
+ message: 'model must be a non-empty string when provider is "openrouter".',
183829
+ params: {
183830
+ kyosoIssue: CODEX_OPENROUTER_MODEL_REQUIRED_ISSUE
183831
+ }
183832
+ });
183833
+ });
183834
+ var reviewBudgetSchema = exports_external.object({
183835
+ maxModelCalls: exports_external.number().int().positive(),
183836
+ maxTotalWallTimeMs: exports_external.number().int().positive(),
183837
+ maxAgentOutputBytes: exports_external.number().int().positive().max(MAX_AGENT_OUTPUT_BYTES),
183838
+ maxFindingsPerAgent: exports_external.number().int().positive(),
183839
+ skipOptionalPhasesWhenTokenUsageUnknown: exports_external.boolean()
183840
+ });
183802
183841
  var kyosoConfigSchema = exports_external.object({
183803
183842
  entrypoints: exports_external.object({
183804
183843
  mcp: exports_external.boolean(),
@@ -183811,8 +183850,8 @@ var kyosoConfigSchema = exports_external.object({
183811
183850
  diffReview: exports_external.boolean()
183812
183851
  }),
183813
183852
  agents: exports_external.object({
183814
- codex: agentSchema,
183815
- claude: agentSchema
183853
+ codex: codexAgentSchema,
183854
+ claude: baseAgentSchema
183816
183855
  }),
183817
183856
  workspace: exports_external.object({
183818
183857
  mode: exports_external.literal("temp_snapshot"),
@@ -183856,6 +183895,7 @@ var kyosoConfigSchema = exports_external.object({
183856
183895
  timeoutMs: exports_external.number().int().positive().default(90000),
183857
183896
  allowDemotion: exports_external.boolean().default(false)
183858
183897
  }),
183898
+ reviewBudget: reviewBudgetSchema,
183859
183899
  audit: exports_external.object({
183860
183900
  enabled: exports_external.boolean(),
183861
183901
  format: exports_external.literal("jsonl"),
@@ -183863,9 +183903,18 @@ var kyosoConfigSchema = exports_external.object({
183863
183903
  includeRawAgentOutput: exports_external.boolean(),
183864
183904
  includeFileContents: exports_external.boolean()
183865
183905
  })
183906
+ }).superRefine((config2, context) => {
183907
+ const enabledPrimaryReviewers = Object.values(config2.agents).filter((agent) => agent.enabled).length;
183908
+ if (config2.reviewBudget.maxModelCalls >= enabledPrimaryReviewers)
183909
+ return;
183910
+ context.addIssue({
183911
+ code: exports_external.ZodIssueCode.custom,
183912
+ path: ["reviewBudget", "maxModelCalls"],
183913
+ message: "must be greater than or equal to the number of enabled primary reviewers."
183914
+ });
183866
183915
  });
183867
183916
  function agentConfigLeafPaths(agent) {
183868
- return [
183917
+ const paths = [
183869
183918
  `agents.${agent}.enabled`,
183870
183919
  `agents.${agent}.type`,
183871
183920
  `agents.${agent}.command`,
@@ -183881,6 +183930,10 @@ function agentConfigLeafPaths(agent) {
183881
183930
  `agents.${agent}.auth.recommendedEnv`,
183882
183931
  `agents.${agent}.auth.envWhitelist`
183883
183932
  ];
183933
+ if (agent === "codex") {
183934
+ paths.push("agents.codex.provider", "agents.codex.allowProjectProvider");
183935
+ }
183936
+ return paths;
183884
183937
  }
183885
183938
  var kyosoConfigKnownLeafPaths = [
183886
183939
  "entrypoints.mcp",
@@ -183917,6 +183970,11 @@ var kyosoConfigKnownLeafPaths = [
183917
183970
  "verification.maxFindings",
183918
183971
  "verification.timeoutMs",
183919
183972
  "verification.allowDemotion",
183973
+ "reviewBudget.maxModelCalls",
183974
+ "reviewBudget.maxTotalWallTimeMs",
183975
+ "reviewBudget.maxAgentOutputBytes",
183976
+ "reviewBudget.maxFindingsPerAgent",
183977
+ "reviewBudget.skipOptionalPhasesWhenTokenUsageUnknown",
183920
183978
  "audit.enabled",
183921
183979
  "audit.format",
183922
183980
  "audit.directory",
@@ -183936,6 +183994,7 @@ var kyosoConfigSecuritySensitivePrefixes = [
183936
183994
  "secrets",
183937
183995
  "securityReview",
183938
183996
  "verification",
183997
+ "reviewBudget",
183939
183998
  "workspace"
183940
183999
  ];
183941
184000
 
@@ -183956,6 +184015,7 @@ var defaultConfig = {
183956
184015
  args: ["-y", "@agentclientprotocol/codex-acp@1.1.2"],
183957
184016
  role: "implementation_reviewer",
183958
184017
  timeoutMs: 120000,
184018
+ allowProjectProvider: [],
183959
184019
  env: {
183960
184020
  INITIAL_AGENT_MODE: "read-only",
183961
184021
  KYOSO_CHILD_AGENT: "1"
@@ -184048,7 +184108,7 @@ var defaultConfig = {
184048
184108
  }
184049
184109
  },
184050
184110
  judge: {
184051
- mode: "deterministic_plus_llm",
184111
+ mode: "deterministic_only",
184052
184112
  provider: "auto",
184053
184113
  timeoutMs: 60000
184054
184114
  },
@@ -184058,6 +184118,13 @@ var defaultConfig = {
184058
184118
  timeoutMs: 90000,
184059
184119
  allowDemotion: false
184060
184120
  },
184121
+ reviewBudget: {
184122
+ maxModelCalls: 4,
184123
+ maxTotalWallTimeMs: 480000,
184124
+ maxAgentOutputBytes: 65536,
184125
+ maxFindingsPerAgent: 10,
184126
+ skipOptionalPhasesWhenTokenUsageUnknown: true
184127
+ },
184061
184128
  audit: {
184062
184129
  enabled: true,
184063
184130
  format: "jsonl",
@@ -184068,17 +184135,21 @@ var defaultConfig = {
184068
184135
  };
184069
184136
 
184070
184137
  // src/config/loadConfig.ts
184071
- import { access, readFile as readFile3 } from "node:fs/promises";
184138
+ import { access, readFile as readFile3, realpath } from "node:fs/promises";
184072
184139
  import { homedir as homedir2 } from "node:os";
184073
184140
  import { stderr, stdin } from "node:process";
184074
- import { extname as extname2, join as join2, resolve as resolve2 } from "node:path";
184141
+ import { dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
184075
184142
  import { createInterface } from "node:readline/promises";
184076
184143
 
184077
184144
  // src/config/projectScope.ts
184078
184145
  var PROJECT_GLOBAL_ONLY_MESSAGE = "Move global-only settings to the user global config:";
184146
+ var PROJECT_GLOBAL_ONLY_REASONS = {
184147
+ "agents.codex.allowProjectProvider": "must be a user-global exact project-directory allowlist"
184148
+ };
184079
184149
  var kyosoConfigOverridePaths = [
184080
184150
  "agents.codex.enabled",
184081
184151
  "agents.codex.model",
184152
+ "agents.codex.provider",
184082
184153
  "agents.codex.effort",
184083
184154
  "agents.codex.role",
184084
184155
  "agents.codex.timeoutMs",
@@ -184118,6 +184189,11 @@ function collectProjectScopeViolations(config2) {
184118
184189
  const violations = [];
184119
184190
  for (const leaf of leaves) {
184120
184191
  const path = leaf.path.join(".");
184192
+ const globalOnlyReason = projectGlobalOnlyReason(leaf.path);
184193
+ if (globalOnlyReason) {
184194
+ violations.push({ path, reason: globalOnlyReason });
184195
+ continue;
184196
+ }
184121
184197
  if (!isAllowedProjectPath(leaf.path)) {
184122
184198
  violations.push({ path });
184123
184199
  continue;
@@ -184128,6 +184204,15 @@ function collectProjectScopeViolations(config2) {
184128
184204
  }
184129
184205
  return violations.sort((left, right) => left.path.localeCompare(right.path));
184130
184206
  }
184207
+ function projectGlobalOnlyReason(path) {
184208
+ const exactReason = PROJECT_GLOBAL_ONLY_REASONS[path.join(".")];
184209
+ if (exactReason)
184210
+ return exactReason;
184211
+ if (path[0] === "reviewBudget") {
184212
+ return "must be a user-global review budget ceiling";
184213
+ }
184214
+ return;
184215
+ }
184131
184216
  function isAllowedProjectPath(path) {
184132
184217
  const [top, second, third, fourth] = path;
184133
184218
  if (isAllowedConfigOverridePath(path))
@@ -184258,12 +184343,6 @@ function isRecord(value) {
184258
184343
  // src/security/redact.ts
184259
184344
  var REDACTION = "[KYOSO_REDACTED]";
184260
184345
 
184261
- // src/core/constants.ts
184262
- var DEFAULT_AGENT_TIMEOUT_MS = 120000;
184263
- var RAW_OUTPUT_MAX_CHARS = 16384;
184264
- var TRACE_DIR = ".kyoso/traces";
184265
- var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
184266
-
184267
184346
  // src/security/sanitizeText.ts
184268
184347
  var SENSITIVE_TEXT_PATTERNS = [
184269
184348
  /\bsk-(?:proj-)?[A-Za-z0-9_-]{8,}\b/g,
@@ -184278,7 +184357,7 @@ function sanitizeText(value) {
184278
184357
  return SENSITIVE_TEXT_PATTERNS.reduce((text, pattern) => text.replace(pattern, REDACTION), value);
184279
184358
  }
184280
184359
  function sanitizeTextForDisplay(value, maxChars = 240) {
184281
- const compact = sanitizeText(value).replace(/\s+/g, " ").trim();
184360
+ const compact = sanitizeText(value).replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "").replace(/\s+/g, " ").trim();
184282
184361
  if (compact.length <= maxChars)
184283
184362
  return compact;
184284
184363
  return `${compact.slice(0, Math.max(0, maxChars - 3))}...`;
@@ -185322,6 +185401,25 @@ function isMissingPathError(error51) {
185322
185401
  }
185323
185402
 
185324
185403
  // src/config/loadConfig.ts
185404
+ var configValidationContexts = new WeakMap;
185405
+ class ProjectOpenRouterAuthorizationError extends Error {
185406
+ code = "PROJECT_OPENROUTER_AUTHORIZATION_REQUIRED";
185407
+ projectPath;
185408
+ projectDirectory;
185409
+ layer;
185410
+ globalConfigPath;
185411
+ constructor(input) {
185412
+ const projectPath = sanitizeWarningText(input.projectPath);
185413
+ const projectDirectory = sanitizeWarningText(input.projectDirectory);
185414
+ const globalConfigPath = sanitizeWarningText(input.globalConfigPath);
185415
+ super(`Project config ${projectPath} changes Codex OpenRouter routing, but its directory ${projectDirectory} is not in the user-global allowlist. Add ${JSON.stringify(projectDirectory)} to agents.codex.allowProjectProvider in ${globalConfigPath} to permit project-level external provider routing.`);
185416
+ this.name = "ProjectOpenRouterAuthorizationError";
185417
+ this.projectPath = projectPath;
185418
+ this.projectDirectory = projectDirectory;
185419
+ this.layer = input.layer;
185420
+ this.globalConfigPath = globalConfigPath;
185421
+ }
185422
+ }
185325
185423
  var KNOWN_GLOBAL_CONFIG_LEAF_PATHS = new Set(kyosoConfigKnownLeafPaths);
185326
185424
  var GLOBAL_CONFIG_RECORD_PREFIXES = kyosoConfigRecordPrefixes.map((path) => path.split("."));
185327
185425
  var SECURITY_SENSITIVE_GLOBAL_PREFIXES = kyosoConfigSecuritySensitivePrefixes.map((path) => path.split("."));
@@ -185349,11 +185447,12 @@ async function loadConfig(options = {}) {
185349
185447
  }
185350
185448
  if (options.configPath) {
185351
185449
  const explicitConfigPath = resolve2(cwd, options.configPath);
185352
- if (!await exists(explicitConfigPath)) {
185450
+ const projectConfig = await resolveProjectConfigIdentity(explicitConfigPath);
185451
+ if (!projectConfig) {
185353
185452
  throw new Error(`Config file not found: ${explicitConfigPath} (from --config)`);
185354
185453
  }
185355
185454
  const loaded = await loadProjectConfig({
185356
- configPath: explicitConfigPath,
185455
+ projectConfig,
185357
185456
  baseConfig: mergedConfig,
185358
185457
  globalConfigPath,
185359
185458
  options
@@ -185367,19 +185466,29 @@ async function loadConfig(options = {}) {
185367
185466
  } else {
185368
185467
  const projectTomlPath = resolve2(cwd, "kyoso.toml");
185369
185468
  const projectTsPath = resolve2(cwd, "kyoso.config.ts");
185370
- const hasProjectToml = await exists(projectTomlPath);
185371
- const hasProjectTs = await exists(projectTsPath);
185372
- if (hasProjectToml) {
185373
- mergedConfig = mergeProjectTomlConfig(mergedConfig, await loadTomlConfigFile(projectTomlPath), { projectPath: projectTomlPath, globalConfigPath });
185374
- configPath = projectTomlPath;
185375
- sources.push({ path: projectTomlPath, layer: "project_toml" });
185376
- if (hasProjectTs) {
185469
+ const projectToml = await resolveProjectConfigIdentity(projectTomlPath);
185470
+ const projectTs = await resolveProjectConfigIdentity(projectTsPath);
185471
+ if (projectToml) {
185472
+ const loaded = await loadProjectConfig({
185473
+ projectConfig: projectToml,
185474
+ baseConfig: mergedConfig,
185475
+ globalConfigPath,
185476
+ options
185477
+ });
185478
+ mergedConfig = loaded.mergedConfig;
185479
+ configPath = loaded.configPath;
185480
+ configHash = loaded.configHash;
185481
+ configTrustStatus = loaded.configTrustStatus;
185482
+ sources.push(loaded.source);
185483
+ warnings.push(...loaded.warnings);
185484
+ if (projectTs) {
185377
185485
  warnings.push(`kyoso.config.ts was ignored because kyoso.toml takes precedence: ${projectTsPath}`);
185378
185486
  }
185379
- } else if (hasProjectTs) {
185487
+ } else if (projectTs) {
185380
185488
  const loaded = await loadProjectTsConfig({
185381
- configPath: projectTsPath,
185489
+ projectConfig: projectTs,
185382
185490
  baseConfig: mergedConfig,
185491
+ globalConfigPath,
185383
185492
  options
185384
185493
  });
185385
185494
  mergedConfig = loaded.mergedConfig;
@@ -185391,9 +185500,17 @@ async function loadConfig(options = {}) {
185391
185500
  }
185392
185501
  }
185393
185502
  }
185394
- const parsed = kyosoConfigSchema.parse(mergedConfig);
185503
+ const parsed = kyosoConfigSchema.safeParse(mergedConfig);
185504
+ if (!parsed.success) {
185505
+ const source = sources.at(-1);
185506
+ configValidationContexts.set(parsed.error, {
185507
+ source,
185508
+ projectTsExecuted: source?.layer === "project_ts" && isTrustedProjectConfigExecution(configTrustStatus)
185509
+ });
185510
+ throw parsed.error;
185511
+ }
185395
185512
  return {
185396
- config: parsed,
185513
+ config: parsed.data,
185397
185514
  configPath,
185398
185515
  configHash,
185399
185516
  configTrustStatus,
@@ -185441,62 +185558,170 @@ function pathStartsWith(path, prefix) {
185441
185558
  return path.length >= prefix.length && prefix.every((part, index) => path[index] === part);
185442
185559
  }
185443
185560
  async function loadProjectConfig(input) {
185444
- const extension = extname2(input.configPath);
185561
+ const { canonicalDirectory, canonicalPath, requestedPath } = input.projectConfig;
185562
+ const extension = extname2(requestedPath);
185445
185563
  if (extension === ".toml") {
185564
+ const projectTomlConfig = await loadTomlConfigFile(canonicalPath);
185565
+ const projectChangesOpenRouterRoute = projectConfigChangesOpenRouterRoute(projectTomlConfig, input.baseConfig);
185566
+ await assertProjectOpenRouterAuthorization({
185567
+ projectConfig: projectTomlConfig,
185568
+ projectPath: requestedPath,
185569
+ projectDirectory: canonicalDirectory,
185570
+ layer: "project_toml",
185571
+ baseConfig: input.baseConfig,
185572
+ globalConfigPath: input.globalConfigPath
185573
+ });
185574
+ const projectMergedConfig = mergeProjectTomlConfig(input.baseConfig, projectTomlConfig, {
185575
+ projectPath: requestedPath,
185576
+ globalConfigPath: input.globalConfigPath
185577
+ });
185446
185578
  return {
185447
- mergedConfig: mergeProjectTomlConfig(input.baseConfig, await loadTomlConfigFile(input.configPath), {
185448
- projectPath: input.configPath,
185449
- globalConfigPath: input.globalConfigPath
185450
- }),
185451
- configPath: input.configPath,
185579
+ mergedConfig: applyProjectCodexProviderReset(input.baseConfig, projectTomlConfig, projectMergedConfig),
185580
+ configPath: requestedPath,
185452
185581
  configTrustStatus: "not_found",
185453
- source: { path: input.configPath, layer: "project_toml" },
185454
- warnings: []
185582
+ source: { path: requestedPath, layer: "project_toml" },
185583
+ warnings: projectChangesOpenRouterRoute ? [openRouterProjectConfigWarning(requestedPath)] : []
185455
185584
  };
185456
185585
  }
185457
185586
  if (extension === ".ts") {
185458
185587
  return await loadProjectTsConfig(input);
185459
185588
  }
185460
- throw new Error(`Unsupported config file extension for ${input.configPath}. Expected .toml or .ts.`);
185589
+ throw new Error(`Unsupported config file extension for ${requestedPath}. Expected .toml or .ts.`);
185590
+ }
185591
+ function projectConfigSelectsOpenRouter(config2) {
185592
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_OPENROUTER_PROVIDER);
185593
+ }
185594
+ function projectConfigSelectsDefaultProvider(config2) {
185595
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_DEFAULT_PROVIDER);
185596
+ }
185597
+ function projectConfigSuppliesCodexModel(config2) {
185598
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.model");
185599
+ }
185600
+ function projectConfigChangesOpenRouterRoute(projectConfig, baseConfig) {
185601
+ return projectConfigSelectsOpenRouter(projectConfig) || configSelectsOpenRouter(baseConfig) && projectConfigSuppliesCodexModel(projectConfig) && !projectConfigSelectsDefaultProvider(projectConfig);
185602
+ }
185603
+ function configSelectsOpenRouter(config2) {
185604
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_OPENROUTER_PROVIDER);
185605
+ }
185606
+ function applyProjectCodexProviderReset(baseConfig, projectConfig, mergedConfig) {
185607
+ if (!projectConfigSelectsDefaultProvider(projectConfig) || projectConfigSuppliesCodexModel(projectConfig) || !configSelectsOpenRouter(baseConfig) || !isRecord3(mergedConfig) || !isRecord3(mergedConfig.agents) || !isRecord3(mergedConfig.agents.codex)) {
185608
+ return mergedConfig;
185609
+ }
185610
+ const codexWithoutInheritedModel = Object.fromEntries(Object.entries(mergedConfig.agents.codex).filter(([key]) => key !== "model"));
185611
+ return {
185612
+ ...mergedConfig,
185613
+ agents: {
185614
+ ...mergedConfig.agents,
185615
+ codex: codexWithoutInheritedModel
185616
+ }
185617
+ };
185618
+ }
185619
+ function openRouterProjectConfigWarning(configPath) {
185620
+ return `Project config ${sanitizeWarningText(configPath)} changes Codex OpenRouter routing under user-global authorization; it can route Codex review content through OpenRouter.`;
185621
+ }
185622
+ async function assertProjectOpenRouterAuthorization(input) {
185623
+ if (!projectConfigChangesOpenRouterRoute(input.projectConfig, input.baseConfig)) {
185624
+ return;
185625
+ }
185626
+ if (await projectProviderIsAuthorized(input.baseConfig, input.projectDirectory)) {
185627
+ return;
185628
+ }
185629
+ throw new ProjectOpenRouterAuthorizationError({
185630
+ projectPath: input.projectPath,
185631
+ projectDirectory: input.projectDirectory,
185632
+ layer: input.layer,
185633
+ globalConfigPath: input.globalConfigPath
185634
+ });
185635
+ }
185636
+ async function projectProviderIsAuthorized(config2, projectDirectory) {
185637
+ if (!isRecord3(config2))
185638
+ return false;
185639
+ const agents = config2.agents;
185640
+ if (!isRecord3(agents))
185641
+ return false;
185642
+ const codex = agents.codex;
185643
+ if (!isRecord3(codex) || !Array.isArray(codex.allowProjectProvider)) {
185644
+ return false;
185645
+ }
185646
+ for (const directory of codex.allowProjectProvider) {
185647
+ if (typeof directory !== "string" || !isAbsolute2(directory))
185648
+ continue;
185649
+ const allowedDirectory = await existingRealpath(directory);
185650
+ if (allowedDirectory === projectDirectory)
185651
+ return true;
185652
+ }
185653
+ return false;
185654
+ }
185655
+ async function resolveProjectConfigIdentity(requestedPath) {
185656
+ const canonicalPath = await existingRealpath(requestedPath);
185657
+ return canonicalPath === undefined ? undefined : {
185658
+ requestedPath,
185659
+ canonicalPath,
185660
+ canonicalDirectory: dirname3(canonicalPath)
185661
+ };
185662
+ }
185663
+ async function existingRealpath(path) {
185664
+ try {
185665
+ return await realpath(path);
185666
+ } catch {
185667
+ return;
185668
+ }
185461
185669
  }
185462
185670
  async function loadProjectTsConfig(input) {
185671
+ const { canonicalDirectory, canonicalPath, requestedPath } = input.projectConfig;
185463
185672
  const warnings = [
185464
185673
  'kyoso.config.ts is deprecated; migrate to kyoso.toml (see README "Configuration")'
185465
185674
  ];
185466
- const source = await readFile3(input.configPath, "utf8");
185675
+ const source = await readFile3(canonicalPath, "utf8");
185467
185676
  const configHash = hashConfigSource(source);
185468
185677
  const trustStorePath = input.options.trustStorePath ?? defaultTrustedConfigStorePath(input.options.env);
185469
- const trusted = await isTrustedConfig(trustStorePath, input.configPath, configHash);
185678
+ const trusted = await isTrustedConfig(trustStorePath, canonicalPath, configHash);
185470
185679
  const trustDecision = await resolveTrustDecision({
185471
- configPath: input.configPath,
185680
+ configPath: requestedPath,
185472
185681
  configHash,
185473
185682
  trusted,
185474
185683
  options: input.options
185475
185684
  });
185476
185685
  if (trustDecision.execute) {
185477
- const userConfig = await loadUserConfig(input.configPath, source);
185686
+ const userConfig = await loadUserConfig(canonicalPath, source);
185687
+ const projectChangesOpenRouterRoute = projectConfigChangesOpenRouterRoute(userConfig, input.baseConfig);
185688
+ await assertProjectOpenRouterAuthorization({
185689
+ projectConfig: userConfig,
185690
+ projectPath: requestedPath,
185691
+ projectDirectory: canonicalDirectory,
185692
+ layer: "project_ts",
185693
+ baseConfig: input.baseConfig,
185694
+ globalConfigPath: input.globalConfigPath
185695
+ });
185478
185696
  if (trustDecision.shouldPersist) {
185479
- await trustConfig(trustStorePath, input.configPath, configHash);
185697
+ await trustConfig(trustStorePath, canonicalPath, configHash);
185698
+ }
185699
+ const projectMergedConfig = deepMerge2(input.baseConfig, userConfig);
185700
+ if (projectChangesOpenRouterRoute) {
185701
+ warnings.push(openRouterProjectConfigWarning(requestedPath));
185480
185702
  }
185481
185703
  return {
185482
- mergedConfig: deepMerge2(input.baseConfig, userConfig),
185483
- configPath: input.configPath,
185704
+ mergedConfig: applyProjectCodexProviderReset(input.baseConfig, userConfig, projectMergedConfig),
185705
+ configPath: requestedPath,
185484
185706
  configHash,
185485
185707
  configTrustStatus: trustDecision.status,
185486
- source: { path: input.configPath, layer: "project_ts" },
185708
+ source: { path: requestedPath, layer: "project_ts" },
185487
185709
  warnings
185488
185710
  };
185489
185711
  }
185490
- warnings.push(`untrusted config was not executed: ${input.configPath}; run \`kyoso doctor --trust-config\` or pass \`--trust-config\` once to trust it`);
185712
+ warnings.push(`untrusted config was not executed: ${requestedPath}; run \`kyoso doctor --trust-config\` or pass \`--trust-config\` once to trust it`);
185491
185713
  return {
185492
185714
  mergedConfig: input.baseConfig,
185493
- configPath: input.configPath,
185715
+ configPath: requestedPath,
185494
185716
  configHash,
185495
185717
  configTrustStatus: trustDecision.status,
185496
- source: { path: input.configPath, layer: "project_ts" },
185718
+ source: { path: requestedPath, layer: "project_ts" },
185497
185719
  warnings
185498
185720
  };
185499
185721
  }
185722
+ function isTrustedProjectConfigExecution(status) {
185723
+ return status === "trusted" || status === "trusted_by_flag" || status === "trusted_interactively";
185724
+ }
185500
185725
  async function loadUserConfig(configPath, source) {
185501
185726
  try {
185502
185727
  const loaded = await loadConfigModule(configPath, source);
@@ -185575,6 +185800,8 @@ function applyConfigOverrides(config2, assignments) {
185575
185800
  for (const override of overrides) {
185576
185801
  writePath2(overridden, override.path, parseConfigOverrideValue(override.value, readPath2(baseConfig, override.path)));
185577
185802
  }
185803
+ clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, overrides);
185804
+ assertOpenRouterProviderOverrideIncludesModel(baseConfig, overridden, overrides);
185578
185805
  const parsed = kyosoConfigSchema.safeParse(overridden);
185579
185806
  if (parsed.success)
185580
185807
  return parsed.data;
@@ -185583,6 +185810,29 @@ function applyConfigOverrides(config2, assignments) {
185583
185810
  const assignment = findAssignmentForPath(overrides, issuePath) ?? assignments.at(-1) ?? "";
185584
185811
  throw new Error(`Invalid --set value ${JSON.stringify(assignment)}: ${issuePath}: ${issue2?.message ?? "config validation failed"}.`);
185585
185812
  }
185813
+ function assertOpenRouterProviderOverrideIncludesModel(baseConfig, overridden, overrides) {
185814
+ const providerPath = ["agents", "codex", "provider"];
185815
+ const modelPath = ["agents", "codex", "model"];
185816
+ const selectsOpenRouter = readPath2(overridden, providerPath) === CODEX_OPENROUTER_PROVIDER;
185817
+ const alreadySelected = readPath2(baseConfig, providerPath) === CODEX_OPENROUTER_PROVIDER;
185818
+ const suppliesModel = overrides.some((override) => override.path.join(".") === modelPath.join("."));
185819
+ if (!selectsOpenRouter || alreadySelected || suppliesModel)
185820
+ return;
185821
+ const assignment = findAssignmentForPath(overrides, providerPath.join(".")) ?? "agents.codex.provider=openrouter";
185822
+ throw new Error(`Invalid --set value ${JSON.stringify(assignment)}: selecting agents.codex.provider=openrouter requires agents.codex.model in the same --set invocation.`);
185823
+ }
185824
+ function clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, overrides) {
185825
+ const providerPath = ["agents", "codex", "provider"];
185826
+ const modelPath = ["agents", "codex", "model"];
185827
+ const selectsDefaultProvider = overrides.some((override) => override.path.join(".") === providerPath.join(".") && override.value === CODEX_DEFAULT_PROVIDER);
185828
+ const suppliesModel = overrides.some((override) => override.path.join(".") === modelPath.join("."));
185829
+ if (!selectsDefaultProvider || suppliesModel || readPath2(baseConfig, providerPath) !== CODEX_OPENROUTER_PROVIDER || readPath2(overridden, providerPath) !== CODEX_DEFAULT_PROVIDER) {
185830
+ return;
185831
+ }
185832
+ const codex = readPath2(overridden, ["agents", "codex"]);
185833
+ if (isRecord4(codex))
185834
+ delete codex.model;
185835
+ }
185586
185836
  function findAssignmentForPath(overrides, path) {
185587
185837
  for (let index = overrides.length - 1;index >= 0; index -= 1) {
185588
185838
  const override = overrides[index];
@@ -185650,8 +185900,8 @@ function isRecord4(value) {
185650
185900
 
185651
185901
  // src/acp/AcpAgentProcess.ts
185652
185902
  import { spawn } from "node:child_process";
185653
- import { readFile as readFile4, realpath } from "node:fs/promises";
185654
- import { isAbsolute, relative, resolve as resolve3 } from "node:path";
185903
+ import { readFile as readFile4, realpath as realpath2 } from "node:fs/promises";
185904
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve3 } from "node:path";
185655
185905
  import { Readable, Writable } from "node:stream";
185656
185906
 
185657
185907
  // node_modules/@agentclientprotocol/sdk/dist/schema/index.js
@@ -189582,7 +189832,35 @@ var legacyClientNotificationMethods = new Set([
189582
189832
  CLIENT_METHODS.elicitation_complete
189583
189833
  ]);
189584
189834
 
189835
+ // src/core/tokenUsage.ts
189836
+ var TOKEN_USAGE_KEYS = [
189837
+ "totalTokens",
189838
+ "inputTokens",
189839
+ "outputTokens",
189840
+ "thoughtTokens",
189841
+ "cachedReadTokens",
189842
+ "cachedWriteTokens"
189843
+ ];
189844
+ function normalizeModelTokenUsage(usage) {
189845
+ if (!isRecord6(usage))
189846
+ return;
189847
+ const normalized = {};
189848
+ for (const key of TOKEN_USAGE_KEYS) {
189849
+ const value = usage[key];
189850
+ if (isTokenCount(value))
189851
+ normalized[key] = value;
189852
+ }
189853
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
189854
+ }
189855
+ function isTokenCount(value) {
189856
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
189857
+ }
189858
+ function isRecord6(value) {
189859
+ return typeof value === "object" && value !== null && !Array.isArray(value);
189860
+ }
189861
+
189585
189862
  // src/utils/env.ts
189863
+ import { stderr as stderr2 } from "node:process";
189586
189864
  var MINIMAL_ENV_KEYS = [
189587
189865
  "PATH",
189588
189866
  "HOME",
@@ -189596,29 +189874,110 @@ var MINIMAL_ENV_KEYS = [
189596
189874
  "USERNAME",
189597
189875
  "SystemRoot"
189598
189876
  ];
189877
+ var CREDENTIAL_ENV_KEYS = new Set([
189878
+ "ANTHROPIC_API_KEY",
189879
+ "CLAUDE_CODE_OAUTH_TOKEN",
189880
+ "CODEX_ACCESS_TOKEN",
189881
+ "CODEX_API_KEY",
189882
+ "OPENAI_API_KEY",
189883
+ "OPENROUTER_API_KEY"
189884
+ ]);
189885
+ var CREDENTIAL_LIKE_ENV_KEY_PATTERN = /(?:^|_)(?:KEY|TOKEN|SECRET|PASSWORD)$/i;
189886
+ var UNEXPANDED_ENV_PLACEHOLDER_PATTERN = /^\s*(?:\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%)\s*$/;
189887
+ var OPENROUTER_EXCLUDED_CREDENTIAL_ENV_KEYS = [
189888
+ "OPENAI_API_KEY",
189889
+ "CODEX_API_KEY",
189890
+ "CODEX_ACCESS_TOKEN"
189891
+ ];
189892
+ var OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
189893
+ var KYOSO_OPENROUTER_PROVIDER_ID = "kyoso-openrouter";
189894
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
189895
+ var OPENROUTER_PROVIDER_PRESET = {
189896
+ name: "OpenRouter",
189897
+ base_url: OPENROUTER_BASE_URL,
189898
+ env_key: OPENROUTER_API_KEY_ENV,
189899
+ wire_api: "responses",
189900
+ requires_openai_auth: false
189901
+ };
189902
+
189903
+ class ChildEnvPreflightError extends Error {
189904
+ code;
189905
+ constructor(code, message) {
189906
+ super(message);
189907
+ this.code = code;
189908
+ this.name = "ChildEnvPreflightError";
189909
+ }
189910
+ }
189599
189911
  function buildChildEnv(parentEnv, whitelist, explicit, options = {}) {
189600
189912
  if (!parentEnv.PATH) {
189601
189913
  throw new Error("PATH is required to launch ACP child agents.");
189602
189914
  }
189603
189915
  const env = {};
189916
+ const warnedCredentialPlaceholderKeys = new Set;
189917
+ const onCredentialPlaceholderDiscarded = (key) => {
189918
+ if (warnedCredentialPlaceholderKeys.has(key))
189919
+ return;
189920
+ warnedCredentialPlaceholderKeys.add(key);
189921
+ (options.onCredentialPlaceholderDiscarded ?? warnCredentialPlaceholderDiscarded)(key);
189922
+ };
189923
+ const openRouterSelected = options.agent === "codex" && options.provider === CODEX_OPENROUTER_PROVIDER;
189604
189924
  for (const key of MINIMAL_ENV_KEYS) {
189605
189925
  if (parentEnv[key])
189606
189926
  env[key] = parentEnv[key];
189607
189927
  }
189608
189928
  for (const key of whitelist) {
189609
- if (parentEnv[key])
189929
+ if (key === OPENROUTER_API_KEY_ENV && !openRouterSelected)
189930
+ continue;
189931
+ if (parentEnv[key] && canCopyEnvValue(key, parentEnv[key], onCredentialPlaceholderDiscarded)) {
189610
189932
  env[key] = parentEnv[key];
189933
+ }
189611
189934
  }
189612
189935
  for (const [key, value] of Object.entries(explicit)) {
189613
- env[key] = value;
189936
+ if (key === OPENROUTER_API_KEY_ENV && !openRouterSelected) {
189937
+ if (value.trim().length > 0) {
189938
+ (options.onOpenRouterCredentialWithheld ?? warnOpenRouterCredentialWithheld)(key);
189939
+ }
189940
+ continue;
189941
+ }
189942
+ if (canCopyEnvValue(key, value, onCredentialPlaceholderDiscarded)) {
189943
+ env[key] = value;
189944
+ }
189945
+ }
189946
+ if (openRouterSelected) {
189947
+ discardOpenRouterExcludedCredentials(env);
189948
+ applyOpenRouterConfig(env, parentEnv, options.model, onCredentialPlaceholderDiscarded, options.onOpenRouterProvidersDiscarded ?? warnOpenRouterProvidersDiscarded);
189949
+ } else {
189950
+ applyModelConfig(env, options.agent, options.model);
189614
189951
  }
189615
- applyModelConfig(env, options.agent, options.model);
189616
189952
  env.KYOSO_CHILD_AGENT = "1";
189617
189953
  if (options.agent === "claude") {
189618
189954
  applyClaudeAuthPreference(env, options.preferApiKey === true);
189619
189955
  }
189620
189956
  return env;
189621
189957
  }
189958
+ function canCopyEnvValue(key, value, onCredentialPlaceholderDiscarded) {
189959
+ if (!isUnexpandedCredentialEnvValue(key, value))
189960
+ return true;
189961
+ (onCredentialPlaceholderDiscarded ?? warnCredentialPlaceholderDiscarded)(key);
189962
+ return false;
189963
+ }
189964
+ function warnCredentialPlaceholderDiscarded(key) {
189965
+ stderr2.write(`kyoso: ignored an unexpanded credential placeholder for ${key}; ensure the client expands it before starting Kyoso.
189966
+ `);
189967
+ }
189968
+ function warnOpenRouterCredentialWithheld(key) {
189969
+ stderr2.write(`kyoso: ignored explicit ${key} because the OpenRouter provider is not selected for this child.
189970
+ `);
189971
+ }
189972
+ function warnOpenRouterProvidersDiscarded(count) {
189973
+ stderr2.write(`kyoso: discarded foreign CODEX_CONFIG.model_providers entries: ${count}; provider IDs and configuration values were not displayed.
189974
+ `);
189975
+ }
189976
+ function discardOpenRouterExcludedCredentials(env) {
189977
+ for (const key of OPENROUTER_EXCLUDED_CREDENTIAL_ENV_KEYS) {
189978
+ delete env[key];
189979
+ }
189980
+ }
189622
189981
  function applyModelConfig(env, agent, model) {
189623
189982
  if (!model)
189624
189983
  return;
@@ -189630,7 +189989,67 @@ function applyModelConfig(env, agent, model) {
189630
189989
  env.CODEX_CONFIG = JSON.stringify({ model });
189631
189990
  }
189632
189991
  }
189992
+ function applyOpenRouterConfig(env, parentEnv, model, onCredentialPlaceholderDiscarded, onOpenRouterProvidersDiscarded) {
189993
+ const configuredModel = model?.trim();
189994
+ if (!configuredModel) {
189995
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", 'agents.codex.provider="openrouter" requires a non-empty agents.codex.model.');
189996
+ }
189997
+ if (!hasEnv(env, OPENROUTER_API_KEY_ENV)) {
189998
+ const parentKey = nonEmptyEnv(parentEnv, OPENROUTER_API_KEY_ENV);
189999
+ if (parentKey)
190000
+ env[OPENROUTER_API_KEY_ENV] = parentKey;
190001
+ else if (isUnexpandedCredentialEnvValue(OPENROUTER_API_KEY_ENV, parentEnv[OPENROUTER_API_KEY_ENV] ?? "")) {
190002
+ onCredentialPlaceholderDiscarded(OPENROUTER_API_KEY_ENV);
190003
+ }
190004
+ }
190005
+ if (!hasEnv(env, OPENROUTER_API_KEY_ENV)) {
190006
+ throw new ChildEnvPreflightError("OPENROUTER_KEY_MISSING", 'agents.codex.provider="openrouter" requires OPENROUTER_API_KEY, but it is not visible to the Kyoso process. Add OPENROUTER_API_KEY to the MCP registration, restart the client, then run `kyoso doctor`.');
190007
+ }
190008
+ const config2 = parseCodexConfig(env.CODEX_CONFIG);
190009
+ assertOpenRouterConfigDoesNotSelectProfile(config2);
190010
+ if (config2.model_providers !== undefined && !isPlainObject2(config2.model_providers)) {
190011
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG.model_providers must be a JSON object for the OpenRouter provider.");
190012
+ }
190013
+ reportDiscardedOpenRouterProviders(config2.model_providers, onOpenRouterProvidersDiscarded);
190014
+ env.MODEL_PROVIDER = KYOSO_OPENROUTER_PROVIDER_ID;
190015
+ env.CODEX_CONFIG = JSON.stringify({
190016
+ ...config2,
190017
+ model: configuredModel,
190018
+ model_provider: KYOSO_OPENROUTER_PROVIDER_ID,
190019
+ model_providers: {
190020
+ [KYOSO_OPENROUTER_PROVIDER_ID]: OPENROUTER_PROVIDER_PRESET
190021
+ }
190022
+ });
190023
+ }
190024
+ function reportDiscardedOpenRouterProviders(modelProviders, onDiscarded) {
190025
+ if (!isPlainObject2(modelProviders))
190026
+ return;
190027
+ const foreignProviderCount = Object.keys(modelProviders).filter((providerId) => providerId !== KYOSO_OPENROUTER_PROVIDER_ID).length;
190028
+ if (foreignProviderCount > 0)
190029
+ onDiscarded(foreignProviderCount);
190030
+ }
190031
+ function parseCodexConfig(value) {
190032
+ if (!value)
190033
+ return {};
190034
+ let parsed;
190035
+ try {
190036
+ parsed = JSON.parse(value);
190037
+ } catch {
190038
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG must contain a JSON object for the OpenRouter provider.");
190039
+ }
190040
+ if (!isPlainObject2(parsed)) {
190041
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG must contain a JSON object for the OpenRouter provider.");
190042
+ }
190043
+ return parsed;
190044
+ }
190045
+ function assertOpenRouterConfigDoesNotSelectProfile(config2) {
190046
+ if (Object.hasOwn(config2, "profile") || Object.hasOwn(config2, "profiles")) {
190047
+ throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", "CODEX_CONFIG.profile and CODEX_CONFIG.profiles are not supported for the OpenRouter provider.");
190048
+ }
190049
+ }
189633
190050
  function applyClaudeAuthPreference(env, preferApiKey) {
190051
+ discardUnusableEnvValue(env, "ANTHROPIC_API_KEY");
190052
+ discardUnusableEnvValue(env, "CLAUDE_CODE_OAUTH_TOKEN");
189634
190053
  const hasApiKey = hasEnv(env, "ANTHROPIC_API_KEY");
189635
190054
  const hasOAuthToken = hasEnv(env, "CLAUDE_CODE_OAUTH_TOKEN");
189636
190055
  if (!hasApiKey || !hasOAuthToken)
@@ -189641,8 +190060,32 @@ function applyClaudeAuthPreference(env, preferApiKey) {
189641
190060
  delete env.ANTHROPIC_API_KEY;
189642
190061
  }
189643
190062
  }
190063
+ function discardUnusableEnvValue(env, key) {
190064
+ if (env[key] !== undefined && !hasEnv(env, key)) {
190065
+ delete env[key];
190066
+ }
190067
+ }
190068
+ function hasUsableEnvValue(env, key) {
190069
+ return nonEmptyEnv(env, key) !== undefined;
190070
+ }
190071
+ function isUnexpandedEnvPlaceholder(value) {
190072
+ return typeof value === "string" && UNEXPANDED_ENV_PLACEHOLDER_PATTERN.test(value);
190073
+ }
189644
190074
  function hasEnv(env, key) {
189645
- return typeof env[key] === "string" && env[key].trim().length > 0;
190075
+ return hasUsableEnvValue(env, key);
190076
+ }
190077
+ function nonEmptyEnv(env, key) {
190078
+ const value = env[key];
190079
+ return typeof value === "string" && value.trim().length > 0 && !isUnexpandedCredentialEnvValue(key, value) ? value : undefined;
190080
+ }
190081
+ function isUnexpandedCredentialEnvValue(key, value) {
190082
+ return (CREDENTIAL_ENV_KEYS.has(key) || CREDENTIAL_LIKE_ENV_KEY_PATTERN.test(key)) && isUnexpandedEnvPlaceholder(value);
190083
+ }
190084
+ function isPlainObject2(value) {
190085
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
190086
+ return false;
190087
+ }
190088
+ return Object.getPrototypeOf(value) === Object.prototype;
189646
190089
  }
189647
190090
 
189648
190091
  // src/acp/AcpAgentManager.ts
@@ -189761,7 +190204,7 @@ function isSeverity(value) {
189761
190204
  return typeof value === "string" && severities.includes(value);
189762
190205
  }
189763
190206
  function normalizeCisaSecureByDesign(value) {
189764
- if (!isRecord6(value))
190207
+ if (!isRecord7(value))
189765
190208
  return;
189766
190209
  const normalized = {};
189767
190210
  const customerSecurityOutcomes = normalizeGateStatus(value.customerSecurityOutcomes);
@@ -189802,7 +190245,7 @@ function normalizeFindingFiles(value) {
189802
190245
  if (!Array.isArray(value))
189803
190246
  return;
189804
190247
  const files = value.flatMap((item) => {
189805
- if (!isRecord6(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
190248
+ if (!isRecord7(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
189806
190249
  return [];
189807
190250
  }
189808
190251
  const file2 = {
@@ -189821,38 +190264,80 @@ function normalizeFindingFiles(value) {
189821
190264
  function normalizeLineNumber(value) {
189822
190265
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
189823
190266
  }
189824
- function isRecord6(value) {
190267
+ function isRecord7(value) {
189825
190268
  return typeof value === "object" && value !== null && !Array.isArray(value);
189826
190269
  }
189827
190270
 
189828
190271
  // src/acp/AcpAgentProcess.ts
189829
190272
  class SubprocessAcpAgentManager extends BaseAcpAgentManager {
189830
190273
  config;
189831
- constructor(config2) {
190274
+ parentEnv;
190275
+ constructor(config2, parentEnv = process.env) {
189832
190276
  super();
189833
190277
  this.config = config2;
190278
+ this.parentEnv = parentEnv;
189834
190279
  }
189835
190280
  async runAgent(input) {
189836
190281
  const agentConfig = this.config.agents[input.agent];
190282
+ const startedAt = new Date().toISOString();
189837
190283
  if (!agentConfig.enabled) {
189838
190284
  return {
189839
190285
  agent: input.agent,
189840
190286
  role: input.role,
189841
190287
  status: "skipped",
189842
- startedAt: new Date().toISOString(),
189843
- completedAt: new Date().toISOString()
190288
+ startedAt,
190289
+ completedAt: startedAt
190290
+ };
190291
+ }
190292
+ const provider = input.agent === "codex" ? this.config.agents.codex.provider : undefined;
190293
+ let env;
190294
+ try {
190295
+ env = buildChildEnv(this.parentEnv, agentConfig.auth.envWhitelist, agentConfig.env, {
190296
+ agent: input.agent,
190297
+ model: agentConfig.model,
190298
+ provider,
190299
+ preferApiKey: agentConfig.auth.preferApiKey
190300
+ });
190301
+ } catch (error51) {
190302
+ return {
190303
+ agent: input.agent,
190304
+ role: input.role,
190305
+ status: "failed",
190306
+ startedAt,
190307
+ completedAt: new Date().toISOString(),
190308
+ error: buildPreflightFailure(error51)
190309
+ };
190310
+ }
190311
+ try {
190312
+ return await runSubprocessAgent(input.agent, agentConfig, input, env);
190313
+ } catch (error51) {
190314
+ return {
190315
+ agent: input.agent,
190316
+ role: input.role,
190317
+ status: "failed",
190318
+ startedAt,
190319
+ completedAt: new Date().toISOString(),
190320
+ error: buildAgentFailure(formatAgentErrorDetail(error51), "Agent process failed.")
189844
190321
  };
189845
190322
  }
189846
- return runSubprocessAgent(input.agent, agentConfig, input);
189847
190323
  }
189848
190324
  }
189849
- async function runSubprocessAgent(agent, agentConfig, input) {
190325
+ async function runSubprocessAgent(agent, agentConfig, input, env) {
189850
190326
  const startedAt = new Date().toISOString();
189851
- const env = buildChildEnv(process.env, agentConfig.auth.envWhitelist, agentConfig.env, {
189852
- agent,
189853
- model: agentConfig.model,
189854
- preferApiKey: agentConfig.auth.preferApiKey
189855
- });
190327
+ const effectiveTimeoutMs = resolveEffectiveTimeoutMs(input);
190328
+ if (effectiveTimeoutMs <= 0) {
190329
+ return {
190330
+ agent,
190331
+ role: input.role,
190332
+ status: "timeout",
190333
+ startedAt,
190334
+ completedAt: startedAt,
190335
+ error: {
190336
+ code: "REVIEW_DEADLINE_EXCEEDED",
190337
+ message: "Review deadline was reached before the agent could start."
190338
+ }
190339
+ };
190340
+ }
189856
190341
  return new Promise((resolveResult) => {
189857
190342
  const child = spawn(agentConfig.command, agentConfig.args, {
189858
190343
  cwd: input.workspaceDir,
@@ -189860,19 +190345,28 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189860
190345
  stdio: ["pipe", "pipe", "pipe"]
189861
190346
  });
189862
190347
  let stdout = "";
189863
- let stderr2 = "";
190348
+ let stderr3 = "";
189864
190349
  let settled = false;
190350
+ let startedWrite;
190351
+ child.once("spawn", () => {
190352
+ if (settled)
190353
+ return;
190354
+ startedWrite = Promise.resolve().then(() => input.onStarted?.()).catch(() => {
190355
+ return;
190356
+ });
190357
+ });
189865
190358
  const abortController = new AbortController;
189866
190359
  const resolveOnce = (result) => {
189867
190360
  if (settled)
189868
190361
  return;
189869
190362
  settled = true;
189870
190363
  clearTimeout(timeout);
189871
- resolveResult(result);
190364
+ (startedWrite ?? Promise.resolve()).then(() => resolveResult(result));
189872
190365
  };
189873
190366
  const timeout = setTimeout(() => {
189874
190367
  abortController.abort(new Error("Kyoso agent timeout"));
189875
190368
  terminateChild(child);
190369
+ const deadlineReached = input.deadlineAtEpochMs !== undefined && Date.now() >= input.deadlineAtEpochMs;
189876
190370
  resolveOnce({
189877
190371
  agent,
189878
190372
  role: input.role,
@@ -189880,13 +190374,13 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189880
190374
  startedAt,
189881
190375
  completedAt: new Date().toISOString(),
189882
190376
  error: {
189883
- code: "AGENT_TIMEOUT",
189884
- message: `Agent timed out after ${input.timeoutMs}ms`
190377
+ code: deadlineReached ? "REVIEW_DEADLINE_EXCEEDED" : "AGENT_TIMEOUT",
190378
+ message: deadlineReached ? "Review deadline reached before the agent completed." : `Agent timed out after ${effectiveTimeoutMs}ms`
189885
190379
  }
189886
190380
  });
189887
- }, input.timeoutMs);
190381
+ }, effectiveTimeoutMs);
189888
190382
  child.stderr.on("data", (chunk) => {
189889
- stderr2 += chunk.toString("utf8");
190383
+ stderr3 += chunk.toString("utf8");
189890
190384
  });
189891
190385
  child.on("error", (error51) => {
189892
190386
  const failure = buildAgentFailure(error51.message, "Agent process could not be started.");
@@ -189900,22 +190394,51 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189900
190394
  error: failure
189901
190395
  });
189902
190396
  });
189903
- runAcpClientWorkflow(child, input, abortController.signal, resolveEffortConfigOption(agent, agentConfig.effort)).then(({ rawText, warnings }) => {
190397
+ runAcpClientWorkflow(child, input, abortController, resolveEffortConfigOption(agent, agentConfig.effort)).then(({ rawText, warnings, usage, outputBytes, stopReason }) => {
189904
190398
  stdout = rawText;
190399
+ const completed = stopReason === "end_turn";
189905
190400
  resolveOnce({
189906
190401
  agent,
189907
190402
  role: input.role,
189908
- status: "completed",
190403
+ status: completed ? "completed" : "failed",
189909
190404
  rawText,
189910
190405
  normalized: normalizeAgentOutput(agent, input.role, rawText),
189911
190406
  startedAt,
189912
190407
  completedAt: new Date().toISOString(),
189913
- ...warnings.length > 0 ? { warnings } : {}
190408
+ outputBytes,
190409
+ stopReason,
190410
+ ...usage ? { usage } : {},
190411
+ ...warnings.length > 0 ? { warnings } : {},
190412
+ ...completed ? {} : {
190413
+ error: {
190414
+ code: "AGENT_STOPPED_EARLY",
190415
+ message: `Agent stopped before completing the review: ${stopReason}.`
190416
+ }
190417
+ }
189914
190418
  });
189915
190419
  }).catch((error51) => {
190420
+ const outputLimitError = findOutputLimitError(error51, abortController);
190421
+ if (outputLimitError) {
190422
+ stdout = outputLimitError.rawText;
190423
+ resolveOnce({
190424
+ agent,
190425
+ role: input.role,
190426
+ status: "failed",
190427
+ rawText: stdout,
190428
+ outputBytes: outputLimitError.outputBytes,
190429
+ stopReason: "cancelled",
190430
+ startedAt,
190431
+ completedAt: new Date().toISOString(),
190432
+ error: {
190433
+ code: "AGENT_OUTPUT_LIMIT",
190434
+ message: `Agent output exceeded ${outputLimitError.maxOutputBytes} bytes and was cancelled.`
190435
+ }
190436
+ });
190437
+ return;
190438
+ }
189916
190439
  if (abortController.signal.aborted)
189917
190440
  return;
189918
- const failureText = [stderr2, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
190441
+ const failureText = [stderr3, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
189919
190442
  `);
189920
190443
  resolveOnce({
189921
190444
  agent,
@@ -189940,12 +190463,12 @@ async function runSubprocessAgent(agent, agentConfig, input) {
189940
190463
  rawText: stdout,
189941
190464
  startedAt,
189942
190465
  completedAt: new Date().toISOString(),
189943
- error: buildAgentFailure(stderr2, fallback)
190466
+ error: buildAgentFailure(stderr3, fallback)
189944
190467
  });
189945
190468
  });
189946
190469
  });
189947
190470
  }
189948
- async function runAcpClientWorkflow(child, input, signal, configOption) {
190471
+ async function runAcpClientWorkflow(child, input, abortController, configOption) {
189949
190472
  if (!child.stdin || !child.stdout) {
189950
190473
  throw new Error("Agent process did not expose stdio streams.");
189951
190474
  }
@@ -189995,8 +190518,8 @@ async function runAcpClientWorkflow(child, input, signal, configOption) {
189995
190518
  }).withSession(async (session) => {
189996
190519
  const warnings = [];
189997
190520
  if (configOption) {
189998
- await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: signal }).catch((error51) => {
189999
- if (signal.aborted)
190521
+ await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: abortController.signal }).catch((error51) => {
190522
+ if (abortController.signal.aborted)
190000
190523
  return;
190001
190524
  const sanitizedValue = sanitizeTextForDisplay(configOption.value);
190002
190525
  const detail = sanitizeTextForDisplay(formatAgentErrorDetail(error51));
@@ -190006,14 +190529,75 @@ async function runAcpClientWorkflow(child, input, signal, configOption) {
190006
190529
  });
190007
190530
  }
190008
190531
  const promptResponse = session.prompt(input.prompt, {
190009
- cancellationSignal: signal
190532
+ cancellationSignal: abortController.signal
190533
+ });
190534
+ promptResponse.catch(() => {
190535
+ return;
190010
190536
  });
190011
- const text = await session.readText();
190012
- await promptResponse;
190013
- return { rawText: text, warnings };
190537
+ let rawText = "";
190538
+ let outputBytes = 0;
190539
+ for (;; ) {
190540
+ const message = await session.nextUpdate();
190541
+ if (message.kind === "stop") {
190542
+ const usage = normalizeUsage(message.response.usage);
190543
+ return {
190544
+ rawText,
190545
+ warnings,
190546
+ ...usage ? { usage } : {},
190547
+ outputBytes,
190548
+ stopReason: message.stopReason
190549
+ };
190550
+ }
190551
+ const update = message.update;
190552
+ if (update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "agent_thought_chunk" || update.content.type !== "text") {
190553
+ continue;
190554
+ }
190555
+ const chunkBytes = Buffer.byteLength(update.content.text, "utf8");
190556
+ const nextOutputBytes = outputBytes + chunkBytes;
190557
+ if (input.maxOutputBytes !== undefined && nextOutputBytes > input.maxOutputBytes) {
190558
+ await ctx.notify(methods.agent.session.cancel, {
190559
+ sessionId: session.sessionId
190560
+ }).catch(() => {
190561
+ return;
190562
+ });
190563
+ const error51 = new AgentOutputLimitError(rawText, nextOutputBytes, input.maxOutputBytes);
190564
+ abortController.abort(error51);
190565
+ throw error51;
190566
+ }
190567
+ if (update.sessionUpdate === "agent_message_chunk") {
190568
+ rawText += update.content.text;
190569
+ }
190570
+ outputBytes = nextOutputBytes;
190571
+ }
190014
190572
  });
190015
190573
  });
190016
190574
  }
190575
+
190576
+ class AgentOutputLimitError extends Error {
190577
+ rawText;
190578
+ outputBytes;
190579
+ maxOutputBytes;
190580
+ constructor(rawText, outputBytes, maxOutputBytes) {
190581
+ super(`Agent output exceeded ${maxOutputBytes} bytes.`);
190582
+ this.rawText = rawText;
190583
+ this.outputBytes = outputBytes;
190584
+ this.maxOutputBytes = maxOutputBytes;
190585
+ this.name = "AgentOutputLimitError";
190586
+ }
190587
+ }
190588
+ function findOutputLimitError(error51, abortController) {
190589
+ if (error51 instanceof AgentOutputLimitError)
190590
+ return error51;
190591
+ const reason = abortController.signal.reason;
190592
+ return reason instanceof AgentOutputLimitError ? reason : undefined;
190593
+ }
190594
+ function resolveEffectiveTimeoutMs(input) {
190595
+ const deadlineRemaining = input.deadlineAtEpochMs === undefined ? Number.POSITIVE_INFINITY : input.deadlineAtEpochMs - Date.now();
190596
+ return Math.max(0, Math.min(input.timeoutMs, deadlineRemaining));
190597
+ }
190598
+ function normalizeUsage(usage) {
190599
+ return normalizeModelTokenUsage(usage);
190600
+ }
190017
190601
  function resolveEffortConfigOption(agent, effort) {
190018
190602
  if (!effort)
190019
190603
  return;
@@ -190024,7 +190608,7 @@ function resolveEffortConfigOption(agent, effort) {
190024
190608
  return;
190025
190609
  }
190026
190610
  async function readWorkspaceFile(workspaceDir, requestedPath, line, limit) {
190027
- const workspaceRoot = await realpath(workspaceDir);
190611
+ const workspaceRoot = await realpath2(workspaceDir);
190028
190612
  const candidates = resolveReadablePaths(workspaceRoot, requestedPath);
190029
190613
  let content;
190030
190614
  for (const absolute of candidates) {
@@ -190047,7 +190631,7 @@ async function readWorkspaceFile(workspaceDir, requestedPath, line, limit) {
190047
190631
  `);
190048
190632
  }
190049
190633
  async function resolveReadableFile(workspaceRoot, absolute) {
190050
- const realPath = await realpath(absolute).catch((error51) => {
190634
+ const realPath = await realpath2(absolute).catch((error51) => {
190051
190635
  if (isMissingPathError2(error51))
190052
190636
  return;
190053
190637
  throw error51;
@@ -190066,7 +190650,7 @@ function resolveReadablePaths(workspaceRoot, requestedPath) {
190066
190650
  }
190067
190651
  const repoPath = resolve3(workspaceRoot, "repo", relativePath);
190068
190652
  assertWithinWorkspace(workspaceRoot, repoPath);
190069
- return isAbsolute(requestedPath) ? [primary, repoPath] : [repoPath, primary];
190653
+ return isAbsolute3(requestedPath) ? [primary, repoPath] : [repoPath, primary];
190070
190654
  }
190071
190655
  function assertWithinWorkspace(workspaceRoot, absolute) {
190072
190656
  const relativePath = relative(workspaceRoot, absolute);
@@ -190098,6 +190682,20 @@ function buildAgentFailure(rawDetail, fallbackMessage) {
190098
190682
  ...detail ? { detail } : {}
190099
190683
  };
190100
190684
  }
190685
+ function buildPreflightFailure(error51) {
190686
+ if (error51 instanceof ChildEnvPreflightError) {
190687
+ return {
190688
+ code: error51.code,
190689
+ message: error51.message
190690
+ };
190691
+ }
190692
+ const detail = sanitizeTextForDisplay(formatAgentErrorDetail(error51));
190693
+ return {
190694
+ code: "AGENT_CONFIG_INVALID",
190695
+ message: "Agent configuration is invalid. Run kyoso doctor and check agent configuration.",
190696
+ ...detail ? { detail } : {}
190697
+ };
190698
+ }
190101
190699
  function safeAgentFailureMessage(code, fallbackMessage) {
190102
190700
  if (code === "AUTH_FAILED") {
190103
190701
  return "Agent authentication failed. Run kyoso doctor and check configured credentials.";
@@ -190113,15 +190711,15 @@ function safeAgentFailureMessage(code, fallbackMessage) {
190113
190711
  }
190114
190712
  return sanitizeTextForDisplay(fallbackMessage);
190115
190713
  }
190116
- function classifyAgentFailure(stderr2) {
190117
- if (/spawn/i.test(stderr2))
190714
+ function classifyAgentFailure(stderr3) {
190715
+ if (/spawn/i.test(stderr3))
190118
190716
  return "AGENT_SPAWN_FAILED";
190119
- if (/ENOTFOUND|ENOTCACHED|ECONNREFUSED|ETIMEDOUT|registry\.npmjs\.org|network request|cache mode/i.test(stderr2)) {
190717
+ if (/ENOTFOUND|ENOTCACHED|ECONNREFUSED|ETIMEDOUT|registry\.npmjs\.org|network request|cache mode/i.test(stderr3)) {
190120
190718
  return "AGENT_NETWORK_FAILED";
190121
190719
  }
190122
- if (/auth|api key|login|credential/i.test(stderr2))
190720
+ if (/auth|api key|login|credential/i.test(stderr3))
190123
190721
  return "AUTH_FAILED";
190124
- if (/permission|policy|write|terminal/i.test(stderr2))
190722
+ if (/permission|policy|write|terminal/i.test(stderr3))
190125
190723
  return "PERMISSION_DENIED";
190126
190724
  return "AGENT_FAILED";
190127
190725
  }
@@ -190158,9 +190756,24 @@ class FakeAgentManager extends BaseAcpAgentManager {
190158
190756
  this.calls.push(input);
190159
190757
  const startedAt = new Date().toISOString();
190160
190758
  if (input.role === "finding_verifier") {
190759
+ await input.onStarted?.();
190161
190760
  return verifierResult(input, startedAt, this.verifierScenarios[input.agent] ?? "confirmed");
190162
190761
  }
190163
190762
  const scenario = this.scenarios[input.agent] ?? "success";
190763
+ if (scenario === "preflight_failure" || scenario === "openrouter_key_missing") {
190764
+ return {
190765
+ agent: input.agent,
190766
+ role: input.role,
190767
+ status: "failed",
190768
+ startedAt,
190769
+ completedAt: new Date().toISOString(),
190770
+ error: {
190771
+ code: scenario === "openrouter_key_missing" ? "OPENROUTER_KEY_MISSING" : "AGENT_CONFIG_INVALID",
190772
+ message: scenario === "openrouter_key_missing" ? "Fake OpenRouter key is missing." : "Fake agent configuration is invalid."
190773
+ }
190774
+ };
190775
+ }
190776
+ await input.onStarted?.();
190164
190777
  if (scenario === "timeout") {
190165
190778
  return {
190166
190779
  agent: input.agent,
@@ -190206,9 +190819,10 @@ ${JSON.stringify(opinion)}
190206
190819
  role: input.role,
190207
190820
  status: "completed",
190208
190821
  rawText,
190209
- normalized: scenario === "success" ? opinion : undefined,
190822
+ normalized: scenario === "success" || scenario === "unknown_usage" ? opinion : undefined,
190210
190823
  startedAt,
190211
- completedAt: new Date().toISOString()
190824
+ completedAt: new Date().toISOString(),
190825
+ ...scenario === "unknown_usage" ? {} : { usage: fakeUsage() }
190212
190826
  };
190213
190827
  }
190214
190828
  }
@@ -190242,9 +190856,13 @@ function verifierResult(input, startedAt, scenario) {
190242
190856
  status: "completed",
190243
190857
  rawText,
190244
190858
  startedAt,
190245
- completedAt: new Date().toISOString()
190859
+ completedAt: new Date().toISOString(),
190860
+ usage: fakeUsage()
190246
190861
  };
190247
190862
  }
190863
+ function fakeUsage() {
190864
+ return { totalTokens: 20, inputTokens: 12, outputTokens: 8 };
190865
+ }
190248
190866
  function findingIdsFromPrompt(prompt) {
190249
190867
  return Array.from(prompt.matchAll(/^Finding ID: (.+)$/gm)).map((match) => match[1] ?? "");
190250
190868
  }
@@ -190784,8 +191402,8 @@ function normalizeTitle(value) {
190784
191402
 
190785
191403
  // src/audit/stateRoot.ts
190786
191404
  import { createHash as createHash2 } from "node:crypto";
190787
- import { lstat, mkdir as mkdir2, realpath as realpath2 } from "node:fs/promises";
190788
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve5 } from "node:path";
191405
+ import { lstat, mkdir as mkdir2, realpath as realpath3 } from "node:fs/promises";
191406
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute4, join as join3, resolve as resolve5 } from "node:path";
190789
191407
 
190790
191408
  // src/context/pathPolicy.ts
190791
191409
  import { normalize, sep } from "node:path";
@@ -190876,7 +191494,7 @@ async function resolveAuditStateRoot(options) {
190876
191494
  return { warnings: [...warnings, AUDIT_WARNING_UNSUPPORTED_CAPABILITY] };
190877
191495
  }
190878
191496
  try {
190879
- const workspaceRoot = await realpath2(resolve5(options.cwd));
191497
+ const workspaceRoot = await realpath3(resolve5(options.cwd));
190880
191498
  const candidate = resolveStateBaseCandidate(options.env ?? process.env);
190881
191499
  if (!candidate)
190882
191500
  throw new Error("missing trusted state base");
@@ -190905,7 +191523,7 @@ async function resolveAuditStateRoot(options) {
190905
191523
  }
190906
191524
  }
190907
191525
  async function ensureTrustedDirectory(options) {
190908
- const realRoot = await realpath2(options.root);
191526
+ const realRoot = await realpath3(options.root);
190909
191527
  assertSafeDirectory(realRoot, options.uid, await lstat(realRoot));
190910
191528
  if (options.workspaceRoot && isPathWithin(realRoot, options.workspaceRoot)) {
190911
191529
  throw new Error("trusted directory resolves inside workspace");
@@ -190922,7 +191540,7 @@ async function ensureTrustedDirectory(options) {
190922
191540
  entry = await lstat(next);
190923
191541
  }
190924
191542
  assertSafeDirectory(next, options.uid, entry);
190925
- const realNext = await realpath2(next);
191543
+ const realNext = await realpath3(next);
190926
191544
  if (!isPathWithin(realNext, realRoot)) {
190927
191545
  throw new Error("managed directory escaped trusted root");
190928
191546
  }
@@ -190938,7 +191556,7 @@ function isResolvedAuditStateRoot(resolution) {
190938
191556
  }
190939
191557
  function validateAuditDirectory(directory, warnings) {
190940
191558
  try {
190941
- if (directory.trim().length === 0 || isAbsolute2(directory) || directory.split(/[\\/]+/).includes("..")) {
191559
+ if (directory.trim().length === 0 || isAbsolute4(directory) || directory.split(/[\\/]+/).includes("..")) {
190942
191560
  throw new Error("unsafe logical directory");
190943
191561
  }
190944
191562
  const normalized = normalizeRelativePath(directory);
@@ -190953,11 +191571,11 @@ function validateAuditDirectory(directory, warnings) {
190953
191571
  }
190954
191572
  function resolveStateBaseCandidate(env) {
190955
191573
  const xdgStateHome = env.XDG_STATE_HOME?.trim();
190956
- if (xdgStateHome && isAbsolute2(xdgStateHome)) {
191574
+ if (xdgStateHome && isAbsolute4(xdgStateHome)) {
190957
191575
  return resolve5(xdgStateHome);
190958
191576
  }
190959
191577
  const home = env.HOME?.trim();
190960
- if (!home || !isAbsolute2(home))
191578
+ if (!home || !isAbsolute4(home))
190961
191579
  return;
190962
191580
  return join3(resolve5(home), ".local", "state");
190963
191581
  }
@@ -190969,7 +191587,7 @@ async function ensureTrustedStateBase(options) {
190969
191587
  assertSafeDirectory(existing.path, options.uid, await lstat(existing.path), {
190970
191588
  allowFilesystemRoot: true
190971
191589
  });
190972
- const realExisting = await realpath2(existing.path);
191590
+ const realExisting = await realpath3(existing.path);
190973
191591
  if (isPathWithin(realExisting, options.workspaceRoot)) {
190974
191592
  throw new Error("state base resolves inside workspace");
190975
191593
  }
@@ -190980,7 +191598,7 @@ async function ensureTrustedStateBase(options) {
190980
191598
  await createDirectory(current);
190981
191599
  const entry = await lstat(current);
190982
191600
  assertSafeDirectory(current, options.uid, entry);
190983
- const realCurrent = await realpath2(current);
191601
+ const realCurrent = await realpath3(current);
190984
191602
  if (!isPathWithin(realCurrent, realExisting)) {
190985
191603
  throw new Error("state base changed while being created");
190986
191604
  }
@@ -190989,7 +191607,7 @@ async function ensureTrustedStateBase(options) {
190989
191607
  }
190990
191608
  current = realCurrent;
190991
191609
  }
190992
- const stateBase = await realpath2(current);
191610
+ const stateBase = await realpath3(current);
190993
191611
  assertSafeDirectory(stateBase, options.uid, await lstat(stateBase));
190994
191612
  if (isPathWithin(stateBase, options.workspaceRoot)) {
190995
191613
  throw new Error("state base resolves inside workspace");
@@ -191008,7 +191626,7 @@ async function findExistingAncestor(candidate) {
191008
191626
  }
191009
191627
  return { path: current, missingSegments };
191010
191628
  }
191011
- const parent = dirname3(current);
191629
+ const parent = dirname4(current);
191012
191630
  if (parent === current)
191013
191631
  throw new Error("state base has no existing ancestor");
191014
191632
  missingSegments.unshift(basename(current));
@@ -191041,12 +191659,12 @@ function getCurrentUid(getuid) {
191041
191659
  }
191042
191660
  }
191043
191661
  function isFilesystemRoot(path) {
191044
- return dirname3(path) === path;
191662
+ return dirname4(path) === path;
191045
191663
  }
191046
191664
  async function assertTrustedAncestorChain(path, uid) {
191047
191665
  let child = path;
191048
191666
  while (!isFilesystemRoot(child)) {
191049
- const parent = dirname3(child);
191667
+ const parent = dirname4(child);
191050
191668
  const [parentStat, childStat] = await Promise.all([
191051
191669
  lstat(parent),
191052
191670
  lstat(child)
@@ -191094,7 +191712,7 @@ function isAlreadyExistsError(error51) {
191094
191712
 
191095
191713
  // src/audit/safeTraceFile.ts
191096
191714
  import { constants } from "node:fs";
191097
- import { lstat as lstat2, open, realpath as realpath3, stat } from "node:fs/promises";
191715
+ import { lstat as lstat2, open, realpath as realpath4, stat } from "node:fs/promises";
191098
191716
  import { join as join4 } from "node:path";
191099
191717
  var AUDIT_WARNING_UNSUPPORTED_OPEN_CAPABILITY = "AUDIT_DISABLED_UNSUPPORTED_CAPABILITY: Audit trace writing requires unavailable filesystem capabilities.";
191100
191718
  async function openVerifiedTraceFile(options) {
@@ -191124,7 +191742,7 @@ async function openVerifiedTraceFile(options) {
191124
191742
  const [handleStat, pathStat, realTracePath] = await Promise.all([
191125
191743
  handle.stat({ bigint: true }),
191126
191744
  stat(tracePath, { bigint: true }),
191127
- realpath3(tracePath)
191745
+ realpath4(tracePath)
191128
191746
  ]);
191129
191747
  if (!handleStat.isFile() || !pathStat.isFile() || handleStat.dev !== pathStat.dev || handleStat.ino !== pathStat.ino || !isPathWithin(realTracePath, options.kyosoRoot)) {
191130
191748
  throw new Error("trace file identity could not be verified");
@@ -191167,6 +191785,16 @@ async function optionalLstat2(path) {
191167
191785
  }
191168
191786
 
191169
191787
  // src/audit/sanitize.ts
191788
+ var USAGE_METADATA_KEYS = new Set([
191789
+ "tokenUsage",
191790
+ "totalTokens",
191791
+ "inputTokens",
191792
+ "outputTokens",
191793
+ "thoughtTokens",
191794
+ "cachedReadTokens",
191795
+ "cachedWriteTokens",
191796
+ "skipOptionalPhasesWhenTokenUsageUnknown"
191797
+ ]);
191170
191798
  function sanitizeForAudit(value, options = {}) {
191171
191799
  if (typeof value === "string")
191172
191800
  return sanitizeText(value);
@@ -191175,7 +191803,7 @@ function sanitizeForAudit(value, options = {}) {
191175
191803
  if (typeof value === "object" && value !== null) {
191176
191804
  const result = {};
191177
191805
  for (const [key, nested] of Object.entries(value)) {
191178
- if (/raw|content|env|credential|token|secret|password/i.test(key) && !(options.includeRawAgentOutput && key === "rawText")) {
191806
+ if (/raw|content|env|credential|token|secret|password/i.test(key) && !(options.includeRawAgentOutput && key === "rawText") && !isUsageMetadata(key, nested)) {
191179
191807
  continue;
191180
191808
  }
191181
191809
  result[key] = sanitizeForAudit(nested, options);
@@ -191184,6 +191812,17 @@ function sanitizeForAudit(value, options = {}) {
191184
191812
  }
191185
191813
  return value;
191186
191814
  }
191815
+ function isUsageMetadata(key, value) {
191816
+ if (!USAGE_METADATA_KEYS.has(key))
191817
+ return false;
191818
+ if (key === "tokenUsage") {
191819
+ return typeof value === "object" && value !== null && !Array.isArray(value);
191820
+ }
191821
+ if (key === "skipOptionalPhasesWhenTokenUsageUnknown") {
191822
+ return typeof value === "boolean";
191823
+ }
191824
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
191825
+ }
191187
191826
 
191188
191827
  // src/audit/trace.ts
191189
191828
  var AUDIT_WARNING_WRITE_FAILED = "AUDIT_WRITE_FAILED: Audit trace writing failed; no further audit events will be written.";
@@ -191449,6 +192088,8 @@ function renderMarkdownResult(tool, result, options = {}) {
191449
192088
  "",
191450
192089
  `**Decision:** ${result.decision}`,
191451
192090
  `**Mode:** ${tool}`,
192091
+ `**Completion:** ${formatCompletion(result)}`,
192092
+ `**Request fingerprint:** ${shortFingerprint(result.requestFingerprint)}`,
191452
192093
  `**Agents:** ${result.agentOpinions.map((opinion) => `${title(opinion.agent)} ${opinion.status}`).join(", ")}`,
191453
192094
  `**Review mode:** ${formatReviewMode(result)}`,
191454
192095
  ...result.verificationMode ? [
@@ -191460,6 +192101,7 @@ function renderMarkdownResult(tool, result, options = {}) {
191460
192101
  "",
191461
192102
  options.summaryText ?? defaultSummaryText(result)
191462
192103
  ];
192104
+ lines.push(...formatExecutionBudget(result));
191463
192105
  if (result.cisaSecureByDesign) {
191464
192106
  lines.push("", "## CISA Secure by Design Gate", "", "| Dimension | Status | Notes |", "|---|---|---|", `| Customer Security Outcomes | ${result.cisaSecureByDesign.customerSecurityOutcomes} | ${notes(result.cisaSecureByDesign.notes)} |`, `| Secure by Default | ${result.cisaSecureByDesign.secureByDefault} | ${notes(result.cisaSecureByDesign.notes)} |`, `| Transparency & Accountability | ${result.cisaSecureByDesign.transparencyAndAccountability} | ${notes(result.cisaSecureByDesign.notes)} |`, `| Governance | ${result.cisaSecureByDesign.governance} | ${notes(result.cisaSecureByDesign.notes)} |`);
191465
192107
  }
@@ -191512,8 +192154,37 @@ function renderMarkdownResult(tool, result, options = {}) {
191512
192154
  `);
191513
192155
  }
191514
192156
  function defaultSummaryText(result) {
192157
+ if (result.completion.status === "incomplete") {
192158
+ const reasons = result.completion.reasons.length > 0 ? result.completion.reasons.join(", ") : "unspecified coverage gap";
192159
+ return `Review incomplete (${reasons}). Decision is block because review coverage is incomplete, not because a code finding was established.`;
192160
+ }
191515
192161
  return result.findings.length === 0 ? "No blocking findings were detected from the supplied context." : `${result.findings.length} finding(s) require attention.`;
191516
192162
  }
192163
+ function formatExecutionBudget(result) {
192164
+ const budget = result.executionBudget;
192165
+ const agentOutputs = Object.entries(budget.agentOutputBytes);
192166
+ const outputLines = agentOutputs.length > 0 ? agentOutputs.map(([agent, bytes]) => `- ${title(agent)}: ${bytes} bytes`) : ["- None reported."];
192167
+ const totalTokens = budget.tokenUsage.totals.totalTokens;
192168
+ return [
192169
+ "",
192170
+ "## Execution Budget",
192171
+ "",
192172
+ `- Model calls: ${budget.modelCalls.planned} planned / ${budget.modelCalls.consumed} consumed / ${budget.modelCalls.skipped} skipped`,
192173
+ `- Wall time: ${budget.wallTime.consumedMs}ms consumed / ${budget.wallTime.limitMs}ms limit`,
192174
+ `- Token usage: ${budget.tokenUsage.status} (${budget.tokenUsage.reportedCalls} reported, ${budget.tokenUsage.unknownCalls} unknown${totalTokens === undefined ? "" : `, ${totalTokens} total`})`,
192175
+ "- Agent output:",
192176
+ ...outputLines
192177
+ ];
192178
+ }
192179
+ function formatCompletion(result) {
192180
+ if (result.completion.status === "complete")
192181
+ return "complete";
192182
+ const reasons = result.completion.reasons.join(", ") || "unspecified";
192183
+ return `incomplete (${reasons}; retryable=${String(result.completion.retryable)})`;
192184
+ }
192185
+ function shortFingerprint(value) {
192186
+ return value.length <= 20 ? value : `${value.slice(0, 20)}…`;
192187
+ }
191517
192188
  function title(value) {
191518
192189
  return value.slice(0, 1).toUpperCase() + value.slice(1);
191519
192190
  }
@@ -191602,7 +192273,7 @@ function parseJudgeOutput(text, fallbackSummaryText) {
191602
192273
  const parsed = JSON.parse(json2);
191603
192274
  const summaryText = typeof parsed.summaryText === "string" && parsed.summaryText.trim().length > 0 ? sanitizeText(parsed.summaryText) : fallbackSummaryText;
191604
192275
  const disagreementComments = Array.isArray(parsed.disagreementComments) ? parsed.disagreementComments.flatMap((item) => {
191605
- if (!isRecord7(item) || typeof item.topic !== "string" || typeof item.judgeComment !== "string") {
192276
+ if (!isRecord8(item) || typeof item.topic !== "string" || typeof item.judgeComment !== "string") {
191606
192277
  return [];
191607
192278
  }
191608
192279
  return [
@@ -191618,7 +192289,7 @@ function parseJudgeOutput(text, fallbackSummaryText) {
191618
192289
  return { summaryText, disagreementComments, analysis };
191619
192290
  }
191620
192291
  function parseAnalysis(value) {
191621
- if (!isRecord7(value))
192292
+ if (!isRecord8(value))
191622
192293
  return;
191623
192294
  if (!Array.isArray(value.blindSpots) || !Array.isArray(value.contradictions) || !Array.isArray(value.partialCoverage)) {
191624
192295
  return;
@@ -191626,7 +192297,7 @@ function parseAnalysis(value) {
191626
192297
  return {
191627
192298
  blindSpots: value.blindSpots.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => typeof item === "string" ? [sanitizeAnalysisText(item)] : []),
191628
192299
  contradictions: value.contradictions.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => {
191629
- if (!isRecord7(item) || typeof item.topic !== "string" || typeof item.detail !== "string") {
192300
+ if (!isRecord8(item) || typeof item.topic !== "string" || typeof item.detail !== "string") {
191630
192301
  return [];
191631
192302
  }
191632
192303
  return [
@@ -191637,7 +192308,7 @@ function parseAnalysis(value) {
191637
192308
  ];
191638
192309
  }),
191639
192310
  partialCoverage: value.partialCoverage.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => {
191640
- if (!isRecord7(item) || typeof item.note !== "string")
192311
+ if (!isRecord8(item) || typeof item.note !== "string")
191641
192312
  return [];
191642
192313
  const findingId = typeof item.findingId === "string" ? sanitizeAnalysisText(item.findingId) : undefined;
191643
192314
  return [
@@ -191684,7 +192355,7 @@ function extractFirstJsonObject2(text) {
191684
192355
  }
191685
192356
  return;
191686
192357
  }
191687
- function isRecord7(value) {
192358
+ function isRecord8(value) {
191688
192359
  return typeof value === "object" && value !== null && !Array.isArray(value);
191689
192360
  }
191690
192361
 
@@ -191702,7 +192373,7 @@ async function runAnthropicJudge(input, timeoutMs) {
191702
192373
  },
191703
192374
  body: JSON.stringify({
191704
192375
  model: input.env.KYOSO_ANTHROPIC_JUDGE_MODEL ?? "claude-haiku-4-5",
191705
- max_tokens: 4096,
192376
+ max_tokens: JUDGE_MAX_OUTPUT_TOKENS,
191706
192377
  temperature: 0,
191707
192378
  messages: [
191708
192379
  {
@@ -191718,7 +192389,21 @@ async function runAnthropicJudge(input, timeoutMs) {
191718
192389
  const content = payload.content?.find((item) => item.type === "text" && item.text)?.text;
191719
192390
  if (!content)
191720
192391
  throw new Error("Anthropic judge response did not include text content.");
191721
- return parseJudgeOutput(content, input.summaryText);
192392
+ const usage = normalizeUsage2(payload.usage);
192393
+ return {
192394
+ output: parseJudgeOutput(content, input.summaryText),
192395
+ ...usage ? { usage } : {}
192396
+ };
192397
+ }
192398
+ function normalizeUsage2(usage) {
192399
+ if (!usage)
192400
+ return;
192401
+ return normalizeModelTokenUsage({
192402
+ inputTokens: usage.input_tokens,
192403
+ outputTokens: usage.output_tokens,
192404
+ cachedReadTokens: usage.cache_read_input_tokens,
192405
+ cachedWriteTokens: usage.cache_creation_input_tokens
192406
+ });
191722
192407
  }
191723
192408
  async function fetchWithTimeout(url2, init, timeoutMs) {
191724
192409
  const controller = new AbortController;
@@ -191762,6 +192447,7 @@ async function runOpenAiJudge(input, timeoutMs) {
191762
192447
  content: buildJudgePrompt(input.tool, input.result, input.summaryText, input.agentFindings)
191763
192448
  }
191764
192449
  ],
192450
+ max_completion_tokens: JUDGE_MAX_OUTPUT_TOKENS,
191765
192451
  temperature: 0
191766
192452
  })
191767
192453
  }, timeoutMs);
@@ -191771,7 +192457,22 @@ async function runOpenAiJudge(input, timeoutMs) {
191771
192457
  const content = payload.choices?.[0]?.message?.content;
191772
192458
  if (!content)
191773
192459
  throw new Error("OpenAI judge response did not include content.");
191774
- return parseJudgeOutput(content, input.summaryText);
192460
+ const usage = normalizeUsage3(payload.usage);
192461
+ return {
192462
+ output: parseJudgeOutput(content, input.summaryText),
192463
+ ...usage ? { usage } : {}
192464
+ };
192465
+ }
192466
+ function normalizeUsage3(usage) {
192467
+ if (!usage)
192468
+ return;
192469
+ return normalizeModelTokenUsage({
192470
+ totalTokens: usage.total_tokens,
192471
+ inputTokens: usage.prompt_tokens,
192472
+ outputTokens: usage.completion_tokens,
192473
+ cachedReadTokens: usage.prompt_tokens_details?.cached_tokens,
192474
+ thoughtTokens: usage.completion_tokens_details?.reasoning_tokens
192475
+ });
191775
192476
  }
191776
192477
  async function fetchWithTimeout2(url2, init, timeoutMs) {
191777
192478
  const controller = new AbortController;
@@ -191813,8 +192514,13 @@ async function runJudge(input) {
191813
192514
  return { provider, status: "deterministic_fallback", output: fallback };
191814
192515
  }
191815
192516
  try {
191816
- const output = provider === "openai" ? await runOpenAiJudge(input, input.config.timeoutMs) : await runAnthropicJudge(input, input.config.timeoutMs);
191817
- return { provider, status: "completed", output };
192517
+ const output = provider === "openai" ? await runOpenAiJudge(input, input.timeoutMs ?? input.config.timeoutMs) : await runAnthropicJudge(input, input.timeoutMs ?? input.config.timeoutMs);
192518
+ return {
192519
+ provider,
192520
+ status: "completed",
192521
+ output: output.output,
192522
+ ...output.usage ? { usage: output.usage } : {}
192523
+ };
191818
192524
  } catch (error51) {
191819
192525
  return {
191820
192526
  provider,
@@ -192020,7 +192726,7 @@ function decide(input) {
192020
192726
 
192021
192727
  // src/workspace/createSnapshot.ts
192022
192728
  import { chmod, mkdir as mkdir3, mkdtemp, writeFile as writeFile2 } from "node:fs/promises";
192023
- import { dirname as dirname4, join as join5 } from "node:path";
192729
+ import { dirname as dirname5, join as join5 } from "node:path";
192024
192730
  import { tmpdir } from "node:os";
192025
192731
  async function createSnapshot(traceId, tool, request, options = {}) {
192026
192732
  const root = await mkdtemp(join5(tmpdir(), `kyoso-${traceId}-`));
@@ -192036,7 +192742,7 @@ async function createSnapshot(traceId, tool, request, options = {}) {
192036
192742
  if (!isAllowedPath(relative2, options.allowPatterns ?? []))
192037
192743
  continue;
192038
192744
  const dest = join5(repoDir, relative2);
192039
- await mkdir3(dirname4(dest), { recursive: true });
192745
+ await mkdir3(dirname5(dest), { recursive: true });
192040
192746
  await writeFile2(dest, file2.content, "utf8");
192041
192747
  await chmod(dest, 292).catch(() => {
192042
192748
  return;
@@ -192087,6 +192793,304 @@ function newTraceId() {
192087
192793
  return `tr_${randomUUID()}`;
192088
192794
  }
192089
192795
 
192796
+ // src/core/requestFingerprint.ts
192797
+ import { createHash as createHash3 } from "node:crypto";
192798
+ var REVIEW_CONTRACT_VERSION = "2026-07-15-v1";
192799
+ function createRequestFingerprint(input) {
192800
+ const reviewers = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => ({
192801
+ agent,
192802
+ role: input.roles[agent] ?? input.config.agents[agent].role,
192803
+ model: input.config.agents[agent].model ?? null,
192804
+ provider: agent === "codex" ? input.config.agents.codex.provider ?? "default" : "default"
192805
+ }));
192806
+ const request = structuredClone(input.request);
192807
+ if (request.options)
192808
+ delete request.options.includeAgentRawOutputs;
192809
+ const payload = {
192810
+ reviewContractVersion: REVIEW_CONTRACT_VERSION,
192811
+ tool: input.tool,
192812
+ request,
192813
+ reviewers,
192814
+ verification: input.config.verification,
192815
+ judge: {
192816
+ ...input.config.judge,
192817
+ requestedProvider: input.request.options?.judgeProvider ?? null
192818
+ },
192819
+ executionBudget: input.budget
192820
+ };
192821
+ return `sha256:${createHash3("sha256").update(canonicalJson(payload), "utf8").digest("hex")}`;
192822
+ }
192823
+ function canonicalJson(value) {
192824
+ return JSON.stringify(canonicalize(value));
192825
+ }
192826
+ function canonicalize(value) {
192827
+ if (Array.isArray(value))
192828
+ return value.map(canonicalize);
192829
+ if (!isRecord9(value))
192830
+ return value;
192831
+ 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)]));
192832
+ }
192833
+ function isRecord9(value) {
192834
+ return typeof value === "object" && value !== null && !Array.isArray(value);
192835
+ }
192836
+
192837
+ // src/core/reviewBudget.ts
192838
+ var REVIEW_BUDGET_KEYS = new Set([
192839
+ "maxModelCalls",
192840
+ "maxTotalWallTimeMs",
192841
+ "maxAgentOutputBytes",
192842
+ "maxFindingsPerAgent",
192843
+ "skipOptionalPhasesWhenTokenUsageUnknown"
192844
+ ]);
192845
+ var MODEL_CALL_KINDS = ["primary", "verifier", "judge"];
192846
+ function resolveReviewBudget(ceiling, requested) {
192847
+ if (requested === undefined)
192848
+ return ceiling;
192849
+ if (!isRecord10(requested)) {
192850
+ throw new KyosoRequestError("options.reviewBudget must be an object.", "REVIEW_BUDGET_INVALID");
192851
+ }
192852
+ for (const [key, value] of Object.entries(requested)) {
192853
+ if (!REVIEW_BUDGET_KEYS.has(key)) {
192854
+ throw new KyosoRequestError(`options.reviewBudget.${key} is not supported.`, "REVIEW_BUDGET_INVALID");
192855
+ }
192856
+ if (key === "skipOptionalPhasesWhenTokenUsageUnknown") {
192857
+ if (typeof value !== "boolean") {
192858
+ throw new KyosoRequestError(`options.reviewBudget.${key} must be a boolean.`, "REVIEW_BUDGET_INVALID");
192859
+ }
192860
+ continue;
192861
+ }
192862
+ if (!isPositiveInteger(value)) {
192863
+ throw new KyosoRequestError(`options.reviewBudget.${key} must be a positive integer.`, "REVIEW_BUDGET_INVALID");
192864
+ }
192865
+ }
192866
+ const numericKeys = [
192867
+ "maxModelCalls",
192868
+ "maxTotalWallTimeMs",
192869
+ "maxAgentOutputBytes",
192870
+ "maxFindingsPerAgent"
192871
+ ];
192872
+ for (const key of numericKeys) {
192873
+ const value = requested[key];
192874
+ if (value === undefined)
192875
+ continue;
192876
+ if (value > ceiling[key]) {
192877
+ throw new KyosoRequestError(`options.reviewBudget.${key} cannot exceed the user-global ceiling.`, "REVIEW_BUDGET_EXCEEDS_CEILING");
192878
+ }
192879
+ }
192880
+ if (ceiling.skipOptionalPhasesWhenTokenUsageUnknown && requested.skipOptionalPhasesWhenTokenUsageUnknown === false) {
192881
+ throw new KyosoRequestError("options.reviewBudget.skipOptionalPhasesWhenTokenUsageUnknown cannot relax the user-global ceiling.", "REVIEW_BUDGET_EXCEEDS_CEILING");
192882
+ }
192883
+ return {
192884
+ maxModelCalls: requested.maxModelCalls ?? ceiling.maxModelCalls,
192885
+ maxTotalWallTimeMs: requested.maxTotalWallTimeMs ?? ceiling.maxTotalWallTimeMs,
192886
+ maxAgentOutputBytes: requested.maxAgentOutputBytes ?? ceiling.maxAgentOutputBytes,
192887
+ maxFindingsPerAgent: requested.maxFindingsPerAgent ?? ceiling.maxFindingsPerAgent,
192888
+ skipOptionalPhasesWhenTokenUsageUnknown: ceiling.skipOptionalPhasesWhenTokenUsageUnknown || requested.skipOptionalPhasesWhenTokenUsageUnknown === true
192889
+ };
192890
+ }
192891
+
192892
+ class ReviewBudgetTracker {
192893
+ budget;
192894
+ startedAtEpochMs;
192895
+ deadlineAtEpochMs;
192896
+ reservations = new Map;
192897
+ skippedCalls = [];
192898
+ incompleteReasons = new Set;
192899
+ nextReservationId = 1;
192900
+ constructor(budget, startedAtEpochMs = Date.now()) {
192901
+ this.budget = budget;
192902
+ this.startedAtEpochMs = startedAtEpochMs;
192903
+ this.deadlineAtEpochMs = startedAtEpochMs + budget.maxTotalWallTimeMs;
192904
+ }
192905
+ remainingWallTimeMs(now = Date.now()) {
192906
+ return Math.max(0, this.deadlineAtEpochMs - now);
192907
+ }
192908
+ hasDeadlineExpired(now = Date.now()) {
192909
+ return this.remainingWallTimeMs(now) === 0;
192910
+ }
192911
+ reserveMany(inputs) {
192912
+ if (this.hasDeadlineExpired())
192913
+ return { failure: { reason: "deadline" } };
192914
+ if (this.usedCapacity() + inputs.length > this.budget.maxModelCalls) {
192915
+ return { failure: { reason: "model_call_budget" } };
192916
+ }
192917
+ const reservations = inputs.map((input) => {
192918
+ const reservation = {
192919
+ id: this.nextReservationId,
192920
+ kind: input.kind,
192921
+ ...input.agent ? { agent: input.agent } : {},
192922
+ status: "reserved"
192923
+ };
192924
+ this.reservations.set(reservation.id, reservation);
192925
+ this.nextReservationId += 1;
192926
+ return reservation;
192927
+ });
192928
+ return {
192929
+ reservations: reservations.map(({ id, kind, agent }) => ({
192930
+ id,
192931
+ kind,
192932
+ ...agent ? { agent } : {}
192933
+ }))
192934
+ };
192935
+ }
192936
+ reserve(input) {
192937
+ const result = this.reserveMany([input]);
192938
+ if ("failure" in result)
192939
+ return result;
192940
+ const reservation = result.reservations[0];
192941
+ if (!reservation) {
192942
+ return { failure: { reason: "model_call_budget" } };
192943
+ }
192944
+ return { reservation };
192945
+ }
192946
+ markStarted(reservation) {
192947
+ const current = this.reservations.get(reservation.id);
192948
+ if (!current || current.status !== "reserved")
192949
+ return;
192950
+ current.status = "started";
192951
+ }
192952
+ hasStarted(reservation) {
192953
+ const current = this.reservations.get(reservation.id);
192954
+ return current?.status === "started" || current?.status === "completed";
192955
+ }
192956
+ complete(reservation, values = {}) {
192957
+ const current = this.reservations.get(reservation.id);
192958
+ if (!current || current.status === "skipped" || current.status === "completed") {
192959
+ return;
192960
+ }
192961
+ current.status = "completed";
192962
+ current.outputBytes = values.outputBytes;
192963
+ current.usage = normalizeModelTokenUsage(values.usage);
192964
+ current.stopReason = values.stopReason;
192965
+ }
192966
+ skip(reservation, reason) {
192967
+ const current = this.reservations.get(reservation.id);
192968
+ if (!current || current.status !== "reserved")
192969
+ return;
192970
+ current.status = "skipped";
192971
+ current.reason = reason;
192972
+ }
192973
+ recordSkipped(input) {
192974
+ this.skippedCalls.push({
192975
+ kind: input.kind,
192976
+ ...input.agent ? { agent: input.agent } : {},
192977
+ status: "skipped",
192978
+ reason: input.reason
192979
+ });
192980
+ }
192981
+ markIncomplete(reason) {
192982
+ this.incompleteReasons.add(reason);
192983
+ }
192984
+ isTokenUsageUnknown() {
192985
+ return Array.from(this.reservations.values()).some((reservation) => reservation.status === "completed" && reservation.usage === undefined);
192986
+ }
192987
+ snapshot(now = Date.now()) {
192988
+ const calls = this.modelCalls();
192989
+ const byKind = Object.fromEntries(MODEL_CALL_KINDS.map((kind) => [
192990
+ kind,
192991
+ { planned: 0, consumed: 0, skipped: 0 }
192992
+ ]));
192993
+ let planned = 0;
192994
+ let consumed = 0;
192995
+ let skipped = 0;
192996
+ const agentOutputBytes = {};
192997
+ const usageTotals = {};
192998
+ let reportedCalls = 0;
192999
+ let unknownCalls = 0;
193000
+ for (const reservation of this.reservations.values()) {
193001
+ planned += 1;
193002
+ byKind[reservation.kind].planned += 1;
193003
+ if (reservation.status === "completed") {
193004
+ consumed += 1;
193005
+ byKind[reservation.kind].consumed += 1;
193006
+ if (reservation.agent && reservation.outputBytes !== undefined) {
193007
+ agentOutputBytes[reservation.agent] = (agentOutputBytes[reservation.agent] ?? 0) + reservation.outputBytes;
193008
+ }
193009
+ if (reservation.usage) {
193010
+ reportedCalls += 1;
193011
+ addUsage(usageTotals, reservation.usage);
193012
+ } else {
193013
+ unknownCalls += 1;
193014
+ }
193015
+ }
193016
+ if (reservation.status === "skipped") {
193017
+ skipped += 1;
193018
+ byKind[reservation.kind].skipped += 1;
193019
+ }
193020
+ }
193021
+ for (const call of this.skippedCalls) {
193022
+ skipped += 1;
193023
+ byKind[call.kind].skipped += 1;
193024
+ }
193025
+ const tokenStatus = consumed === 0 || reportedCalls === 0 ? "unknown" : unknownCalls === 0 ? "reported" : "partial";
193026
+ const completionReasons = Array.from(this.incompleteReasons).sort();
193027
+ const consumedMs = Math.max(0, now - this.startedAtEpochMs);
193028
+ return {
193029
+ completion: {
193030
+ status: completionReasons.length > 0 ? "incomplete" : "complete",
193031
+ reasons: completionReasons,
193032
+ retryable: false
193033
+ },
193034
+ executionBudget: {
193035
+ maxModelCalls: this.budget.maxModelCalls,
193036
+ modelCalls: { planned, consumed, skipped, byKind },
193037
+ wallTime: {
193038
+ limitMs: this.budget.maxTotalWallTimeMs,
193039
+ consumedMs,
193040
+ remainingMs: this.remainingWallTimeMs(now)
193041
+ },
193042
+ maxAgentOutputBytes: this.budget.maxAgentOutputBytes,
193043
+ maxFindingsPerAgent: this.budget.maxFindingsPerAgent,
193044
+ skipOptionalPhasesWhenTokenUsageUnknown: this.budget.skipOptionalPhasesWhenTokenUsageUnknown,
193045
+ agentOutputBytes,
193046
+ tokenUsage: {
193047
+ status: tokenStatus,
193048
+ reportedCalls,
193049
+ unknownCalls,
193050
+ totals: usageTotals
193051
+ }
193052
+ },
193053
+ modelCalls: calls
193054
+ };
193055
+ }
193056
+ usedCapacity() {
193057
+ return Array.from(this.reservations.values()).filter((reservation) => reservation.status === "reserved" || reservation.status === "started" || reservation.status === "completed").length;
193058
+ }
193059
+ modelCalls() {
193060
+ const reservations = Array.from(this.reservations.values()).filter((reservation) => reservation.status === "completed" || reservation.status === "skipped").map((reservation) => ({
193061
+ kind: reservation.kind,
193062
+ ...reservation.agent ? { agent: reservation.agent } : {},
193063
+ status: reservation.status === "completed" ? "completed" : "skipped",
193064
+ ...reservation.reason ? { reason: reservation.reason } : {},
193065
+ ...reservation.outputBytes !== undefined ? { outputBytes: reservation.outputBytes } : {},
193066
+ ...reservation.usage ? { usage: reservation.usage } : {},
193067
+ ...reservation.stopReason ? { stopReason: reservation.stopReason } : {}
193068
+ }));
193069
+ return [...reservations, ...this.skippedCalls];
193070
+ }
193071
+ }
193072
+ function addUsage(total, usage) {
193073
+ for (const key of [
193074
+ "totalTokens",
193075
+ "inputTokens",
193076
+ "outputTokens",
193077
+ "thoughtTokens",
193078
+ "cachedReadTokens",
193079
+ "cachedWriteTokens"
193080
+ ]) {
193081
+ const value = usage[key];
193082
+ if (value === undefined)
193083
+ continue;
193084
+ total[key] = (total[key] ?? 0) + value;
193085
+ }
193086
+ }
193087
+ function isPositiveInteger(value) {
193088
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
193089
+ }
193090
+ function isRecord10(value) {
193091
+ return typeof value === "object" && value !== null && !Array.isArray(value);
193092
+ }
193093
+
192090
193094
  // src/core/verification.ts
192091
193095
  var REAL_AGENTS = ["codex", "claude"];
192092
193096
  var VERIFIABLE_SEVERITIES = new Set(["critical", "high"]);
@@ -192128,9 +193132,12 @@ function groupVerificationTargetsByVerifier(targets) {
192128
193132
  findings
192129
193133
  }));
192130
193134
  }
192131
- function markVerificationOverflow(targets) {
193135
+ function markVerificationOverflow(targets, reason) {
192132
193136
  for (const target of targets) {
192133
- target.finding.verification = { status: "not_verified" };
193137
+ target.finding.verification = {
193138
+ status: "not_verified",
193139
+ ...reason ? { note: reason } : {}
193140
+ };
192134
193141
  }
192135
193142
  }
192136
193143
  function parseVerificationVerdicts(rawText) {
@@ -192142,7 +193149,7 @@ function parseVerificationVerdicts(rawText) {
192142
193149
  if (!Array.isArray(parsed.verdicts))
192143
193150
  return;
192144
193151
  return parsed.verdicts.flatMap((item) => {
192145
- if (!isRecord8(item))
193152
+ if (!isRecord11(item))
192146
193153
  return [];
192147
193154
  if (typeof item.findingId !== "string")
192148
193155
  return [];
@@ -192220,15 +193227,23 @@ function verificationNote(reasoning) {
192220
193227
  function isVerdict(value) {
192221
193228
  return value === "confirmed" || value === "refuted" || value === "uncertain";
192222
193229
  }
192223
- function isRecord8(value) {
193230
+ function isRecord11(value) {
192224
193231
  return typeof value === "object" && value !== null && !Array.isArray(value);
192225
193232
  }
192226
193233
 
192227
193234
  // src/core/runReview.ts
193235
+ function requestForRecursionFingerprint(request) {
193236
+ try {
193237
+ return scanAndRedactSecrets(request).redactedRequest;
193238
+ } catch {
193239
+ return { goal: "" };
193240
+ }
193241
+ }
192228
193242
  async function runReview(tool, request, options = {}) {
192229
193243
  const cwd = options.cwd ?? process.cwd();
192230
193244
  const traceId = newTraceId();
192231
- const startedAt = new Date().toISOString();
193245
+ const startedAtEpochMs = Date.now();
193246
+ const startedAt = new Date(startedAtEpochMs).toISOString();
192232
193247
  const auditEnv = { ...process.env, ...options.env };
192233
193248
  const traceWriterFactory = options.traceWriterFactory ?? createTraceWriter;
192234
193249
  let snapshot;
@@ -192237,6 +193252,14 @@ async function runReview(tool, request, options = {}) {
192237
193252
  } catch (error51) {
192238
193253
  if (error51 instanceof KyosoRequestError) {
192239
193254
  const config2 = kyosoConfigSchema.parse(defaultConfig);
193255
+ const budgetTracker = new ReviewBudgetTracker(config2.reviewBudget, startedAtEpochMs);
193256
+ const requestFingerprint = createRequestFingerprint({
193257
+ tool,
193258
+ request: requestForRecursionFingerprint(request),
193259
+ config: config2,
193260
+ roles: resolveAgentRoles(config2),
193261
+ budget: config2.reviewBudget
193262
+ });
192240
193263
  const trace2 = traceWriterFactory({
192241
193264
  enabled: config2.audit.enabled,
192242
193265
  directory: config2.audit.directory,
@@ -192251,6 +193274,12 @@ async function runReview(tool, request, options = {}) {
192251
193274
  tool,
192252
193275
  timestamp: new Date().toISOString()
192253
193276
  });
193277
+ await writeReviewBudgetPlanned({
193278
+ trace: trace2,
193279
+ traceId,
193280
+ budgetTracker,
193281
+ requestFingerprint
193282
+ });
192254
193283
  return await buildPolicyBlockResult({
192255
193284
  tool,
192256
193285
  trace: trace2,
@@ -192258,6 +193287,8 @@ async function runReview(tool, request, options = {}) {
192258
193287
  startedAt,
192259
193288
  networkMode: config2.network.defaultMode,
192260
193289
  warning: error51.message,
193290
+ budgetTracker,
193291
+ requestFingerprint,
192261
193292
  finding: {
192262
193293
  id: "KYOSO-1",
192263
193294
  severity: "critical",
@@ -192323,6 +193354,8 @@ async function runReview(tool, request, options = {}) {
192323
193354
  timestamp: new Date().toISOString()
192324
193355
  });
192325
193356
  validateReviewRequest(tool, request);
193357
+ const reviewBudget = resolveReviewBudget(loaded.config.reviewBudget, request.options?.reviewBudget);
193358
+ const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs);
192326
193359
  assertTrustedWorkspaceRoot(request.workspace?.root, loaded.config.workspace.root, cwd);
192327
193360
  const networkMode = resolveNetworkMode(request.options?.network, loaded.config.network.defaultMode, options.mcpNetworkMode);
192328
193361
  if (networkMode === "unrestricted" && !loaded.config.network.allowUnrestricted) {
@@ -192341,6 +193374,19 @@ async function runReview(tool, request, options = {}) {
192341
193374
  });
192342
193375
  const allowSecretOverride = loaded.config.secrets.allowOverride && request.options?.allowSecretRedaction === true;
192343
193376
  if (secretScan.detected && loaded.config.secrets.blockOnDetectedSecret && !allowSecretOverride) {
193377
+ const requestFingerprint2 = createRequestFingerprint({
193378
+ tool,
193379
+ request: secretScan.redactedRequest,
193380
+ config: loaded.config,
193381
+ roles: resolveAgentRoles(loaded.config),
193382
+ budget: reviewBudget
193383
+ });
193384
+ await writeReviewBudgetPlanned({
193385
+ trace,
193386
+ traceId,
193387
+ budgetTracker,
193388
+ requestFingerprint: requestFingerprint2
193389
+ });
192344
193390
  return await buildSecretBlockResult({
192345
193391
  tool,
192346
193392
  trace,
@@ -192349,7 +193395,9 @@ async function runReview(tool, request, options = {}) {
192349
193395
  configHash: loaded.configHash,
192350
193396
  networkMode,
192351
193397
  secretScan,
192352
- warnings
193398
+ warnings,
193399
+ budgetTracker,
193400
+ requestFingerprint: requestFingerprint2
192353
193401
  });
192354
193402
  }
192355
193403
  const denyPatterns = mergeDenyPatterns(loaded.config.workspace.deny, secretScan.redactedRequest.workspace?.denyRead);
@@ -192362,6 +193410,19 @@ async function runReview(tool, request, options = {}) {
192362
193410
  });
192363
193411
  warnings.push(...built.warnings);
192364
193412
  const agentRoles = resolveAgentRoles(loaded.config);
193413
+ const requestFingerprint = createRequestFingerprint({
193414
+ tool,
193415
+ request: built.request,
193416
+ config: loaded.config,
193417
+ roles: agentRoles,
193418
+ budget: reviewBudget
193419
+ });
193420
+ await writeReviewBudgetPlanned({
193421
+ trace,
193422
+ traceId,
193423
+ budgetTracker,
193424
+ requestFingerprint
193425
+ });
192365
193426
  snapshot = await createSnapshot(traceId, tool, built.request, {
192366
193427
  denyPatterns,
192367
193428
  allowPatterns,
@@ -192374,7 +193435,7 @@ async function runReview(tool, request, options = {}) {
192374
193435
  fileCount: snapshot.fileCount,
192375
193436
  timestamp: new Date().toISOString()
192376
193437
  });
192377
- const manager = options.agentManager ?? defaultAgentManager(loaded.config);
193438
+ const manager = options.agentManager ?? defaultAgentManager(loaded.config, options.env ?? process.env);
192378
193439
  const agentResults = await runAgents({
192379
193440
  tool,
192380
193441
  request: built.request,
@@ -192383,14 +193444,23 @@ async function runReview(tool, request, options = {}) {
192383
193444
  workspaceDir: snapshot.root,
192384
193445
  networkMode,
192385
193446
  manager,
192386
- trace
193447
+ trace,
193448
+ warnings,
193449
+ budgetTracker
192387
193450
  });
192388
193451
  warnings.push(...agentResults.flatMap((result) => (result.warnings ?? []).map((warning) => `Agent ${result.agent} ${warning}`)));
192389
- const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
192390
- const agentsUsed = normalizedAgentResults.map((result) => result.agent);
192391
- const reviewMode = agentsUsed.length === 1 ? "single_agent" : "multi_agent";
193452
+ const normalized = agentResults.map((result) => normalizeAgentRunResult(result, reviewBudget.maxFindingsPerAgent));
193453
+ const normalizedAgentResults = normalized.map((item) => item.result);
193454
+ for (const item of normalized.filter((item2) => item2.findingsCapped)) {
193455
+ budgetTracker.markIncomplete("coverage_incomplete");
193456
+ warnings.push(`Agent ${item.result.agent} findings were capped at ${reviewBudget.maxFindingsPerAgent}; review coverage is incomplete.`);
193457
+ }
193458
+ const enabledAgents = ["codex", "claude"].filter((agent) => loaded.config.agents[agent].enabled);
193459
+ const agentsUsed = normalizedAgentResults.filter((result) => result.status !== "skipped").map((result) => result.agent);
193460
+ const reviewMode = enabledAgents.length === 1 ? "single_agent" : "multi_agent";
192392
193461
  const completed = normalizedAgentResults.filter((result) => result.status === "completed");
192393
- const degraded = completed.length !== agentResults.length;
193462
+ const attempted = normalizedAgentResults.filter((result) => result.status !== "skipped");
193463
+ const degraded = completed.length !== attempted.length || enabledAgents.length === 0;
192394
193464
  let aggregate = aggregateAgentResults(normalizedAgentResults, {
192395
193465
  reviewMode
192396
193466
  });
@@ -192406,7 +193476,12 @@ async function runReview(tool, request, options = {}) {
192406
193476
  ])
192407
193477
  };
192408
193478
  }
192409
- if (completed.length === 0) {
193479
+ if (completed.length === 0 && (attempted.length > 0 || enabledAgents.length === 0)) {
193480
+ const noPrimaryAgents = enabledAgents.length === 0;
193481
+ if (noPrimaryAgents) {
193482
+ budgetTracker.markIncomplete("coverage_incomplete");
193483
+ warnings.push("No primary review agents are enabled; review coverage is incomplete.");
193484
+ }
192410
193485
  aggregate = {
192411
193486
  ...aggregate,
192412
193487
  findings: [
@@ -192415,9 +193490,9 @@ async function runReview(tool, request, options = {}) {
192415
193490
  id: `KYOSO-${aggregate.findings.length + 1}`,
192416
193491
  severity: "critical",
192417
193492
  category: "other",
192418
- title: "All backend agents failed",
192419
- evidence: normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
192420
- recommendation: "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
193493
+ title: noPrimaryAgents ? "No primary review agents enabled" : "All backend agents failed",
193494
+ evidence: noPrimaryAgents ? "Both configured primary reviewers are disabled." : normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
193495
+ recommendation: noPrimaryAgents ? "Enable at least one primary reviewer before running Kyoso." : "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
192421
193496
  sourceAgents: ["kyoso_policy"],
192422
193497
  confidence: "high"
192423
193498
  }
@@ -192430,7 +193505,7 @@ async function runReview(tool, request, options = {}) {
192430
193505
  findingCount: aggregate.findings.length,
192431
193506
  timestamp: new Date().toISOString()
192432
193507
  });
192433
- const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled ? "cross_agent" : undefined;
193508
+ const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled && enabledAgents.length > 1 ? "cross_agent" : undefined;
192434
193509
  if (verificationMode === "cross_agent") {
192435
193510
  warnings.push(...await runFindingVerification({
192436
193511
  tool,
@@ -192441,11 +193516,16 @@ async function runReview(tool, request, options = {}) {
192441
193516
  networkMode,
192442
193517
  manager,
192443
193518
  trace,
192444
- findings: aggregate.findings
193519
+ findings: aggregate.findings,
193520
+ budgetTracker
192445
193521
  }));
192446
193522
  }
193523
+ if (aggregate.findings.some((finding) => finding.verification?.status === "refuted")) {
193524
+ budgetTracker.markIncomplete("disputed_finding");
193525
+ }
192447
193526
  const cisa = tool === "security_review" ? computeCisaGate(aggregate.findings, normalizedAgentResults) : undefined;
192448
- const decision = decide({
193527
+ const budgetBeforeJudge = budgetTracker.snapshot();
193528
+ const decision = budgetBeforeJudge.completion.status === "incomplete" ? "block" : decide({
192449
193529
  tool,
192450
193530
  findings: aggregate.findings,
192451
193531
  cisa,
@@ -192455,6 +193535,9 @@ async function runReview(tool, request, options = {}) {
192455
193535
  const completedAt = new Date().toISOString();
192456
193536
  const resultWithoutMarkdown = {
192457
193537
  decision,
193538
+ completion: budgetBeforeJudge.completion,
193539
+ executionBudget: budgetBeforeJudge.executionBudget,
193540
+ requestFingerprint,
192458
193541
  degraded,
192459
193542
  agentsUsed,
192460
193543
  reviewMode,
@@ -192476,18 +193559,22 @@ async function runReview(tool, request, options = {}) {
192476
193559
  networkMode,
192477
193560
  workspaceMode: "temp_snapshot",
192478
193561
  configHash: loaded.configHash,
192479
- warnings: Array.from(new Set([...warnings, ...trace.warnings]))
193562
+ warnings: Array.from(new Set([...warnings, ...trace.warnings])),
193563
+ modelCalls: budgetBeforeJudge.modelCalls
192480
193564
  }
192481
193565
  };
192482
193566
  const summaryText = defaultSummaryText(resultWithoutMarkdown);
192483
- const judge = await runJudge({
193567
+ const judge = await runBudgetedJudge({
192484
193568
  tool,
192485
193569
  result: resultWithoutMarkdown,
192486
193570
  summaryText,
192487
193571
  agentFindings: buildJudgeAgentFindings(normalizedAgentResults),
192488
193572
  config: loaded.config.judge,
192489
193573
  requestedProvider: request.options?.judgeProvider,
192490
- env: options.env ?? process.env
193574
+ env: options.env ?? process.env,
193575
+ budgetTracker,
193576
+ trace,
193577
+ traceId
192491
193578
  });
192492
193579
  const judgeComments = new Map(judge.output.disagreementComments.map((comment) => [
192493
193580
  comment.topic,
@@ -192498,10 +193585,20 @@ async function runReview(tool, request, options = {}) {
192498
193585
  judgeComment: judgeComments.get(disagreement.topic) ?? disagreement.judgeComment
192499
193586
  }));
192500
193587
  const crossModelAnalysis = buildCrossModelAnalysis(judge, reviewMode);
193588
+ const budgetAfterJudge = budgetTracker.snapshot();
193589
+ const finalDecision = budgetAfterJudge.completion.status === "incomplete" ? "block" : decision;
192501
193590
  const resultAfterJudge = {
192502
193591
  ...resultWithoutMarkdown,
193592
+ decision: finalDecision,
193593
+ completion: budgetAfterJudge.completion,
193594
+ executionBudget: budgetAfterJudge.executionBudget,
192503
193595
  disagreements,
192504
- ...crossModelAnalysis ? { crossModelAnalysis } : {}
193596
+ ...crossModelAnalysis ? { crossModelAnalysis } : {},
193597
+ audit: {
193598
+ ...resultWithoutMarkdown.audit,
193599
+ completedAt: new Date().toISOString(),
193600
+ modelCalls: budgetAfterJudge.modelCalls
193601
+ }
192505
193602
  };
192506
193603
  const judgeEvent = {
192507
193604
  type: "judge_completed",
@@ -192514,10 +193611,16 @@ async function runReview(tool, request, options = {}) {
192514
193611
  judgeEvent.error = judge.error;
192515
193612
  await trace.write(judgeEvent);
192516
193613
  resultAfterJudge.audit.completedAt = new Date().toISOString();
193614
+ await writeReviewBudgetCompleted({
193615
+ trace,
193616
+ traceId,
193617
+ budgetTracker,
193618
+ requestFingerprint
193619
+ });
192517
193620
  await trace.write({
192518
193621
  type: "decision_completed",
192519
193622
  traceId,
192520
- decision,
193623
+ decision: finalDecision,
192521
193624
  timestamp: new Date().toISOString()
192522
193625
  });
192523
193626
  await trace.write({
@@ -192529,7 +193632,7 @@ async function runReview(tool, request, options = {}) {
192529
193632
  tool,
192530
193633
  trace,
192531
193634
  result: resultAfterJudge,
192532
- summaryText: judge.output.summaryText
193635
+ summaryText: resultAfterJudge.completion.status === "incomplete" ? defaultSummaryText(resultAfterJudge) : judge.output.summaryText
192533
193636
  });
192534
193637
  } finally {
192535
193638
  await trace.finalize();
@@ -192540,37 +193643,180 @@ async function runReview(tool, request, options = {}) {
192540
193643
  async function runFindingVerification(input) {
192541
193644
  const allowDemotionRequested = input.config.verification.allowDemotion;
192542
193645
  const selection = selectVerificationTargets(input.findings, input.config.verification.maxFindings);
192543
- markVerificationOverflow(selection.overflow);
192544
- if (selection.selected.length === 0)
192545
- return [];
192546
193646
  const warnings = [];
192547
- const groups = groupVerificationTargetsByVerifier(selection.selected);
193647
+ if (selection.overflow.length > 0) {
193648
+ markVerificationOverflow(selection.overflow, "verification_max_findings");
193649
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193650
+ }
193651
+ if (selection.selected.length === 0)
193652
+ return warnings;
193653
+ const potentialGroups = groupVerificationTargetsByVerifier(selection.selected);
192548
193654
  await input.trace.write({
192549
193655
  type: "verification_started",
192550
193656
  traceId: input.traceId,
192551
193657
  targetCount: selection.selected.length,
192552
193658
  notVerifiedCount: selection.overflow.length,
192553
- verifierCount: groups.length,
193659
+ verifierCount: potentialGroups.length,
192554
193660
  timeoutMs: input.config.verification.timeoutMs,
192555
193661
  allowDemotionRequested,
192556
193662
  timestamp: new Date().toISOString()
192557
193663
  });
192558
- const agentInputs = groups.map(({ verifier, findings }) => ({
193664
+ if (input.budgetTracker.budget.skipOptionalPhasesWhenTokenUsageUnknown && input.budgetTracker.isTokenUsageUnknown()) {
193665
+ markVerificationOverflow(selection.selected, "token_usage_unknown");
193666
+ input.budgetTracker.markIncomplete("token_usage_unknown");
193667
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193668
+ warnings.push("Finding verification was skipped because primary-agent token usage was not reported.");
193669
+ for (const group of potentialGroups) {
193670
+ input.budgetTracker.recordSkipped({
193671
+ kind: "verifier",
193672
+ agent: group.verifier,
193673
+ reason: "token_usage_unknown"
193674
+ });
193675
+ await input.trace.write({
193676
+ type: "model_call_skipped",
193677
+ traceId: input.traceId,
193678
+ kind: "verifier",
193679
+ agent: group.verifier,
193680
+ reason: "token_usage_unknown",
193681
+ timestamp: new Date().toISOString()
193682
+ });
193683
+ }
193684
+ await input.trace.write({
193685
+ type: "verification_completed",
193686
+ traceId: input.traceId,
193687
+ counts: countVerificationStatuses(input.findings),
193688
+ timestamp: new Date().toISOString()
193689
+ });
193690
+ return warnings;
193691
+ }
193692
+ const groups = new Map;
193693
+ const unavailableVerifiers = new Map;
193694
+ for (const target of selection.selected) {
193695
+ const existing = groups.get(target.verifier);
193696
+ if (existing) {
193697
+ existing.targets.push(target);
193698
+ continue;
193699
+ }
193700
+ const unavailable = unavailableVerifiers.get(target.verifier);
193701
+ if (unavailable) {
193702
+ markVerificationOverflow([target], unavailable === "model_call_budget" ? "budget_exhausted" : "deadline");
193703
+ continue;
193704
+ }
193705
+ const reservationResult = input.budgetTracker.reserve({
193706
+ kind: "verifier",
193707
+ agent: target.verifier
193708
+ });
193709
+ if ("failure" in reservationResult) {
193710
+ unavailableVerifiers.set(target.verifier, reservationResult.failure.reason);
193711
+ markVerificationOverflow([target], reservationResult.failure.reason === "model_call_budget" ? "budget_exhausted" : "deadline");
193712
+ input.budgetTracker.recordSkipped({
193713
+ kind: "verifier",
193714
+ agent: target.verifier,
193715
+ reason: reservationResult.failure.reason
193716
+ });
193717
+ input.budgetTracker.markIncomplete(reservationResult.failure.reason);
193718
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193719
+ await input.trace.write({
193720
+ type: "review_budget_exhausted",
193721
+ traceId: input.traceId,
193722
+ phase: "verification",
193723
+ kind: "verifier",
193724
+ agent: target.verifier,
193725
+ reason: reservationResult.failure.reason,
193726
+ timestamp: new Date().toISOString()
193727
+ });
193728
+ await input.trace.write({
193729
+ type: "model_call_skipped",
193730
+ traceId: input.traceId,
193731
+ kind: "verifier",
193732
+ agent: target.verifier,
193733
+ reason: reservationResult.failure.reason,
193734
+ timestamp: new Date().toISOString()
193735
+ });
193736
+ continue;
193737
+ }
193738
+ const group = {
193739
+ verifier: target.verifier,
193740
+ targets: [target],
193741
+ reservation: reservationResult.reservation
193742
+ };
193743
+ groups.set(target.verifier, group);
193744
+ await input.trace.write({
193745
+ type: "model_call_reserved",
193746
+ traceId: input.traceId,
193747
+ kind: "verifier",
193748
+ agent: target.verifier,
193749
+ timestamp: new Date().toISOString()
193750
+ });
193751
+ }
193752
+ const scheduledGroups = [];
193753
+ for (const group of groups.values()) {
193754
+ const timeoutMs = Math.min(input.config.verification.timeoutMs, input.budgetTracker.remainingWallTimeMs());
193755
+ if (timeoutMs > 0) {
193756
+ scheduledGroups.push(group);
193757
+ continue;
193758
+ }
193759
+ input.budgetTracker.skip(group.reservation, "deadline");
193760
+ input.budgetTracker.markIncomplete("deadline");
193761
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193762
+ markVerificationOverflow(group.targets, "deadline");
193763
+ await input.trace.write({
193764
+ type: "review_budget_exhausted",
193765
+ traceId: input.traceId,
193766
+ phase: "verification",
193767
+ kind: "verifier",
193768
+ agent: group.verifier,
193769
+ reason: "deadline",
193770
+ timestamp: new Date().toISOString()
193771
+ });
193772
+ await input.trace.write({
193773
+ type: "model_call_skipped",
193774
+ traceId: input.traceId,
193775
+ kind: "verifier",
193776
+ agent: group.verifier,
193777
+ reason: "deadline",
193778
+ timestamp: new Date().toISOString()
193779
+ });
193780
+ }
193781
+ if (scheduledGroups.length === 0) {
193782
+ await input.trace.write({
193783
+ type: "verification_completed",
193784
+ traceId: input.traceId,
193785
+ counts: countVerificationStatuses(input.findings),
193786
+ timestamp: new Date().toISOString()
193787
+ });
193788
+ return warnings;
193789
+ }
193790
+ const agentInputs = scheduledGroups.map((group) => ({
192559
193791
  traceId: input.traceId,
192560
- agent: verifier,
193792
+ agent: group.verifier,
192561
193793
  role: "finding_verifier",
192562
193794
  tool: input.tool,
192563
- prompt: buildFindingVerifierPrompt(input.tool, input.request, verifier, findings),
193795
+ prompt: buildFindingVerifierPrompt(input.tool, input.request, group.verifier, group.targets.map((target) => target.finding)),
192564
193796
  workspaceDir: input.workspaceDir,
192565
- timeoutMs: input.config.verification.timeoutMs,
192566
- networkMode: input.networkMode
193797
+ timeoutMs: Math.min(input.config.verification.timeoutMs, input.budgetTracker.remainingWallTimeMs()),
193798
+ deadlineAtEpochMs: input.budgetTracker.deadlineAtEpochMs,
193799
+ maxOutputBytes: input.budgetTracker.budget.maxAgentOutputBytes,
193800
+ networkMode: input.networkMode,
193801
+ onStarted: () => {
193802
+ input.budgetTracker.markStarted(group.reservation);
193803
+ return Promise.resolve();
193804
+ }
192567
193805
  }));
192568
193806
  let results;
192569
193807
  try {
192570
193808
  results = await input.manager.runAll(agentInputs);
192571
193809
  } catch (error51) {
192572
- for (const group of groups) {
192573
- applyVerificationVerdicts(selection.selected, group.verifier, undefined);
193810
+ for (const group of scheduledGroups) {
193811
+ applyVerificationVerdicts(group.targets, group.verifier, undefined);
193812
+ await finalizeModelCallResult({
193813
+ budgetTracker: input.budgetTracker,
193814
+ reservation: group.reservation,
193815
+ result: failedVerifierResult(group.verifier, "AGENT_MANAGER_FAILED"),
193816
+ trace: input.trace,
193817
+ traceId: input.traceId
193818
+ });
193819
+ input.budgetTracker.markIncomplete("coverage_incomplete");
192574
193820
  }
192575
193821
  const message = `Finding verification failed: ${sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51))}`;
192576
193822
  warnings.push(message);
@@ -192583,9 +193829,32 @@ async function runFindingVerification(input) {
192583
193829
  });
192584
193830
  return warnings;
192585
193831
  }
192586
- for (const result of results) {
193832
+ const resultByAgent = new Map(results.map((result) => [result.agent, result]));
193833
+ for (const group of scheduledGroups) {
193834
+ const result = resultByAgent.get(group.verifier);
193835
+ if (!result) {
193836
+ applyVerificationVerdicts(group.targets, group.verifier, undefined);
193837
+ await finalizeModelCallResult({
193838
+ budgetTracker: input.budgetTracker,
193839
+ reservation: group.reservation,
193840
+ result: failedVerifierResult(group.verifier, "AGENT_RESULT_MISSING"),
193841
+ trace: input.trace,
193842
+ traceId: input.traceId
193843
+ });
193844
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193845
+ warnings.push(`Finding verification by ${group.verifier} did not return a result.`);
193846
+ continue;
193847
+ }
193848
+ await finalizeModelCallResult({
193849
+ budgetTracker: input.budgetTracker,
193850
+ reservation: group.reservation,
193851
+ result,
193852
+ trace: input.trace,
193853
+ traceId: input.traceId
193854
+ });
192587
193855
  if (result.status !== "completed") {
192588
- applyVerificationVerdicts(selection.selected, result.agent, undefined);
193856
+ applyVerificationVerdicts(group.targets, result.agent, undefined);
193857
+ input.budgetTracker.markIncomplete("coverage_incomplete");
192589
193858
  const message = `Finding verification by ${result.agent} ${result.status}: ${result.error?.message ?? result.error?.code ?? "no detail"}`;
192590
193859
  warnings.push(sanitizeTextForDisplay(message));
192591
193860
  await input.trace.write({
@@ -192599,8 +193868,9 @@ async function runFindingVerification(input) {
192599
193868
  continue;
192600
193869
  }
192601
193870
  const verdicts = parseVerificationVerdicts(result.rawText);
192602
- applyVerificationVerdicts(selection.selected, result.agent, verdicts);
193871
+ applyVerificationVerdicts(group.targets, result.agent, verdicts);
192603
193872
  if (!verdicts) {
193873
+ input.budgetTracker.markIncomplete("coverage_incomplete");
192604
193874
  const message = `Finding verification by ${result.agent} returned malformed verdict JSON.`;
192605
193875
  warnings.push(message);
192606
193876
  await input.trace.write({
@@ -192620,6 +193890,20 @@ async function runFindingVerification(input) {
192620
193890
  });
192621
193891
  return warnings;
192622
193892
  }
193893
+ function failedVerifierResult(agent, code) {
193894
+ const timestamp = new Date().toISOString();
193895
+ return {
193896
+ agent,
193897
+ role: "finding_verifier",
193898
+ status: "failed",
193899
+ startedAt: timestamp,
193900
+ completedAt: timestamp,
193901
+ error: {
193902
+ code,
193903
+ message: "The agent manager did not return a verification result."
193904
+ }
193905
+ };
193906
+ }
192623
193907
  function buildJudgeAgentFindings(results) {
192624
193908
  return results.flatMap((result) => {
192625
193909
  if (!result.normalized)
@@ -192656,27 +193940,277 @@ function buildCrossModelAnalysis(judge, reviewMode) {
192656
193940
  provider: judge.provider
192657
193941
  };
192658
193942
  }
192659
- async function runAgents(input) {
192660
- const agentRoles = resolveAgentRoles(input.config);
192661
- const agentInputs = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => ({
193943
+ async function runBudgetedJudge(input) {
193944
+ const configuredProvider = input.requestedProvider ?? input.config.provider;
193945
+ const provider = resolveJudgeProvider(configuredProvider, input.env);
193946
+ if (input.config.mode === "deterministic_only" || provider === "deterministic_fallback") {
193947
+ return runJudge(input);
193948
+ }
193949
+ const fallback = () => runJudge({
193950
+ ...input,
193951
+ config: { ...input.config, mode: "deterministic_only" }
193952
+ });
193953
+ if (input.budgetTracker.snapshot().completion.status === "incomplete") {
193954
+ await recordSkippedJudgeCall(input, "review_incomplete");
193955
+ return fallback();
193956
+ }
193957
+ if (input.budgetTracker.budget.skipOptionalPhasesWhenTokenUsageUnknown && input.budgetTracker.isTokenUsageUnknown()) {
193958
+ await recordSkippedJudgeCall(input, "token_usage_unknown");
193959
+ return fallback();
193960
+ }
193961
+ const reservationResult = input.budgetTracker.reserve({ kind: "judge" });
193962
+ if ("failure" in reservationResult) {
193963
+ await recordSkippedJudgeCall(input, reservationResult.failure.reason);
193964
+ await input.trace.write({
193965
+ type: "review_budget_exhausted",
193966
+ traceId: input.traceId,
193967
+ phase: "judge",
193968
+ kind: "judge",
193969
+ reason: reservationResult.failure.reason,
193970
+ timestamp: new Date().toISOString()
193971
+ });
193972
+ return fallback();
193973
+ }
193974
+ const reservation = reservationResult.reservation;
193975
+ await input.trace.write({
193976
+ type: "model_call_reserved",
192662
193977
  traceId: input.traceId,
192663
- agent,
192664
- role: agentRoles[agent] ?? input.config.agents[agent].role,
192665
- tool: input.tool,
192666
- prompt: buildAgentPrompt(input.tool, input.request, agent, agentRoles[agent] ?? input.config.agents[agent].role),
192667
- workspaceDir: input.workspaceDir,
192668
- timeoutMs: input.request.options?.maxAgentTimeoutMs ?? input.config.agents[agent].timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
192669
- networkMode: input.networkMode
192670
- }));
192671
- await Promise.all(agentInputs.map((agentInput) => input.trace.write({
192672
- type: "agent_started",
193978
+ kind: "judge",
193979
+ timestamp: new Date().toISOString()
193980
+ });
193981
+ const timeoutMs = Math.min(input.config.timeoutMs, input.budgetTracker.remainingWallTimeMs());
193982
+ if (timeoutMs <= 0) {
193983
+ input.budgetTracker.skip(reservation, "deadline");
193984
+ await input.trace.write({
193985
+ type: "review_budget_exhausted",
193986
+ traceId: input.traceId,
193987
+ phase: "judge",
193988
+ kind: "judge",
193989
+ reason: "deadline",
193990
+ timestamp: new Date().toISOString()
193991
+ });
193992
+ await input.trace.write({
193993
+ type: "model_call_skipped",
193994
+ traceId: input.traceId,
193995
+ kind: "judge",
193996
+ reason: "deadline",
193997
+ timestamp: new Date().toISOString()
193998
+ });
193999
+ return fallback();
194000
+ }
194001
+ input.budgetTracker.markStarted(reservation);
194002
+ const judge = await runJudge({ ...input, timeoutMs });
194003
+ const usage = normalizeModelTokenUsage(judge.usage);
194004
+ input.budgetTracker.complete(reservation, {
194005
+ ...usage ? { usage } : {}
194006
+ });
194007
+ await input.trace.write({
194008
+ type: "model_call_completed",
192673
194009
  traceId: input.traceId,
192674
- agent: agentInput.agent,
192675
- role: agentInput.role,
194010
+ kind: "judge",
194011
+ provider: judge.provider,
194012
+ resultStatus: judge.status,
194013
+ ...usage ? { usage } : {},
192676
194014
  timestamp: new Date().toISOString()
192677
- })));
192678
- const results = await input.manager.runAll(agentInputs);
192679
- await Promise.all(results.map((result) => {
194015
+ });
194016
+ return judge;
194017
+ }
194018
+ async function recordSkippedJudgeCall(input, reason) {
194019
+ input.budgetTracker.recordSkipped({ kind: "judge", reason });
194020
+ await input.trace.write({
194021
+ type: "model_call_skipped",
194022
+ traceId: input.traceId,
194023
+ kind: "judge",
194024
+ reason,
194025
+ timestamp: new Date().toISOString()
194026
+ });
194027
+ }
194028
+ async function runAgents(input) {
194029
+ const agentRoles = resolveAgentRoles(input.config);
194030
+ const enabledAgents = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled);
194031
+ if (enabledAgents.length === 0)
194032
+ return [];
194033
+ const reservationResult = input.budgetTracker.reserveMany(enabledAgents.map((agent) => ({ kind: "primary", agent })));
194034
+ if ("failure" in reservationResult) {
194035
+ input.budgetTracker.markIncomplete(reservationResult.failure.reason);
194036
+ input.budgetTracker.markIncomplete("coverage_incomplete");
194037
+ await input.trace.write({
194038
+ type: "review_budget_exhausted",
194039
+ traceId: input.traceId,
194040
+ phase: "primary",
194041
+ reason: reservationResult.failure.reason,
194042
+ requiredCalls: enabledAgents.length,
194043
+ timestamp: new Date().toISOString()
194044
+ });
194045
+ for (const agent of enabledAgents) {
194046
+ input.budgetTracker.recordSkipped({
194047
+ kind: "primary",
194048
+ agent,
194049
+ reason: reservationResult.failure.reason
194050
+ });
194051
+ await input.trace.write({
194052
+ type: "model_call_skipped",
194053
+ traceId: input.traceId,
194054
+ kind: "primary",
194055
+ agent,
194056
+ reason: reservationResult.failure.reason,
194057
+ timestamp: new Date().toISOString()
194058
+ });
194059
+ }
194060
+ return enabledAgents.map((agent) => {
194061
+ const role = agentRoles[agent] ?? input.config.agents[agent].role;
194062
+ const timestamp = new Date().toISOString();
194063
+ return {
194064
+ agent,
194065
+ role,
194066
+ status: "skipped",
194067
+ startedAt: timestamp,
194068
+ completedAt: timestamp,
194069
+ error: {
194070
+ code: reservationResult.failure.reason === "deadline" ? "REVIEW_DEADLINE_EXCEEDED" : "MODEL_CALL_BUDGET_EXHAUSTED",
194071
+ message: reservationResult.failure.reason === "deadline" ? "Review deadline was reached before primary agents could start." : "The review model-call budget cannot reserve all primary agents."
194072
+ }
194073
+ };
194074
+ });
194075
+ }
194076
+ const reservations = new Map(reservationResult.reservations.map((reservation) => [
194077
+ reservation.agent,
194078
+ reservation
194079
+ ]));
194080
+ for (const reservation of reservationResult.reservations) {
194081
+ await input.trace.write({
194082
+ type: "model_call_reserved",
194083
+ traceId: input.traceId,
194084
+ kind: reservation.kind,
194085
+ agent: reservation.agent,
194086
+ timestamp: new Date().toISOString()
194087
+ });
194088
+ }
194089
+ if (input.budgetTracker.remainingWallTimeMs() <= 0) {
194090
+ input.budgetTracker.markIncomplete("deadline");
194091
+ input.budgetTracker.markIncomplete("coverage_incomplete");
194092
+ await input.trace.write({
194093
+ type: "review_budget_exhausted",
194094
+ traceId: input.traceId,
194095
+ phase: "primary",
194096
+ reason: "deadline",
194097
+ timestamp: new Date().toISOString()
194098
+ });
194099
+ return await skipReservedPrimaryAgents({
194100
+ trace: input.trace,
194101
+ traceId: input.traceId,
194102
+ budgetTracker: input.budgetTracker,
194103
+ config: input.config,
194104
+ agents: enabledAgents,
194105
+ agentRoles,
194106
+ reservations,
194107
+ reason: "deadline"
194108
+ });
194109
+ }
194110
+ const startedWrites = [];
194111
+ let acceptingStartedEvents = true;
194112
+ const agentInputs = enabledAgents.map((agent) => {
194113
+ const agentConfig = input.config.agents[agent];
194114
+ const role = agentRoles[agent] ?? agentConfig.role;
194115
+ const reservation = reservations.get(agent);
194116
+ if (!reservation) {
194117
+ throw new Error(`Missing primary budget reservation for ${agent}.`);
194118
+ }
194119
+ return {
194120
+ traceId: input.traceId,
194121
+ agent,
194122
+ role,
194123
+ tool: input.tool,
194124
+ prompt: buildAgentPrompt(input.tool, input.request, agent, role),
194125
+ workspaceDir: input.workspaceDir,
194126
+ timeoutMs: Math.min(input.request.options?.maxAgentTimeoutMs ?? Number.POSITIVE_INFINITY, agentConfig.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS, input.budgetTracker.remainingWallTimeMs()),
194127
+ deadlineAtEpochMs: input.budgetTracker.deadlineAtEpochMs,
194128
+ maxOutputBytes: input.budgetTracker.budget.maxAgentOutputBytes,
194129
+ networkMode: input.networkMode,
194130
+ onStarted: () => {
194131
+ input.budgetTracker.markStarted(reservation);
194132
+ if (!acceptingStartedEvents)
194133
+ return Promise.resolve();
194134
+ const event = {
194135
+ type: "agent_started",
194136
+ traceId: input.traceId,
194137
+ agent,
194138
+ role,
194139
+ timestamp: new Date().toISOString()
194140
+ };
194141
+ if (agentConfig.model) {
194142
+ event.model = sanitizeTextForDisplay(agentConfig.model);
194143
+ }
194144
+ if (agent === "codex" && input.config.agents.codex.provider === CODEX_OPENROUTER_PROVIDER) {
194145
+ event.provider = CODEX_OPENROUTER_PROVIDER;
194146
+ }
194147
+ const write = (async () => {
194148
+ try {
194149
+ await input.trace.write(event);
194150
+ } catch {
194151
+ input.warnings.push("AUDIT_WRITE_FAILED: agent_started event could not be recorded.");
194152
+ }
194153
+ })();
194154
+ startedWrites.push(write);
194155
+ return write;
194156
+ }
194157
+ };
194158
+ });
194159
+ let results;
194160
+ try {
194161
+ results = await input.manager.runAll(agentInputs);
194162
+ } catch (error51) {
194163
+ const detail = sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51));
194164
+ input.warnings.push(`Primary-agent execution failed: ${detail}`);
194165
+ results = agentInputs.map((agentInput) => ({
194166
+ agent: agentInput.agent,
194167
+ role: agentInput.role,
194168
+ status: "failed",
194169
+ startedAt: new Date().toISOString(),
194170
+ completedAt: new Date().toISOString(),
194171
+ error: {
194172
+ code: "AGENT_MANAGER_FAILED",
194173
+ message: "The agent manager did not return a review result."
194174
+ }
194175
+ }));
194176
+ }
194177
+ acceptingStartedEvents = false;
194178
+ await Promise.all(startedWrites);
194179
+ const resultByAgent = new Map(results.map((result) => [result.agent, result]));
194180
+ const orderedResults = enabledAgents.map((agent) => {
194181
+ const existing = resultByAgent.get(agent);
194182
+ if (existing)
194183
+ return existing;
194184
+ const role = agentRoles[agent] ?? input.config.agents[agent].role;
194185
+ const timestamp = new Date().toISOString();
194186
+ return {
194187
+ agent,
194188
+ role,
194189
+ status: "failed",
194190
+ startedAt: timestamp,
194191
+ completedAt: timestamp,
194192
+ error: {
194193
+ code: "AGENT_RESULT_MISSING",
194194
+ message: "The agent manager did not return a review result."
194195
+ }
194196
+ };
194197
+ });
194198
+ for (const result of orderedResults) {
194199
+ const reservation = reservations.get(result.agent);
194200
+ if (!reservation)
194201
+ continue;
194202
+ await finalizeModelCallResult({
194203
+ budgetTracker: input.budgetTracker,
194204
+ reservation,
194205
+ result,
194206
+ trace: input.trace,
194207
+ traceId: input.traceId
194208
+ });
194209
+ if (result.status !== "completed") {
194210
+ input.budgetTracker.markIncomplete("coverage_incomplete");
194211
+ }
194212
+ }
194213
+ await Promise.all(orderedResults.map((result) => {
192680
194214
  const event = {
192681
194215
  type: "agent_completed",
192682
194216
  traceId: input.traceId,
@@ -192696,8 +194230,92 @@ async function runAgents(input) {
192696
194230
  }
192697
194231
  return input.trace.write(event);
192698
194232
  }));
194233
+ return orderedResults;
194234
+ }
194235
+ async function skipReservedPrimaryAgents(input) {
194236
+ const results = [];
194237
+ for (const agent of input.agents) {
194238
+ const reservation = input.reservations.get(agent);
194239
+ if (reservation)
194240
+ input.budgetTracker.skip(reservation, input.reason);
194241
+ await input.trace.write({
194242
+ type: "model_call_skipped",
194243
+ traceId: input.traceId,
194244
+ kind: "primary",
194245
+ agent,
194246
+ reason: input.reason,
194247
+ timestamp: new Date().toISOString()
194248
+ });
194249
+ const timestamp = new Date().toISOString();
194250
+ results.push({
194251
+ agent,
194252
+ role: input.agentRoles[agent] ?? input.config.agents[agent].role,
194253
+ status: "skipped",
194254
+ startedAt: timestamp,
194255
+ completedAt: timestamp,
194256
+ error: {
194257
+ code: "REVIEW_DEADLINE_EXCEEDED",
194258
+ message: "Review deadline was reached before the agent could start."
194259
+ }
194260
+ });
194261
+ }
192699
194262
  return results;
192700
194263
  }
194264
+ async function finalizeModelCallResult(input) {
194265
+ const reason = input.result.error?.code ?? input.result.status;
194266
+ const hasStarted = input.budgetTracker.hasStarted(input.reservation);
194267
+ const canSkip = input.result.status === "skipped" || isPreflightAgentFailure(input.result) || !hasStarted && input.result.error?.code === "REVIEW_DEADLINE_EXCEEDED";
194268
+ if (canSkip && !hasStarted) {
194269
+ input.budgetTracker.skip(input.reservation, reason);
194270
+ if (input.result.error?.code === "REVIEW_DEADLINE_EXCEEDED") {
194271
+ input.budgetTracker.markIncomplete("deadline");
194272
+ }
194273
+ await input.trace.write({
194274
+ type: "model_call_skipped",
194275
+ traceId: input.traceId,
194276
+ kind: input.reservation.kind,
194277
+ agent: input.reservation.agent,
194278
+ reason,
194279
+ timestamp: new Date().toISOString()
194280
+ });
194281
+ return;
194282
+ }
194283
+ input.budgetTracker.markStarted(input.reservation);
194284
+ const usage = normalizeModelTokenUsage(input.result.usage);
194285
+ const outputBytes = input.result.outputBytes ?? (input.result.rawText ? Buffer.byteLength(input.result.rawText, "utf8") : undefined);
194286
+ input.budgetTracker.complete(input.reservation, {
194287
+ ...outputBytes === undefined ? {} : { outputBytes },
194288
+ ...usage ? { usage } : {},
194289
+ ...input.result.stopReason ? { stopReason: input.result.stopReason } : {}
194290
+ });
194291
+ if (input.result.error?.code === "AGENT_OUTPUT_LIMIT") {
194292
+ input.budgetTracker.markIncomplete("agent_output_limit");
194293
+ }
194294
+ if (input.result.error?.code === "REVIEW_DEADLINE_EXCEEDED") {
194295
+ input.budgetTracker.markIncomplete("deadline");
194296
+ }
194297
+ await input.trace.write({
194298
+ type: "model_call_completed",
194299
+ traceId: input.traceId,
194300
+ kind: input.reservation.kind,
194301
+ agent: input.reservation.agent,
194302
+ resultStatus: input.result.status,
194303
+ ...input.result.error?.code ? { errorCode: input.result.error.code } : {},
194304
+ ...outputBytes === undefined ? {} : { outputBytes },
194305
+ ...usage ? { usage } : {},
194306
+ ...input.result.stopReason ? { stopReason: input.result.stopReason } : {},
194307
+ timestamp: new Date().toISOString()
194308
+ });
194309
+ }
194310
+ function isPreflightAgentFailure(result) {
194311
+ return result.status === "failed" && [
194312
+ "AGENT_CONFIG_INVALID",
194313
+ "OPENROUTER_KEY_MISSING",
194314
+ "AGENT_SPAWN_FAILED",
194315
+ "AGENT_MANAGER_FAILED",
194316
+ "AGENT_RESULT_MISSING"
194317
+ ].includes(result.error?.code ?? "");
194318
+ }
192701
194319
  function resolveAgentRoles(config2) {
192702
194320
  const enabledAgents = ["codex", "claude"].filter((agent) => config2.agents[agent].enabled);
192703
194321
  const singleAgentMode = enabledAgents.length === 1;
@@ -192707,19 +194325,36 @@ function resolveAgentRoles(config2) {
192707
194325
  }
192708
194326
  return roles;
192709
194327
  }
192710
- function defaultAgentManager(config2) {
192711
- if (process.env.KYOSO_TEST_FAKE_AGENTS === "1")
194328
+ function defaultAgentManager(config2, parentEnv) {
194329
+ if (parentEnv.KYOSO_TEST_FAKE_AGENTS === "1") {
192712
194330
  return new FakeAgentManager;
192713
- return new SubprocessAcpAgentManager(config2);
192714
- }
192715
- function normalizeAgentRunResult(result) {
192716
- if (result.status === "completed" && result.rawText && !result.normalized) {
192717
- return {
192718
- ...result,
192719
- normalized: normalizeAgentOutput(result.agent, result.role, result.rawText)
192720
- };
192721
194331
  }
192722
- return result;
194332
+ return new SubprocessAcpAgentManager(config2, parentEnv);
194333
+ }
194334
+ function normalizeAgentRunResult(result, maxFindingsPerAgent) {
194335
+ const normalizedResult = result.status === "completed" && result.rawText && !result.normalized ? {
194336
+ ...result,
194337
+ normalized: normalizeAgentOutput(result.agent, result.role, result.rawText)
194338
+ } : result;
194339
+ const normalized = normalizedResult.normalized;
194340
+ const findings = normalized?.findings;
194341
+ if (!normalized || !findings || findings.length <= maxFindingsPerAgent) {
194342
+ return { result: normalizedResult, findingsCapped: false };
194343
+ }
194344
+ const limitedFindings = findings.map((finding, index) => ({ finding, index })).sort((left, right) => {
194345
+ const severity = compareSeverity(left.finding.severity, right.finding.severity);
194346
+ return severity === 0 ? left.index - right.index : severity;
194347
+ }).slice(0, maxFindingsPerAgent).map(({ finding }) => finding);
194348
+ return {
194349
+ result: {
194350
+ ...normalizedResult,
194351
+ normalized: {
194352
+ ...normalized,
194353
+ findings: limitedFindings
194354
+ }
194355
+ },
194356
+ findingsCapped: true
194357
+ };
192723
194358
  }
192724
194359
  function agentOpinionSummary(result, includeRawText = false) {
192725
194360
  const opinion = {
@@ -192741,8 +194376,12 @@ async function buildSecretBlockResult(input) {
192741
194376
  });
192742
194377
  const cisa = input.tool === "security_review" ? computeCisaGate([finding], []) : undefined;
192743
194378
  const completedAt = new Date().toISOString();
194379
+ const budget = input.budgetTracker.snapshot();
192744
194380
  const resultWithoutMarkdown = {
192745
194381
  decision: "block",
194382
+ completion: budget.completion,
194383
+ executionBudget: budget.executionBudget,
194384
+ requestFingerprint: input.requestFingerprint,
192746
194385
  degraded: false,
192747
194386
  agentsUsed: [],
192748
194387
  reviewMode: "multi_agent",
@@ -192778,9 +194417,16 @@ async function buildSecretBlockResult(input) {
192778
194417
  networkMode: input.networkMode,
192779
194418
  workspaceMode: "temp_snapshot",
192780
194419
  configHash: input.configHash,
192781
- warnings: input.warnings
194420
+ warnings: input.warnings,
194421
+ modelCalls: budget.modelCalls
192782
194422
  }
192783
194423
  };
194424
+ await writeReviewBudgetCompleted({
194425
+ trace: input.trace,
194426
+ traceId: input.traceId,
194427
+ budgetTracker: input.budgetTracker,
194428
+ requestFingerprint: input.requestFingerprint
194429
+ });
192784
194430
  await input.trace.write({
192785
194431
  type: "decision_completed",
192786
194432
  traceId: input.traceId,
@@ -192823,8 +194469,12 @@ function reindexFindings(findings) {
192823
194469
  }
192824
194470
  async function buildPolicyBlockResult(input) {
192825
194471
  const completedAt = new Date().toISOString();
194472
+ const budget = input.budgetTracker.snapshot();
192826
194473
  const resultWithoutMarkdown = {
192827
194474
  decision: "block",
194475
+ completion: budget.completion,
194476
+ executionBudget: budget.executionBudget,
194477
+ requestFingerprint: input.requestFingerprint,
192828
194478
  degraded: false,
192829
194479
  agentsUsed: [],
192830
194480
  reviewMode: "multi_agent",
@@ -192843,9 +194493,16 @@ async function buildPolicyBlockResult(input) {
192843
194493
  networkMode: input.networkMode,
192844
194494
  workspaceMode: "temp_snapshot",
192845
194495
  configHash: input.configHash,
192846
- warnings: [input.warning]
194496
+ warnings: [input.warning],
194497
+ modelCalls: budget.modelCalls
192847
194498
  }
192848
194499
  };
194500
+ await writeReviewBudgetCompleted({
194501
+ trace: input.trace,
194502
+ traceId: input.traceId,
194503
+ budgetTracker: input.budgetTracker,
194504
+ requestFingerprint: input.requestFingerprint
194505
+ });
192849
194506
  await input.trace.write({
192850
194507
  type: "decision_completed",
192851
194508
  traceId: input.traceId,
@@ -192882,6 +194539,33 @@ async function finalizeReviewResult(input) {
192882
194539
  })
192883
194540
  };
192884
194541
  }
194542
+ async function writeReviewBudgetPlanned(input) {
194543
+ const snapshot = input.budgetTracker.snapshot();
194544
+ await input.trace.write({
194545
+ type: "review_budget_planned",
194546
+ traceId: input.traceId,
194547
+ requestFingerprint: input.requestFingerprint,
194548
+ maxModelCalls: snapshot.executionBudget.maxModelCalls,
194549
+ maxTotalWallTimeMs: snapshot.executionBudget.wallTime.limitMs,
194550
+ maxAgentOutputBytes: snapshot.executionBudget.maxAgentOutputBytes,
194551
+ maxFindingsPerAgent: snapshot.executionBudget.maxFindingsPerAgent,
194552
+ skipOptionalPhasesWhenTokenUsageUnknown: snapshot.executionBudget.skipOptionalPhasesWhenTokenUsageUnknown,
194553
+ timestamp: new Date().toISOString()
194554
+ });
194555
+ }
194556
+ async function writeReviewBudgetCompleted(input) {
194557
+ const snapshot = input.budgetTracker.snapshot();
194558
+ await input.trace.write({
194559
+ type: "review_budget_completed",
194560
+ traceId: input.traceId,
194561
+ requestFingerprint: input.requestFingerprint,
194562
+ completion: snapshot.completion,
194563
+ modelCalls: snapshot.executionBudget.modelCalls,
194564
+ wallTime: snapshot.executionBudget.wallTime,
194565
+ tokenUsage: snapshot.executionBudget.tokenUsage,
194566
+ timestamp: new Date().toISOString()
194567
+ });
194568
+ }
192885
194569
  function mergeDenyPatterns(configDeny, requestDeny) {
192886
194570
  return Array.from(new Set([...configDeny, ...requestDeny ?? []]));
192887
194571
  }