@kyo-so/cli 0.10.0 → 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/bin/kyoso.js CHANGED
@@ -183945,10 +183945,12 @@ function matchesPathPattern(path, patterns, mode) {
183945
183945
 
183946
183946
  // src/core/constants.ts
183947
183947
  var DEFAULT_AGENT_TIMEOUT_MS = 120000;
183948
+ var MAX_AGENT_OUTPUT_BYTES = 1048576;
183949
+ var JUDGE_MAX_OUTPUT_TOKENS = 4096;
183948
183950
  var RAW_OUTPUT_MAX_CHARS = 16384;
183949
183951
  var TRACE_DIR = ".kyoso/traces";
183950
183952
  var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
183951
- var KYOSO_VERSION = "0.10.0";
183953
+ var KYOSO_VERSION = "0.11.0";
183952
183954
 
183953
183955
  // src/utils/pathContainment.ts
183954
183956
  import { resolve, sep as sep2 } from "node:path";
@@ -184345,7 +184347,7 @@ var defaultConfig = {
184345
184347
  }
184346
184348
  },
184347
184349
  judge: {
184348
- mode: "deterministic_plus_llm",
184350
+ mode: "deterministic_only",
184349
184351
  provider: "auto",
184350
184352
  timeoutMs: 60000
184351
184353
  },
@@ -184355,6 +184357,13 @@ var defaultConfig = {
184355
184357
  timeoutMs: 90000,
184356
184358
  allowDemotion: false
184357
184359
  },
184360
+ reviewBudget: {
184361
+ maxModelCalls: 4,
184362
+ maxTotalWallTimeMs: 480000,
184363
+ maxAgentOutputBytes: 65536,
184364
+ maxFindingsPerAgent: 10,
184365
+ skipOptionalPhasesWhenTokenUsageUnknown: true
184366
+ },
184358
184367
  audit: {
184359
184368
  enabled: true,
184360
184369
  format: "jsonl",
@@ -184412,7 +184421,7 @@ function collectProjectScopeViolations(config2) {
184412
184421
  const violations = [];
184413
184422
  for (const leaf of leaves) {
184414
184423
  const path = leaf.path.join(".");
184415
- const globalOnlyReason = PROJECT_GLOBAL_ONLY_REASONS[path];
184424
+ const globalOnlyReason = projectGlobalOnlyReason(leaf.path);
184416
184425
  if (globalOnlyReason) {
184417
184426
  violations.push({ path, reason: globalOnlyReason });
184418
184427
  continue;
@@ -184427,6 +184436,15 @@ function collectProjectScopeViolations(config2) {
184427
184436
  }
184428
184437
  return violations.sort((left, right) => left.path.localeCompare(right.path));
184429
184438
  }
184439
+ function projectGlobalOnlyReason(path) {
184440
+ const exactReason = PROJECT_GLOBAL_ONLY_REASONS[path.join(".")];
184441
+ if (exactReason)
184442
+ return exactReason;
184443
+ if (path[0] === "reviewBudget") {
184444
+ return "must be a user-global review budget ceiling";
184445
+ }
184446
+ return;
184447
+ }
184430
184448
  function isAllowedProjectPath(path) {
184431
184449
  const [top, second, third, fourth] = path;
184432
184450
  if (isAllowedConfigOverridePath(path))
@@ -184599,6 +184617,13 @@ var codexAgentSchema = baseAgentSchema.extend({
184599
184617
  }
184600
184618
  });
184601
184619
  });
184620
+ var reviewBudgetSchema = exports_external.object({
184621
+ maxModelCalls: exports_external.number().int().positive(),
184622
+ maxTotalWallTimeMs: exports_external.number().int().positive(),
184623
+ maxAgentOutputBytes: exports_external.number().int().positive().max(MAX_AGENT_OUTPUT_BYTES),
184624
+ maxFindingsPerAgent: exports_external.number().int().positive(),
184625
+ skipOptionalPhasesWhenTokenUsageUnknown: exports_external.boolean()
184626
+ });
184602
184627
  var kyosoConfigSchema = exports_external.object({
184603
184628
  entrypoints: exports_external.object({
184604
184629
  mcp: exports_external.boolean(),
@@ -184656,6 +184681,7 @@ var kyosoConfigSchema = exports_external.object({
184656
184681
  timeoutMs: exports_external.number().int().positive().default(90000),
184657
184682
  allowDemotion: exports_external.boolean().default(false)
184658
184683
  }),
184684
+ reviewBudget: reviewBudgetSchema,
184659
184685
  audit: exports_external.object({
184660
184686
  enabled: exports_external.boolean(),
184661
184687
  format: exports_external.literal("jsonl"),
@@ -184663,6 +184689,15 @@ var kyosoConfigSchema = exports_external.object({
184663
184689
  includeRawAgentOutput: exports_external.boolean(),
184664
184690
  includeFileContents: exports_external.boolean()
184665
184691
  })
184692
+ }).superRefine((config2, context) => {
184693
+ const enabledPrimaryReviewers = Object.values(config2.agents).filter((agent) => agent.enabled).length;
184694
+ if (config2.reviewBudget.maxModelCalls >= enabledPrimaryReviewers)
184695
+ return;
184696
+ context.addIssue({
184697
+ code: exports_external.ZodIssueCode.custom,
184698
+ path: ["reviewBudget", "maxModelCalls"],
184699
+ message: "must be greater than or equal to the number of enabled primary reviewers."
184700
+ });
184666
184701
  });
184667
184702
  function agentConfigLeafPaths(agent) {
184668
184703
  const paths = [
@@ -184721,6 +184756,11 @@ var kyosoConfigKnownLeafPaths = [
184721
184756
  "verification.maxFindings",
184722
184757
  "verification.timeoutMs",
184723
184758
  "verification.allowDemotion",
184759
+ "reviewBudget.maxModelCalls",
184760
+ "reviewBudget.maxTotalWallTimeMs",
184761
+ "reviewBudget.maxAgentOutputBytes",
184762
+ "reviewBudget.maxFindingsPerAgent",
184763
+ "reviewBudget.skipOptionalPhasesWhenTokenUsageUnknown",
184724
184764
  "audit.enabled",
184725
184765
  "audit.format",
184726
184766
  "audit.directory",
@@ -184740,6 +184780,7 @@ var kyosoConfigSecuritySensitivePrefixes = [
184740
184780
  "secrets",
184741
184781
  "securityReview",
184742
184782
  "verification",
184783
+ "reviewBudget",
184743
184784
  "workspace"
184744
184785
  ];
184745
184786
 
@@ -186203,6 +186244,33 @@ async function exists2(path) {
186203
186244
  }
186204
186245
  }
186205
186246
 
186247
+ // src/core/tokenUsage.ts
186248
+ var TOKEN_USAGE_KEYS = [
186249
+ "totalTokens",
186250
+ "inputTokens",
186251
+ "outputTokens",
186252
+ "thoughtTokens",
186253
+ "cachedReadTokens",
186254
+ "cachedWriteTokens"
186255
+ ];
186256
+ function normalizeModelTokenUsage(usage) {
186257
+ if (!isRecord4(usage))
186258
+ return;
186259
+ const normalized = {};
186260
+ for (const key of TOKEN_USAGE_KEYS) {
186261
+ const value = usage[key];
186262
+ if (isTokenCount(value))
186263
+ normalized[key] = value;
186264
+ }
186265
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
186266
+ }
186267
+ function isTokenCount(value) {
186268
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
186269
+ }
186270
+ function isRecord4(value) {
186271
+ return typeof value === "object" && value !== null && !Array.isArray(value);
186272
+ }
186273
+
186206
186274
  // src/judge/prompt.ts
186207
186275
  var ANALYSIS_MAX_ITEMS = 5;
186208
186276
  var ANALYSIS_MAX_CHARS = 500;
@@ -186251,7 +186319,7 @@ function parseJudgeOutput(text, fallbackSummaryText) {
186251
186319
  const parsed = JSON.parse(json2);
186252
186320
  const summaryText = typeof parsed.summaryText === "string" && parsed.summaryText.trim().length > 0 ? sanitizeText(parsed.summaryText) : fallbackSummaryText;
186253
186321
  const disagreementComments = Array.isArray(parsed.disagreementComments) ? parsed.disagreementComments.flatMap((item) => {
186254
- if (!isRecord4(item) || typeof item.topic !== "string" || typeof item.judgeComment !== "string") {
186322
+ if (!isRecord5(item) || typeof item.topic !== "string" || typeof item.judgeComment !== "string") {
186255
186323
  return [];
186256
186324
  }
186257
186325
  return [
@@ -186267,7 +186335,7 @@ function parseJudgeOutput(text, fallbackSummaryText) {
186267
186335
  return { summaryText, disagreementComments, analysis };
186268
186336
  }
186269
186337
  function parseAnalysis(value) {
186270
- if (!isRecord4(value))
186338
+ if (!isRecord5(value))
186271
186339
  return;
186272
186340
  if (!Array.isArray(value.blindSpots) || !Array.isArray(value.contradictions) || !Array.isArray(value.partialCoverage)) {
186273
186341
  return;
@@ -186275,7 +186343,7 @@ function parseAnalysis(value) {
186275
186343
  return {
186276
186344
  blindSpots: value.blindSpots.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => typeof item === "string" ? [sanitizeAnalysisText(item)] : []),
186277
186345
  contradictions: value.contradictions.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => {
186278
- if (!isRecord4(item) || typeof item.topic !== "string" || typeof item.detail !== "string") {
186346
+ if (!isRecord5(item) || typeof item.topic !== "string" || typeof item.detail !== "string") {
186279
186347
  return [];
186280
186348
  }
186281
186349
  return [
@@ -186286,7 +186354,7 @@ function parseAnalysis(value) {
186286
186354
  ];
186287
186355
  }),
186288
186356
  partialCoverage: value.partialCoverage.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => {
186289
- if (!isRecord4(item) || typeof item.note !== "string")
186357
+ if (!isRecord5(item) || typeof item.note !== "string")
186290
186358
  return [];
186291
186359
  const findingId = typeof item.findingId === "string" ? sanitizeAnalysisText(item.findingId) : undefined;
186292
186360
  return [
@@ -186333,7 +186401,7 @@ function extractFirstJsonObject(text) {
186333
186401
  }
186334
186402
  return;
186335
186403
  }
186336
- function isRecord4(value) {
186404
+ function isRecord5(value) {
186337
186405
  return typeof value === "object" && value !== null && !Array.isArray(value);
186338
186406
  }
186339
186407
 
@@ -186351,7 +186419,7 @@ async function runAnthropicJudge(input2, timeoutMs) {
186351
186419
  },
186352
186420
  body: JSON.stringify({
186353
186421
  model: input2.env.KYOSO_ANTHROPIC_JUDGE_MODEL ?? "claude-haiku-4-5",
186354
- max_tokens: 4096,
186422
+ max_tokens: JUDGE_MAX_OUTPUT_TOKENS,
186355
186423
  temperature: 0,
186356
186424
  messages: [
186357
186425
  {
@@ -186367,7 +186435,21 @@ async function runAnthropicJudge(input2, timeoutMs) {
186367
186435
  const content = payload.content?.find((item) => item.type === "text" && item.text)?.text;
186368
186436
  if (!content)
186369
186437
  throw new Error("Anthropic judge response did not include text content.");
186370
- return parseJudgeOutput(content, input2.summaryText);
186438
+ const usage = normalizeUsage(payload.usage);
186439
+ return {
186440
+ output: parseJudgeOutput(content, input2.summaryText),
186441
+ ...usage ? { usage } : {}
186442
+ };
186443
+ }
186444
+ function normalizeUsage(usage) {
186445
+ if (!usage)
186446
+ return;
186447
+ return normalizeModelTokenUsage({
186448
+ inputTokens: usage.input_tokens,
186449
+ outputTokens: usage.output_tokens,
186450
+ cachedReadTokens: usage.cache_read_input_tokens,
186451
+ cachedWriteTokens: usage.cache_creation_input_tokens
186452
+ });
186371
186453
  }
186372
186454
  async function fetchWithTimeout(url2, init, timeoutMs) {
186373
186455
  const controller = new AbortController;
@@ -186411,6 +186493,7 @@ async function runOpenAiJudge(input2, timeoutMs) {
186411
186493
  content: buildJudgePrompt(input2.tool, input2.result, input2.summaryText, input2.agentFindings)
186412
186494
  }
186413
186495
  ],
186496
+ max_completion_tokens: JUDGE_MAX_OUTPUT_TOKENS,
186414
186497
  temperature: 0
186415
186498
  })
186416
186499
  }, timeoutMs);
@@ -186420,7 +186503,22 @@ async function runOpenAiJudge(input2, timeoutMs) {
186420
186503
  const content = payload.choices?.[0]?.message?.content;
186421
186504
  if (!content)
186422
186505
  throw new Error("OpenAI judge response did not include content.");
186423
- return parseJudgeOutput(content, input2.summaryText);
186506
+ const usage = normalizeUsage2(payload.usage);
186507
+ return {
186508
+ output: parseJudgeOutput(content, input2.summaryText),
186509
+ ...usage ? { usage } : {}
186510
+ };
186511
+ }
186512
+ function normalizeUsage2(usage) {
186513
+ if (!usage)
186514
+ return;
186515
+ return normalizeModelTokenUsage({
186516
+ totalTokens: usage.total_tokens,
186517
+ inputTokens: usage.prompt_tokens,
186518
+ outputTokens: usage.completion_tokens,
186519
+ cachedReadTokens: usage.prompt_tokens_details?.cached_tokens,
186520
+ thoughtTokens: usage.completion_tokens_details?.reasoning_tokens
186521
+ });
186424
186522
  }
186425
186523
  async function fetchWithTimeout2(url2, init, timeoutMs) {
186426
186524
  const controller = new AbortController;
@@ -186462,8 +186560,13 @@ async function runJudge(input2) {
186462
186560
  return { provider, status: "deterministic_fallback", output: fallback };
186463
186561
  }
186464
186562
  try {
186465
- const output2 = provider === "openai" ? await runOpenAiJudge(input2, input2.config.timeoutMs) : await runAnthropicJudge(input2, input2.config.timeoutMs);
186466
- return { provider, status: "completed", output: output2 };
186563
+ const output2 = provider === "openai" ? await runOpenAiJudge(input2, input2.timeoutMs ?? input2.config.timeoutMs) : await runAnthropicJudge(input2, input2.timeoutMs ?? input2.config.timeoutMs);
186564
+ return {
186565
+ provider,
186566
+ status: "completed",
186567
+ output: output2.output,
186568
+ ...output2.usage ? { usage: output2.usage } : {}
186569
+ };
186467
186570
  } catch (error51) {
186468
186571
  return {
186469
186572
  provider,
@@ -186714,9 +186817,9 @@ var PLUGIN_RUNTIME_COMPATIBILITY_SCHEMA_VERSION = 1;
186714
186817
  var MINIMUM_SUPPORTED_CODEX_VERSION = "0.144.0-alpha.4";
186715
186818
  var PLUGIN_RUNTIME_EXPECTED_CONTRACT = {
186716
186819
  distribution: {
186717
- pluginVersion: "0.3.1",
186820
+ pluginVersion: "0.4.0",
186718
186821
  mcpCommand: "npx",
186719
- mcpPackagePin: "@kyo-so/cli@0.9.1"
186822
+ mcpPackagePin: "@kyo-so/cli@0.10.0"
186720
186823
  },
186721
186824
  marketplace: {
186722
186825
  name: "kyoso",
@@ -186744,6 +186847,7 @@ var PLUGIN_RUNTIME_EXPECTED_CONTRACT = {
186744
186847
  "CODEX_API_KEY",
186745
186848
  "CODEX_HOME",
186746
186849
  "CODEX_ACCESS_TOKEN",
186850
+ "OPENROUTER_API_KEY",
186747
186851
  "ANTHROPIC_API_KEY",
186748
186852
  "CLAUDE_CODE_OAUTH_TOKEN"
186749
186853
  ],
@@ -186760,6 +186864,7 @@ var PLUGIN_RUNTIME_EXPECTED_CONTRACT = {
186760
186864
  "CODEX_API_KEY",
186761
186865
  "CODEX_HOME",
186762
186866
  "CODEX_ACCESS_TOKEN",
186867
+ "OPENROUTER_API_KEY",
186763
186868
  "ANTHROPIC_API_KEY",
186764
186869
  "CLAUDE_CODE_OAUTH_TOKEN"
186765
186870
  ],
@@ -186806,6 +186911,7 @@ var PLUGIN_RUNTIME_EXPECTED_CONTRACT = {
186806
186911
  OPENAI_API_KEY: true,
186807
186912
  CODEX_API_KEY: true,
186808
186913
  CODEX_ACCESS_TOKEN: true,
186914
+ OPENROUTER_API_KEY: true,
186809
186915
  ANTHROPIC_API_KEY: true,
186810
186916
  CLAUDE_CODE_OAUTH_TOKEN: true
186811
186917
  },
@@ -186958,7 +187064,7 @@ function parseJson(value) {
186958
187064
  }
186959
187065
  }
186960
187066
  function parsePluginList(value) {
186961
- if (!isRecord5(value))
187067
+ if (!isRecord6(value))
186962
187068
  return;
186963
187069
  const allowedKeys = new Set(PLUGIN_LIST_JSON_SCHEMA.collections);
186964
187070
  if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
@@ -186986,7 +187092,7 @@ function parsePluginEntries(value) {
186986
187092
  return;
186987
187093
  const entries = [];
186988
187094
  for (const item of value) {
186989
- if (!isRecord5(item))
187095
+ if (!isRecord6(item))
186990
187096
  return;
186991
187097
  if (typeof item.pluginId !== "string" || typeof item.installed !== "boolean" || typeof item.enabled !== "boolean") {
186992
187098
  return;
@@ -187012,7 +187118,7 @@ function parseMcpList(value) {
187012
187118
  return;
187013
187119
  const matches = [];
187014
187120
  for (const item of value) {
187015
- if (!isRecord5(item) || typeof item.name !== "string")
187121
+ if (!isRecord6(item) || typeof item.name !== "string")
187016
187122
  return "unknown";
187017
187123
  if (item.name !== "kyoso")
187018
187124
  continue;
@@ -187094,7 +187200,7 @@ function comparePrerelease(left, right) {
187094
187200
  }
187095
187201
  return 0;
187096
187202
  }
187097
- function isRecord5(value) {
187203
+ function isRecord6(value) {
187098
187204
  return typeof value === "object" && value !== null && !Array.isArray(value);
187099
187205
  }
187100
187206
 
@@ -187200,7 +187306,7 @@ function findKyosoPackage(executable) {
187200
187306
  if (existsSync(packagePath)) {
187201
187307
  try {
187202
187308
  const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
187203
- if (isRecord6(parsed) && parsed.name === "@kyo-so/cli" && typeof parsed.version === "string" && parsed.version.trim().length > 0) {
187309
+ if (isRecord7(parsed) && parsed.name === "@kyo-so/cli" && typeof parsed.version === "string" && parsed.version.trim().length > 0) {
187204
187310
  return { directory, version: parsed.version };
187205
187311
  }
187206
187312
  } catch {}
@@ -187286,7 +187392,7 @@ function isWithin(path, parent) {
187286
187392
  const relativePath = relative(resolve5(parent), resolve5(path));
187287
187393
  return relativePath === "" || !relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && relativePath !== ".." && !isAbsolute4(relativePath);
187288
187394
  }
187289
- function isRecord6(value) {
187395
+ function isRecord7(value) {
187290
187396
  return typeof value === "object" && value !== null && !Array.isArray(value);
187291
187397
  }
187292
187398
 
@@ -187324,7 +187430,7 @@ import {
187324
187430
  } from "node:path";
187325
187431
 
187326
187432
  // src/cli/knownSkillDigests.ts
187327
- var CURRENT_SKILL_DIGEST = "sha256:98e715396ee3e1994c29ab49bc489197f242091c8ac0e06e182c0eccd5ab277e";
187433
+ var CURRENT_SKILL_DIGEST = "sha256:570f83f716734f34db00147f1b98bc8cd4e9c0016d3946b352ecef4a5d6b8734";
187328
187434
  var KNOWN_SKILL_DIGESTS_BY_VERSION = {
187329
187435
  "0.8.0": [
187330
187436
  {
@@ -188073,7 +188179,7 @@ async function ensureClaudeMcp(context) {
188073
188179
  const configPath = join6(context.cwd, ".mcp.json");
188074
188180
  const current = await readJsonObject(configPath);
188075
188181
  const mcpServers = recordValue(current.mcpServers);
188076
- if (isRecord7(mcpServers.kyoso)) {
188182
+ if (isRecord8(mcpServers.kyoso)) {
188077
188183
  return {
188078
188184
  kind: "mcp",
188079
188185
  registration: "preserved",
@@ -188313,7 +188419,7 @@ async function readJsonObject(path) {
188313
188419
  if (content.trim().length === 0)
188314
188420
  return {};
188315
188421
  const parsed = JSON.parse(content);
188316
- if (!isRecord7(parsed))
188422
+ if (!isRecord8(parsed))
188317
188423
  throw new Error(`${path} must contain a JSON object`);
188318
188424
  return parsed;
188319
188425
  }
@@ -188323,9 +188429,9 @@ function hasCodexMcpContent(content) {
188323
188429
  function codexMcpStatusFromContent(content) {
188324
188430
  try {
188325
188431
  const parsed = parse5(content);
188326
- if (!isRecord7(parsed))
188432
+ if (!isRecord8(parsed))
188327
188433
  return "unknown";
188328
- if (!isRecord7(parsed.mcp_servers))
188434
+ if (!isRecord8(parsed.mcp_servers))
188329
188435
  return "missing";
188330
188436
  if (!("kyoso" in parsed.mcp_servers))
188331
188437
  return "missing";
@@ -188354,11 +188460,11 @@ function detectCodexMcp(path, cwd, home) {
188354
188460
  if (hasUnprobedProjectIntegrationOverride(parsed, cwd, home)) {
188355
188461
  return { status: "unknown", paths: [path] };
188356
188462
  }
188357
- if (!isRecord7(parsed))
188463
+ if (!isRecord8(parsed))
188358
188464
  return { status: "unknown", paths: [path] };
188359
188465
  if (!("mcp_servers" in parsed))
188360
188466
  return { status: "missing", paths: [] };
188361
- if (!isRecord7(parsed.mcp_servers)) {
188467
+ if (!isRecord8(parsed.mcp_servers)) {
188362
188468
  return { status: "unknown", paths: [path] };
188363
188469
  }
188364
188470
  if (!("kyoso" in parsed.mcp_servers)) {
@@ -188383,18 +188489,18 @@ function detectClaudeMcp(path, cwd, home) {
188383
188489
  }
188384
188490
  }
188385
188491
  function jsonMcpStatuses(value, cwd, home) {
188386
- if (!isRecord7(value))
188492
+ if (!isRecord8(value))
188387
188493
  return ["unknown"];
188388
188494
  const statuses = directMcpStatuses(value);
188389
188495
  if (!("projects" in value))
188390
188496
  return statuses;
188391
- if (!isRecord7(value.projects))
188497
+ if (!isRecord8(value.projects))
188392
188498
  return [...statuses, "unknown"];
188393
188499
  const currentProject = normalizeProjectPath(cwd, home);
188394
188500
  for (const [projectPath, projectConfig] of Object.entries(value.projects)) {
188395
188501
  if (normalizeProjectPath(projectPath, home) !== currentProject)
188396
188502
  continue;
188397
- if (!isRecord7(projectConfig)) {
188503
+ if (!isRecord8(projectConfig)) {
188398
188504
  statuses.push("unknown");
188399
188505
  continue;
188400
188506
  }
@@ -188405,7 +188511,7 @@ function jsonMcpStatuses(value, cwd, home) {
188405
188511
  function directMcpStatuses(value) {
188406
188512
  const statuses = [];
188407
188513
  if ("mcpServers" in value) {
188408
- if (!isRecord7(value.mcpServers)) {
188514
+ if (!isRecord8(value.mcpServers)) {
188409
188515
  statuses.push("unknown");
188410
188516
  } else if ("kyoso" in value.mcpServers) {
188411
188517
  statuses.push(mcpEntryStatus(value.mcpServers.kyoso));
@@ -188416,7 +188522,7 @@ function directMcpStatuses(value) {
188416
188522
  function nestedMcpEntryStatus(value, path) {
188417
188523
  let current = value;
188418
188524
  for (const key of path) {
188419
- if (!isRecord7(current))
188525
+ if (!isRecord8(current))
188420
188526
  return "unknown";
188421
188527
  if (!(key in current))
188422
188528
  return "missing";
@@ -188425,11 +188531,11 @@ function nestedMcpEntryStatus(value, path) {
188425
188531
  return mcpEntryStatus(current);
188426
188532
  }
188427
188533
  function hasUnprobedProjectIntegrationOverride(value, cwd, home) {
188428
- if (!isRecord7(value) || !isRecord7(value.projects))
188534
+ if (!isRecord8(value) || !isRecord8(value.projects))
188429
188535
  return false;
188430
188536
  const currentProject = normalizeProjectPath(cwd, home);
188431
188537
  for (const [projectPath, projectConfig] of Object.entries(value.projects)) {
188432
- if (normalizeProjectPath(projectPath, home) !== currentProject || !isRecord7(projectConfig)) {
188538
+ if (normalizeProjectPath(projectPath, home) !== currentProject || !isRecord8(projectConfig)) {
188433
188539
  continue;
188434
188540
  }
188435
188541
  if ("mcp_servers" in projectConfig || "plugins" in projectConfig) {
@@ -188452,7 +188558,7 @@ function normalizeProjectPath(path, home) {
188452
188558
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
188453
188559
  }
188454
188560
  function mcpEntryStatus(value) {
188455
- if (!isRecord7(value))
188561
+ if (!isRecord8(value))
188456
188562
  return "unknown";
188457
188563
  if (!("enabled" in value))
188458
188564
  return "enabled";
@@ -188486,7 +188592,7 @@ function readTextSync(path) {
188486
188592
  function recordValue(value) {
188487
188593
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
188488
188594
  }
188489
- function isRecord7(value) {
188595
+ function isRecord8(value) {
188490
188596
  return typeof value === "object" && value !== null && !Array.isArray(value);
188491
188597
  }
188492
188598
  function diffForAppend(path, snippet) {
@@ -191486,14 +191592,14 @@ var zGuardCreateElicitationResponseCancel = object({
191486
191592
  });
191487
191593
  // node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js
191488
191594
  var CANCEL_REQUEST_METHOD = "$/cancel_request";
191489
- function isRecord8(value) {
191595
+ function isRecord9(value) {
191490
191596
  return typeof value === "object" && value !== null;
191491
191597
  }
191492
191598
  function isJsonRpcId(value) {
191493
191599
  return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
191494
191600
  }
191495
191601
  function cancelRequestId(params) {
191496
- if (!isRecord8(params) || !isJsonRpcId(params["requestId"])) {
191602
+ if (!isRecord9(params) || !isJsonRpcId(params["requestId"])) {
191497
191603
  return;
191498
191604
  }
191499
191605
  return params["requestId"];
@@ -191840,7 +191946,7 @@ class Connection {
191840
191946
  if (this.abortController.signal.aborted) {
191841
191947
  return;
191842
191948
  }
191843
- if (!isRecord8(message)) {
191949
+ if (!isRecord9(message)) {
191844
191950
  console.error("Invalid message", { message });
191845
191951
  return;
191846
191952
  }
@@ -191933,7 +192039,7 @@ class Connection {
191933
192039
  pendingResponse.cleanup?.();
191934
192040
  if ("result" in response) {
191935
192041
  pendingResponse.resolve(response.result);
191936
- } else if ("error" in response && isRecord8(response.error)) {
192042
+ } else if ("error" in response && isRecord9(response.error)) {
191937
192043
  const { code, message, data } = response.error;
191938
192044
  pendingResponse.reject(new RequestError(code, message, data));
191939
192045
  } else {
@@ -192143,7 +192249,7 @@ function ndJsonStream(output2, input2) {
192143
192249
  if (trimmedLine) {
192144
192250
  try {
192145
192251
  const message = JSON.parse(trimmedLine);
192146
- if (isRecord8(message)) {
192252
+ if (isRecord9(message)) {
192147
192253
  controller.enqueue(message);
192148
192254
  } else {
192149
192255
  console.warn("Skipping JSON line that is not an object:", trimmedLine);
@@ -193141,7 +193247,7 @@ function isSeverity(value) {
193141
193247
  return typeof value === "string" && severities.includes(value);
193142
193248
  }
193143
193249
  function normalizeCisaSecureByDesign(value) {
193144
- if (!isRecord9(value))
193250
+ if (!isRecord10(value))
193145
193251
  return;
193146
193252
  const normalized = {};
193147
193253
  const customerSecurityOutcomes = normalizeGateStatus(value.customerSecurityOutcomes);
@@ -193182,7 +193288,7 @@ function normalizeFindingFiles(value) {
193182
193288
  if (!Array.isArray(value))
193183
193289
  return;
193184
193290
  const files = value.flatMap((item) => {
193185
- if (!isRecord9(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
193291
+ if (!isRecord10(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
193186
193292
  return [];
193187
193293
  }
193188
193294
  const file2 = {
@@ -193201,7 +193307,7 @@ function normalizeFindingFiles(value) {
193201
193307
  function normalizeLineNumber(value) {
193202
193308
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
193203
193309
  }
193204
- function isRecord9(value) {
193310
+ function isRecord10(value) {
193205
193311
  return typeof value === "object" && value !== null && !Array.isArray(value);
193206
193312
  }
193207
193313
 
@@ -193261,6 +193367,20 @@ class SubprocessAcpAgentManager extends BaseAcpAgentManager {
193261
193367
  }
193262
193368
  async function runSubprocessAgent(agent, agentConfig, input2, env) {
193263
193369
  const startedAt = new Date().toISOString();
193370
+ const effectiveTimeoutMs = resolveEffectiveTimeoutMs(input2);
193371
+ if (effectiveTimeoutMs <= 0) {
193372
+ return {
193373
+ agent,
193374
+ role: input2.role,
193375
+ status: "timeout",
193376
+ startedAt,
193377
+ completedAt: startedAt,
193378
+ error: {
193379
+ code: "REVIEW_DEADLINE_EXCEEDED",
193380
+ message: "Review deadline was reached before the agent could start."
193381
+ }
193382
+ };
193383
+ }
193264
193384
  return new Promise((resolveResult) => {
193265
193385
  const child = spawn(agentConfig.command, agentConfig.args, {
193266
193386
  cwd: input2.workspaceDir,
@@ -193289,6 +193409,7 @@ async function runSubprocessAgent(agent, agentConfig, input2, env) {
193289
193409
  const timeout = setTimeout(() => {
193290
193410
  abortController.abort(new Error("Kyoso agent timeout"));
193291
193411
  terminateChild(child);
193412
+ const deadlineReached = input2.deadlineAtEpochMs !== undefined && Date.now() >= input2.deadlineAtEpochMs;
193292
193413
  resolveOnce({
193293
193414
  agent,
193294
193415
  role: input2.role,
@@ -193296,11 +193417,11 @@ async function runSubprocessAgent(agent, agentConfig, input2, env) {
193296
193417
  startedAt,
193297
193418
  completedAt: new Date().toISOString(),
193298
193419
  error: {
193299
- code: "AGENT_TIMEOUT",
193300
- message: `Agent timed out after ${input2.timeoutMs}ms`
193420
+ code: deadlineReached ? "REVIEW_DEADLINE_EXCEEDED" : "AGENT_TIMEOUT",
193421
+ message: deadlineReached ? "Review deadline reached before the agent completed." : `Agent timed out after ${effectiveTimeoutMs}ms`
193301
193422
  }
193302
193423
  });
193303
- }, input2.timeoutMs);
193424
+ }, effectiveTimeoutMs);
193304
193425
  child.stderr.on("data", (chunk) => {
193305
193426
  stderr3 += chunk.toString("utf8");
193306
193427
  });
@@ -193316,19 +193437,48 @@ async function runSubprocessAgent(agent, agentConfig, input2, env) {
193316
193437
  error: failure
193317
193438
  });
193318
193439
  });
193319
- runAcpClientWorkflow(child, input2, abortController.signal, resolveEffortConfigOption(agent, agentConfig.effort)).then(({ rawText, warnings }) => {
193440
+ runAcpClientWorkflow(child, input2, abortController, resolveEffortConfigOption(agent, agentConfig.effort)).then(({ rawText, warnings, usage, outputBytes, stopReason }) => {
193320
193441
  stdout = rawText;
193442
+ const completed = stopReason === "end_turn";
193321
193443
  resolveOnce({
193322
193444
  agent,
193323
193445
  role: input2.role,
193324
- status: "completed",
193446
+ status: completed ? "completed" : "failed",
193325
193447
  rawText,
193326
193448
  normalized: normalizeAgentOutput(agent, input2.role, rawText),
193327
193449
  startedAt,
193328
193450
  completedAt: new Date().toISOString(),
193329
- ...warnings.length > 0 ? { warnings } : {}
193451
+ outputBytes,
193452
+ stopReason,
193453
+ ...usage ? { usage } : {},
193454
+ ...warnings.length > 0 ? { warnings } : {},
193455
+ ...completed ? {} : {
193456
+ error: {
193457
+ code: "AGENT_STOPPED_EARLY",
193458
+ message: `Agent stopped before completing the review: ${stopReason}.`
193459
+ }
193460
+ }
193330
193461
  });
193331
193462
  }).catch((error51) => {
193463
+ const outputLimitError = findOutputLimitError(error51, abortController);
193464
+ if (outputLimitError) {
193465
+ stdout = outputLimitError.rawText;
193466
+ resolveOnce({
193467
+ agent,
193468
+ role: input2.role,
193469
+ status: "failed",
193470
+ rawText: stdout,
193471
+ outputBytes: outputLimitError.outputBytes,
193472
+ stopReason: "cancelled",
193473
+ startedAt,
193474
+ completedAt: new Date().toISOString(),
193475
+ error: {
193476
+ code: "AGENT_OUTPUT_LIMIT",
193477
+ message: `Agent output exceeded ${outputLimitError.maxOutputBytes} bytes and was cancelled.`
193478
+ }
193479
+ });
193480
+ return;
193481
+ }
193332
193482
  if (abortController.signal.aborted)
193333
193483
  return;
193334
193484
  const failureText = [stderr3, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
@@ -193361,7 +193511,7 @@ async function runSubprocessAgent(agent, agentConfig, input2, env) {
193361
193511
  });
193362
193512
  });
193363
193513
  }
193364
- async function runAcpClientWorkflow(child, input2, signal, configOption) {
193514
+ async function runAcpClientWorkflow(child, input2, abortController, configOption) {
193365
193515
  if (!child.stdin || !child.stdout) {
193366
193516
  throw new Error("Agent process did not expose stdio streams.");
193367
193517
  }
@@ -193411,8 +193561,8 @@ async function runAcpClientWorkflow(child, input2, signal, configOption) {
193411
193561
  }).withSession(async (session) => {
193412
193562
  const warnings = [];
193413
193563
  if (configOption) {
193414
- await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: signal }).catch((error51) => {
193415
- if (signal.aborted)
193564
+ await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: abortController.signal }).catch((error51) => {
193565
+ if (abortController.signal.aborted)
193416
193566
  return;
193417
193567
  const sanitizedValue = sanitizeTextForDisplay(configOption.value);
193418
193568
  const detail = sanitizeTextForDisplay(formatAgentErrorDetail(error51));
@@ -193422,14 +193572,75 @@ async function runAcpClientWorkflow(child, input2, signal, configOption) {
193422
193572
  });
193423
193573
  }
193424
193574
  const promptResponse = session.prompt(input2.prompt, {
193425
- cancellationSignal: signal
193575
+ cancellationSignal: abortController.signal
193426
193576
  });
193427
- const text = await session.readText();
193428
- await promptResponse;
193429
- return { rawText: text, warnings };
193577
+ promptResponse.catch(() => {
193578
+ return;
193579
+ });
193580
+ let rawText = "";
193581
+ let outputBytes = 0;
193582
+ for (;; ) {
193583
+ const message = await session.nextUpdate();
193584
+ if (message.kind === "stop") {
193585
+ const usage = normalizeUsage3(message.response.usage);
193586
+ return {
193587
+ rawText,
193588
+ warnings,
193589
+ ...usage ? { usage } : {},
193590
+ outputBytes,
193591
+ stopReason: message.stopReason
193592
+ };
193593
+ }
193594
+ const update = message.update;
193595
+ if (update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "agent_thought_chunk" || update.content.type !== "text") {
193596
+ continue;
193597
+ }
193598
+ const chunkBytes = Buffer.byteLength(update.content.text, "utf8");
193599
+ const nextOutputBytes = outputBytes + chunkBytes;
193600
+ if (input2.maxOutputBytes !== undefined && nextOutputBytes > input2.maxOutputBytes) {
193601
+ await ctx.notify(methods.agent.session.cancel, {
193602
+ sessionId: session.sessionId
193603
+ }).catch(() => {
193604
+ return;
193605
+ });
193606
+ const error51 = new AgentOutputLimitError(rawText, nextOutputBytes, input2.maxOutputBytes);
193607
+ abortController.abort(error51);
193608
+ throw error51;
193609
+ }
193610
+ if (update.sessionUpdate === "agent_message_chunk") {
193611
+ rawText += update.content.text;
193612
+ }
193613
+ outputBytes = nextOutputBytes;
193614
+ }
193430
193615
  });
193431
193616
  });
193432
193617
  }
193618
+
193619
+ class AgentOutputLimitError extends Error {
193620
+ rawText;
193621
+ outputBytes;
193622
+ maxOutputBytes;
193623
+ constructor(rawText, outputBytes, maxOutputBytes) {
193624
+ super(`Agent output exceeded ${maxOutputBytes} bytes.`);
193625
+ this.rawText = rawText;
193626
+ this.outputBytes = outputBytes;
193627
+ this.maxOutputBytes = maxOutputBytes;
193628
+ this.name = "AgentOutputLimitError";
193629
+ }
193630
+ }
193631
+ function findOutputLimitError(error51, abortController) {
193632
+ if (error51 instanceof AgentOutputLimitError)
193633
+ return error51;
193634
+ const reason = abortController.signal.reason;
193635
+ return reason instanceof AgentOutputLimitError ? reason : undefined;
193636
+ }
193637
+ function resolveEffectiveTimeoutMs(input2) {
193638
+ const deadlineRemaining = input2.deadlineAtEpochMs === undefined ? Number.POSITIVE_INFINITY : input2.deadlineAtEpochMs - Date.now();
193639
+ return Math.max(0, Math.min(input2.timeoutMs, deadlineRemaining));
193640
+ }
193641
+ function normalizeUsage3(usage) {
193642
+ return normalizeModelTokenUsage(usage);
193643
+ }
193433
193644
  function resolveEffortConfigOption(agent, effort) {
193434
193645
  if (!effort)
193435
193646
  return;
@@ -207299,7 +207510,7 @@ function clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, o
207299
207510
  return;
207300
207511
  }
207301
207512
  const codex = readPath2(overridden, ["agents", "codex"]);
207302
- if (isRecord10(codex))
207513
+ if (isRecord11(codex))
207303
207514
  delete codex.model;
207304
207515
  }
207305
207516
  function findAssignmentForPath(overrides, path) {
@@ -207344,7 +207555,7 @@ function parseConfigOverrideValue(value, currentValue) {
207344
207555
  function readPath2(target, path) {
207345
207556
  let current = target;
207346
207557
  for (const key of path) {
207347
- if (!isRecord10(current))
207558
+ if (!isRecord11(current))
207348
207559
  return;
207349
207560
  current = current[key];
207350
207561
  }
@@ -207354,7 +207565,7 @@ function writePath2(target, path, value) {
207354
207565
  let current = target;
207355
207566
  for (const key of path.slice(0, -1)) {
207356
207567
  const child = current[key];
207357
- if (!isRecord10(child)) {
207568
+ if (!isRecord11(child)) {
207358
207569
  throw new Error(`Cannot apply --set key ${JSON.stringify(path.join("."))}.`);
207359
207570
  }
207360
207571
  current = child;
@@ -207363,7 +207574,7 @@ function writePath2(target, path, value) {
207363
207574
  if (leaf)
207364
207575
  current[leaf] = value;
207365
207576
  }
207366
- function isRecord10(value) {
207577
+ function isRecord11(value) {
207367
207578
  return typeof value === "object" && value !== null && !Array.isArray(value);
207368
207579
  }
207369
207580
 
@@ -207444,9 +207655,10 @@ ${JSON.stringify(opinion)}
207444
207655
  role: input2.role,
207445
207656
  status: "completed",
207446
207657
  rawText,
207447
- normalized: scenario === "success" ? opinion : undefined,
207658
+ normalized: scenario === "success" || scenario === "unknown_usage" ? opinion : undefined,
207448
207659
  startedAt,
207449
- completedAt: new Date().toISOString()
207660
+ completedAt: new Date().toISOString(),
207661
+ ...scenario === "unknown_usage" ? {} : { usage: fakeUsage() }
207450
207662
  };
207451
207663
  }
207452
207664
  }
@@ -207480,9 +207692,13 @@ function verifierResult(input2, startedAt, scenario) {
207480
207692
  status: "completed",
207481
207693
  rawText,
207482
207694
  startedAt,
207483
- completedAt: new Date().toISOString()
207695
+ completedAt: new Date().toISOString(),
207696
+ usage: fakeUsage()
207484
207697
  };
207485
207698
  }
207699
+ function fakeUsage() {
207700
+ return { totalTokens: 20, inputTokens: 12, outputTokens: 8 };
207701
+ }
207486
207702
  function findingIdsFromPrompt(prompt) {
207487
207703
  return Array.from(prompt.matchAll(/^Finding ID: (.+)$/gm)).map((match) => match[1] ?? "");
207488
207704
  }
@@ -208095,6 +208311,16 @@ async function optionalLstat3(path) {
208095
208311
  }
208096
208312
 
208097
208313
  // src/audit/sanitize.ts
208314
+ var USAGE_METADATA_KEYS = new Set([
208315
+ "tokenUsage",
208316
+ "totalTokens",
208317
+ "inputTokens",
208318
+ "outputTokens",
208319
+ "thoughtTokens",
208320
+ "cachedReadTokens",
208321
+ "cachedWriteTokens",
208322
+ "skipOptionalPhasesWhenTokenUsageUnknown"
208323
+ ]);
208098
208324
  function sanitizeForAudit(value, options = {}) {
208099
208325
  if (typeof value === "string")
208100
208326
  return sanitizeText(value);
@@ -208103,7 +208329,7 @@ function sanitizeForAudit(value, options = {}) {
208103
208329
  if (typeof value === "object" && value !== null) {
208104
208330
  const result = {};
208105
208331
  for (const [key, nested] of Object.entries(value)) {
208106
- if (/raw|content|env|credential|token|secret|password/i.test(key) && !(options.includeRawAgentOutput && key === "rawText")) {
208332
+ if (/raw|content|env|credential|token|secret|password/i.test(key) && !(options.includeRawAgentOutput && key === "rawText") && !isUsageMetadata(key, nested)) {
208107
208333
  continue;
208108
208334
  }
208109
208335
  result[key] = sanitizeForAudit(nested, options);
@@ -208112,6 +208338,17 @@ function sanitizeForAudit(value, options = {}) {
208112
208338
  }
208113
208339
  return value;
208114
208340
  }
208341
+ function isUsageMetadata(key, value) {
208342
+ if (!USAGE_METADATA_KEYS.has(key))
208343
+ return false;
208344
+ if (key === "tokenUsage") {
208345
+ return typeof value === "object" && value !== null && !Array.isArray(value);
208346
+ }
208347
+ if (key === "skipOptionalPhasesWhenTokenUsageUnknown") {
208348
+ return typeof value === "boolean";
208349
+ }
208350
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
208351
+ }
208115
208352
 
208116
208353
  // src/audit/trace.ts
208117
208354
  var AUDIT_WARNING_WRITE_FAILED = "AUDIT_WRITE_FAILED: Audit trace writing failed; no further audit events will be written.";
@@ -208377,6 +208614,8 @@ function renderMarkdownResult(tool, result, options = {}) {
208377
208614
  "",
208378
208615
  `**Decision:** ${result.decision}`,
208379
208616
  `**Mode:** ${tool}`,
208617
+ `**Completion:** ${formatCompletion(result)}`,
208618
+ `**Request fingerprint:** ${shortFingerprint(result.requestFingerprint)}`,
208380
208619
  `**Agents:** ${result.agentOpinions.map((opinion) => `${title(opinion.agent)} ${opinion.status}`).join(", ")}`,
208381
208620
  `**Review mode:** ${formatReviewMode(result)}`,
208382
208621
  ...result.verificationMode ? [
@@ -208388,6 +208627,7 @@ function renderMarkdownResult(tool, result, options = {}) {
208388
208627
  "",
208389
208628
  options.summaryText ?? defaultSummaryText(result)
208390
208629
  ];
208630
+ lines.push(...formatExecutionBudget(result));
208391
208631
  if (result.cisaSecureByDesign) {
208392
208632
  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)} |`);
208393
208633
  }
@@ -208440,8 +208680,37 @@ function renderMarkdownResult(tool, result, options = {}) {
208440
208680
  `);
208441
208681
  }
208442
208682
  function defaultSummaryText(result) {
208683
+ if (result.completion.status === "incomplete") {
208684
+ const reasons = result.completion.reasons.length > 0 ? result.completion.reasons.join(", ") : "unspecified coverage gap";
208685
+ return `Review incomplete (${reasons}). Decision is block because review coverage is incomplete, not because a code finding was established.`;
208686
+ }
208443
208687
  return result.findings.length === 0 ? "No blocking findings were detected from the supplied context." : `${result.findings.length} finding(s) require attention.`;
208444
208688
  }
208689
+ function formatExecutionBudget(result) {
208690
+ const budget = result.executionBudget;
208691
+ const agentOutputs = Object.entries(budget.agentOutputBytes);
208692
+ const outputLines = agentOutputs.length > 0 ? agentOutputs.map(([agent, bytes]) => `- ${title(agent)}: ${bytes} bytes`) : ["- None reported."];
208693
+ const totalTokens = budget.tokenUsage.totals.totalTokens;
208694
+ return [
208695
+ "",
208696
+ "## Execution Budget",
208697
+ "",
208698
+ `- Model calls: ${budget.modelCalls.planned} planned / ${budget.modelCalls.consumed} consumed / ${budget.modelCalls.skipped} skipped`,
208699
+ `- Wall time: ${budget.wallTime.consumedMs}ms consumed / ${budget.wallTime.limitMs}ms limit`,
208700
+ `- Token usage: ${budget.tokenUsage.status} (${budget.tokenUsage.reportedCalls} reported, ${budget.tokenUsage.unknownCalls} unknown${totalTokens === undefined ? "" : `, ${totalTokens} total`})`,
208701
+ "- Agent output:",
208702
+ ...outputLines
208703
+ ];
208704
+ }
208705
+ function formatCompletion(result) {
208706
+ if (result.completion.status === "complete")
208707
+ return "complete";
208708
+ const reasons = result.completion.reasons.join(", ") || "unspecified";
208709
+ return `incomplete (${reasons}; retryable=${String(result.completion.retryable)})`;
208710
+ }
208711
+ function shortFingerprint(value) {
208712
+ return value.length <= 20 ? value : `${value.slice(0, 20)}…`;
208713
+ }
208445
208714
  function title(value) {
208446
208715
  return value.slice(0, 1).toUpperCase() + value.slice(1);
208447
208716
  }
@@ -208741,6 +209010,304 @@ function newTraceId() {
208741
209010
  return `tr_${randomUUID2()}`;
208742
209011
  }
208743
209012
 
209013
+ // src/core/requestFingerprint.ts
209014
+ import { createHash as createHash4 } from "node:crypto";
209015
+ var REVIEW_CONTRACT_VERSION = "2026-07-15-v1";
209016
+ function createRequestFingerprint(input2) {
209017
+ const reviewers = ["codex", "claude"].filter((agent) => input2.config.agents[agent].enabled).map((agent) => ({
209018
+ agent,
209019
+ role: input2.roles[agent] ?? input2.config.agents[agent].role,
209020
+ model: input2.config.agents[agent].model ?? null,
209021
+ provider: agent === "codex" ? input2.config.agents.codex.provider ?? "default" : "default"
209022
+ }));
209023
+ const request = structuredClone(input2.request);
209024
+ if (request.options)
209025
+ delete request.options.includeAgentRawOutputs;
209026
+ const payload = {
209027
+ reviewContractVersion: REVIEW_CONTRACT_VERSION,
209028
+ tool: input2.tool,
209029
+ request,
209030
+ reviewers,
209031
+ verification: input2.config.verification,
209032
+ judge: {
209033
+ ...input2.config.judge,
209034
+ requestedProvider: input2.request.options?.judgeProvider ?? null
209035
+ },
209036
+ executionBudget: input2.budget
209037
+ };
209038
+ return `sha256:${createHash4("sha256").update(canonicalJson(payload), "utf8").digest("hex")}`;
209039
+ }
209040
+ function canonicalJson(value) {
209041
+ return JSON.stringify(canonicalize(value));
209042
+ }
209043
+ function canonicalize(value) {
209044
+ if (Array.isArray(value))
209045
+ return value.map(canonicalize);
209046
+ if (!isRecord12(value))
209047
+ return value;
209048
+ 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)]));
209049
+ }
209050
+ function isRecord12(value) {
209051
+ return typeof value === "object" && value !== null && !Array.isArray(value);
209052
+ }
209053
+
209054
+ // src/core/reviewBudget.ts
209055
+ var REVIEW_BUDGET_KEYS = new Set([
209056
+ "maxModelCalls",
209057
+ "maxTotalWallTimeMs",
209058
+ "maxAgentOutputBytes",
209059
+ "maxFindingsPerAgent",
209060
+ "skipOptionalPhasesWhenTokenUsageUnknown"
209061
+ ]);
209062
+ var MODEL_CALL_KINDS = ["primary", "verifier", "judge"];
209063
+ function resolveReviewBudget(ceiling, requested) {
209064
+ if (requested === undefined)
209065
+ return ceiling;
209066
+ if (!isRecord13(requested)) {
209067
+ throw new KyosoRequestError("options.reviewBudget must be an object.", "REVIEW_BUDGET_INVALID");
209068
+ }
209069
+ for (const [key, value] of Object.entries(requested)) {
209070
+ if (!REVIEW_BUDGET_KEYS.has(key)) {
209071
+ throw new KyosoRequestError(`options.reviewBudget.${key} is not supported.`, "REVIEW_BUDGET_INVALID");
209072
+ }
209073
+ if (key === "skipOptionalPhasesWhenTokenUsageUnknown") {
209074
+ if (typeof value !== "boolean") {
209075
+ throw new KyosoRequestError(`options.reviewBudget.${key} must be a boolean.`, "REVIEW_BUDGET_INVALID");
209076
+ }
209077
+ continue;
209078
+ }
209079
+ if (!isPositiveInteger(value)) {
209080
+ throw new KyosoRequestError(`options.reviewBudget.${key} must be a positive integer.`, "REVIEW_BUDGET_INVALID");
209081
+ }
209082
+ }
209083
+ const numericKeys = [
209084
+ "maxModelCalls",
209085
+ "maxTotalWallTimeMs",
209086
+ "maxAgentOutputBytes",
209087
+ "maxFindingsPerAgent"
209088
+ ];
209089
+ for (const key of numericKeys) {
209090
+ const value = requested[key];
209091
+ if (value === undefined)
209092
+ continue;
209093
+ if (value > ceiling[key]) {
209094
+ throw new KyosoRequestError(`options.reviewBudget.${key} cannot exceed the user-global ceiling.`, "REVIEW_BUDGET_EXCEEDS_CEILING");
209095
+ }
209096
+ }
209097
+ if (ceiling.skipOptionalPhasesWhenTokenUsageUnknown && requested.skipOptionalPhasesWhenTokenUsageUnknown === false) {
209098
+ throw new KyosoRequestError("options.reviewBudget.skipOptionalPhasesWhenTokenUsageUnknown cannot relax the user-global ceiling.", "REVIEW_BUDGET_EXCEEDS_CEILING");
209099
+ }
209100
+ return {
209101
+ maxModelCalls: requested.maxModelCalls ?? ceiling.maxModelCalls,
209102
+ maxTotalWallTimeMs: requested.maxTotalWallTimeMs ?? ceiling.maxTotalWallTimeMs,
209103
+ maxAgentOutputBytes: requested.maxAgentOutputBytes ?? ceiling.maxAgentOutputBytes,
209104
+ maxFindingsPerAgent: requested.maxFindingsPerAgent ?? ceiling.maxFindingsPerAgent,
209105
+ skipOptionalPhasesWhenTokenUsageUnknown: ceiling.skipOptionalPhasesWhenTokenUsageUnknown || requested.skipOptionalPhasesWhenTokenUsageUnknown === true
209106
+ };
209107
+ }
209108
+
209109
+ class ReviewBudgetTracker {
209110
+ budget;
209111
+ startedAtEpochMs;
209112
+ deadlineAtEpochMs;
209113
+ reservations = new Map;
209114
+ skippedCalls = [];
209115
+ incompleteReasons = new Set;
209116
+ nextReservationId = 1;
209117
+ constructor(budget, startedAtEpochMs = Date.now()) {
209118
+ this.budget = budget;
209119
+ this.startedAtEpochMs = startedAtEpochMs;
209120
+ this.deadlineAtEpochMs = startedAtEpochMs + budget.maxTotalWallTimeMs;
209121
+ }
209122
+ remainingWallTimeMs(now = Date.now()) {
209123
+ return Math.max(0, this.deadlineAtEpochMs - now);
209124
+ }
209125
+ hasDeadlineExpired(now = Date.now()) {
209126
+ return this.remainingWallTimeMs(now) === 0;
209127
+ }
209128
+ reserveMany(inputs) {
209129
+ if (this.hasDeadlineExpired())
209130
+ return { failure: { reason: "deadline" } };
209131
+ if (this.usedCapacity() + inputs.length > this.budget.maxModelCalls) {
209132
+ return { failure: { reason: "model_call_budget" } };
209133
+ }
209134
+ const reservations = inputs.map((input2) => {
209135
+ const reservation = {
209136
+ id: this.nextReservationId,
209137
+ kind: input2.kind,
209138
+ ...input2.agent ? { agent: input2.agent } : {},
209139
+ status: "reserved"
209140
+ };
209141
+ this.reservations.set(reservation.id, reservation);
209142
+ this.nextReservationId += 1;
209143
+ return reservation;
209144
+ });
209145
+ return {
209146
+ reservations: reservations.map(({ id, kind, agent }) => ({
209147
+ id,
209148
+ kind,
209149
+ ...agent ? { agent } : {}
209150
+ }))
209151
+ };
209152
+ }
209153
+ reserve(input2) {
209154
+ const result = this.reserveMany([input2]);
209155
+ if ("failure" in result)
209156
+ return result;
209157
+ const reservation = result.reservations[0];
209158
+ if (!reservation) {
209159
+ return { failure: { reason: "model_call_budget" } };
209160
+ }
209161
+ return { reservation };
209162
+ }
209163
+ markStarted(reservation) {
209164
+ const current = this.reservations.get(reservation.id);
209165
+ if (!current || current.status !== "reserved")
209166
+ return;
209167
+ current.status = "started";
209168
+ }
209169
+ hasStarted(reservation) {
209170
+ const current = this.reservations.get(reservation.id);
209171
+ return current?.status === "started" || current?.status === "completed";
209172
+ }
209173
+ complete(reservation, values = {}) {
209174
+ const current = this.reservations.get(reservation.id);
209175
+ if (!current || current.status === "skipped" || current.status === "completed") {
209176
+ return;
209177
+ }
209178
+ current.status = "completed";
209179
+ current.outputBytes = values.outputBytes;
209180
+ current.usage = normalizeModelTokenUsage(values.usage);
209181
+ current.stopReason = values.stopReason;
209182
+ }
209183
+ skip(reservation, reason) {
209184
+ const current = this.reservations.get(reservation.id);
209185
+ if (!current || current.status !== "reserved")
209186
+ return;
209187
+ current.status = "skipped";
209188
+ current.reason = reason;
209189
+ }
209190
+ recordSkipped(input2) {
209191
+ this.skippedCalls.push({
209192
+ kind: input2.kind,
209193
+ ...input2.agent ? { agent: input2.agent } : {},
209194
+ status: "skipped",
209195
+ reason: input2.reason
209196
+ });
209197
+ }
209198
+ markIncomplete(reason) {
209199
+ this.incompleteReasons.add(reason);
209200
+ }
209201
+ isTokenUsageUnknown() {
209202
+ return Array.from(this.reservations.values()).some((reservation) => reservation.status === "completed" && reservation.usage === undefined);
209203
+ }
209204
+ snapshot(now = Date.now()) {
209205
+ const calls = this.modelCalls();
209206
+ const byKind = Object.fromEntries(MODEL_CALL_KINDS.map((kind) => [
209207
+ kind,
209208
+ { planned: 0, consumed: 0, skipped: 0 }
209209
+ ]));
209210
+ let planned = 0;
209211
+ let consumed = 0;
209212
+ let skipped = 0;
209213
+ const agentOutputBytes = {};
209214
+ const usageTotals = {};
209215
+ let reportedCalls = 0;
209216
+ let unknownCalls = 0;
209217
+ for (const reservation of this.reservations.values()) {
209218
+ planned += 1;
209219
+ byKind[reservation.kind].planned += 1;
209220
+ if (reservation.status === "completed") {
209221
+ consumed += 1;
209222
+ byKind[reservation.kind].consumed += 1;
209223
+ if (reservation.agent && reservation.outputBytes !== undefined) {
209224
+ agentOutputBytes[reservation.agent] = (agentOutputBytes[reservation.agent] ?? 0) + reservation.outputBytes;
209225
+ }
209226
+ if (reservation.usage) {
209227
+ reportedCalls += 1;
209228
+ addUsage(usageTotals, reservation.usage);
209229
+ } else {
209230
+ unknownCalls += 1;
209231
+ }
209232
+ }
209233
+ if (reservation.status === "skipped") {
209234
+ skipped += 1;
209235
+ byKind[reservation.kind].skipped += 1;
209236
+ }
209237
+ }
209238
+ for (const call of this.skippedCalls) {
209239
+ skipped += 1;
209240
+ byKind[call.kind].skipped += 1;
209241
+ }
209242
+ const tokenStatus = consumed === 0 || reportedCalls === 0 ? "unknown" : unknownCalls === 0 ? "reported" : "partial";
209243
+ const completionReasons = Array.from(this.incompleteReasons).sort();
209244
+ const consumedMs = Math.max(0, now - this.startedAtEpochMs);
209245
+ return {
209246
+ completion: {
209247
+ status: completionReasons.length > 0 ? "incomplete" : "complete",
209248
+ reasons: completionReasons,
209249
+ retryable: false
209250
+ },
209251
+ executionBudget: {
209252
+ maxModelCalls: this.budget.maxModelCalls,
209253
+ modelCalls: { planned, consumed, skipped, byKind },
209254
+ wallTime: {
209255
+ limitMs: this.budget.maxTotalWallTimeMs,
209256
+ consumedMs,
209257
+ remainingMs: this.remainingWallTimeMs(now)
209258
+ },
209259
+ maxAgentOutputBytes: this.budget.maxAgentOutputBytes,
209260
+ maxFindingsPerAgent: this.budget.maxFindingsPerAgent,
209261
+ skipOptionalPhasesWhenTokenUsageUnknown: this.budget.skipOptionalPhasesWhenTokenUsageUnknown,
209262
+ agentOutputBytes,
209263
+ tokenUsage: {
209264
+ status: tokenStatus,
209265
+ reportedCalls,
209266
+ unknownCalls,
209267
+ totals: usageTotals
209268
+ }
209269
+ },
209270
+ modelCalls: calls
209271
+ };
209272
+ }
209273
+ usedCapacity() {
209274
+ return Array.from(this.reservations.values()).filter((reservation) => reservation.status === "reserved" || reservation.status === "started" || reservation.status === "completed").length;
209275
+ }
209276
+ modelCalls() {
209277
+ const reservations = Array.from(this.reservations.values()).filter((reservation) => reservation.status === "completed" || reservation.status === "skipped").map((reservation) => ({
209278
+ kind: reservation.kind,
209279
+ ...reservation.agent ? { agent: reservation.agent } : {},
209280
+ status: reservation.status === "completed" ? "completed" : "skipped",
209281
+ ...reservation.reason ? { reason: reservation.reason } : {},
209282
+ ...reservation.outputBytes !== undefined ? { outputBytes: reservation.outputBytes } : {},
209283
+ ...reservation.usage ? { usage: reservation.usage } : {},
209284
+ ...reservation.stopReason ? { stopReason: reservation.stopReason } : {}
209285
+ }));
209286
+ return [...reservations, ...this.skippedCalls];
209287
+ }
209288
+ }
209289
+ function addUsage(total, usage) {
209290
+ for (const key of [
209291
+ "totalTokens",
209292
+ "inputTokens",
209293
+ "outputTokens",
209294
+ "thoughtTokens",
209295
+ "cachedReadTokens",
209296
+ "cachedWriteTokens"
209297
+ ]) {
209298
+ const value = usage[key];
209299
+ if (value === undefined)
209300
+ continue;
209301
+ total[key] = (total[key] ?? 0) + value;
209302
+ }
209303
+ }
209304
+ function isPositiveInteger(value) {
209305
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
209306
+ }
209307
+ function isRecord13(value) {
209308
+ return typeof value === "object" && value !== null && !Array.isArray(value);
209309
+ }
209310
+
208744
209311
  // src/core/verification.ts
208745
209312
  var REAL_AGENTS = ["codex", "claude"];
208746
209313
  var VERIFIABLE_SEVERITIES = new Set(["critical", "high"]);
@@ -208782,9 +209349,12 @@ function groupVerificationTargetsByVerifier(targets) {
208782
209349
  findings
208783
209350
  }));
208784
209351
  }
208785
- function markVerificationOverflow(targets) {
209352
+ function markVerificationOverflow(targets, reason) {
208786
209353
  for (const target of targets) {
208787
- target.finding.verification = { status: "not_verified" };
209354
+ target.finding.verification = {
209355
+ status: "not_verified",
209356
+ ...reason ? { note: reason } : {}
209357
+ };
208788
209358
  }
208789
209359
  }
208790
209360
  function parseVerificationVerdicts(rawText) {
@@ -208796,7 +209366,7 @@ function parseVerificationVerdicts(rawText) {
208796
209366
  if (!Array.isArray(parsed.verdicts))
208797
209367
  return;
208798
209368
  return parsed.verdicts.flatMap((item) => {
208799
- if (!isRecord11(item))
209369
+ if (!isRecord14(item))
208800
209370
  return [];
208801
209371
  if (typeof item.findingId !== "string")
208802
209372
  return [];
@@ -208874,15 +209444,23 @@ function verificationNote(reasoning) {
208874
209444
  function isVerdict(value) {
208875
209445
  return value === "confirmed" || value === "refuted" || value === "uncertain";
208876
209446
  }
208877
- function isRecord11(value) {
209447
+ function isRecord14(value) {
208878
209448
  return typeof value === "object" && value !== null && !Array.isArray(value);
208879
209449
  }
208880
209450
 
208881
209451
  // src/core/runReview.ts
209452
+ function requestForRecursionFingerprint(request) {
209453
+ try {
209454
+ return scanAndRedactSecrets(request).redactedRequest;
209455
+ } catch {
209456
+ return { goal: "" };
209457
+ }
209458
+ }
208882
209459
  async function runReview(tool, request, options = {}) {
208883
209460
  const cwd = options.cwd ?? process.cwd();
208884
209461
  const traceId = newTraceId();
208885
- const startedAt = new Date().toISOString();
209462
+ const startedAtEpochMs = Date.now();
209463
+ const startedAt = new Date(startedAtEpochMs).toISOString();
208886
209464
  const auditEnv = { ...process.env, ...options.env };
208887
209465
  const traceWriterFactory = options.traceWriterFactory ?? createTraceWriter;
208888
209466
  let snapshot;
@@ -208891,6 +209469,14 @@ async function runReview(tool, request, options = {}) {
208891
209469
  } catch (error51) {
208892
209470
  if (error51 instanceof KyosoRequestError) {
208893
209471
  const config2 = kyosoConfigSchema.parse(defaultConfig);
209472
+ const budgetTracker = new ReviewBudgetTracker(config2.reviewBudget, startedAtEpochMs);
209473
+ const requestFingerprint = createRequestFingerprint({
209474
+ tool,
209475
+ request: requestForRecursionFingerprint(request),
209476
+ config: config2,
209477
+ roles: resolveAgentRoles(config2),
209478
+ budget: config2.reviewBudget
209479
+ });
208894
209480
  const trace2 = traceWriterFactory({
208895
209481
  enabled: config2.audit.enabled,
208896
209482
  directory: config2.audit.directory,
@@ -208905,6 +209491,12 @@ async function runReview(tool, request, options = {}) {
208905
209491
  tool,
208906
209492
  timestamp: new Date().toISOString()
208907
209493
  });
209494
+ await writeReviewBudgetPlanned({
209495
+ trace: trace2,
209496
+ traceId,
209497
+ budgetTracker,
209498
+ requestFingerprint
209499
+ });
208908
209500
  return await buildPolicyBlockResult({
208909
209501
  tool,
208910
209502
  trace: trace2,
@@ -208912,6 +209504,8 @@ async function runReview(tool, request, options = {}) {
208912
209504
  startedAt,
208913
209505
  networkMode: config2.network.defaultMode,
208914
209506
  warning: error51.message,
209507
+ budgetTracker,
209508
+ requestFingerprint,
208915
209509
  finding: {
208916
209510
  id: "KYOSO-1",
208917
209511
  severity: "critical",
@@ -208977,6 +209571,8 @@ async function runReview(tool, request, options = {}) {
208977
209571
  timestamp: new Date().toISOString()
208978
209572
  });
208979
209573
  validateReviewRequest(tool, request);
209574
+ const reviewBudget = resolveReviewBudget(loaded.config.reviewBudget, request.options?.reviewBudget);
209575
+ const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs);
208980
209576
  assertTrustedWorkspaceRoot(request.workspace?.root, loaded.config.workspace.root, cwd);
208981
209577
  const networkMode = resolveNetworkMode(request.options?.network, loaded.config.network.defaultMode, options.mcpNetworkMode);
208982
209578
  if (networkMode === "unrestricted" && !loaded.config.network.allowUnrestricted) {
@@ -208995,6 +209591,19 @@ async function runReview(tool, request, options = {}) {
208995
209591
  });
208996
209592
  const allowSecretOverride = loaded.config.secrets.allowOverride && request.options?.allowSecretRedaction === true;
208997
209593
  if (secretScan.detected && loaded.config.secrets.blockOnDetectedSecret && !allowSecretOverride) {
209594
+ const requestFingerprint2 = createRequestFingerprint({
209595
+ tool,
209596
+ request: secretScan.redactedRequest,
209597
+ config: loaded.config,
209598
+ roles: resolveAgentRoles(loaded.config),
209599
+ budget: reviewBudget
209600
+ });
209601
+ await writeReviewBudgetPlanned({
209602
+ trace,
209603
+ traceId,
209604
+ budgetTracker,
209605
+ requestFingerprint: requestFingerprint2
209606
+ });
208998
209607
  return await buildSecretBlockResult({
208999
209608
  tool,
209000
209609
  trace,
@@ -209003,7 +209612,9 @@ async function runReview(tool, request, options = {}) {
209003
209612
  configHash: loaded.configHash,
209004
209613
  networkMode,
209005
209614
  secretScan,
209006
- warnings
209615
+ warnings,
209616
+ budgetTracker,
209617
+ requestFingerprint: requestFingerprint2
209007
209618
  });
209008
209619
  }
209009
209620
  const denyPatterns = mergeDenyPatterns(loaded.config.workspace.deny, secretScan.redactedRequest.workspace?.denyRead);
@@ -209016,6 +209627,19 @@ async function runReview(tool, request, options = {}) {
209016
209627
  });
209017
209628
  warnings.push(...built.warnings);
209018
209629
  const agentRoles = resolveAgentRoles(loaded.config);
209630
+ const requestFingerprint = createRequestFingerprint({
209631
+ tool,
209632
+ request: built.request,
209633
+ config: loaded.config,
209634
+ roles: agentRoles,
209635
+ budget: reviewBudget
209636
+ });
209637
+ await writeReviewBudgetPlanned({
209638
+ trace,
209639
+ traceId,
209640
+ budgetTracker,
209641
+ requestFingerprint
209642
+ });
209019
209643
  snapshot = await createSnapshot(traceId, tool, built.request, {
209020
209644
  denyPatterns,
209021
209645
  allowPatterns,
@@ -209038,14 +209662,22 @@ async function runReview(tool, request, options = {}) {
209038
209662
  networkMode,
209039
209663
  manager,
209040
209664
  trace,
209041
- warnings
209665
+ warnings,
209666
+ budgetTracker
209042
209667
  });
209043
209668
  warnings.push(...agentResults.flatMap((result) => (result.warnings ?? []).map((warning) => `Agent ${result.agent} ${warning}`)));
209044
- const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
209045
- const agentsUsed = normalizedAgentResults.map((result) => result.agent);
209046
- const reviewMode = agentsUsed.length === 1 ? "single_agent" : "multi_agent";
209669
+ const normalized = agentResults.map((result) => normalizeAgentRunResult(result, reviewBudget.maxFindingsPerAgent));
209670
+ const normalizedAgentResults = normalized.map((item) => item.result);
209671
+ for (const item of normalized.filter((item2) => item2.findingsCapped)) {
209672
+ budgetTracker.markIncomplete("coverage_incomplete");
209673
+ warnings.push(`Agent ${item.result.agent} findings were capped at ${reviewBudget.maxFindingsPerAgent}; review coverage is incomplete.`);
209674
+ }
209675
+ const enabledAgents = ["codex", "claude"].filter((agent) => loaded.config.agents[agent].enabled);
209676
+ const agentsUsed = normalizedAgentResults.filter((result) => result.status !== "skipped").map((result) => result.agent);
209677
+ const reviewMode = enabledAgents.length === 1 ? "single_agent" : "multi_agent";
209047
209678
  const completed = normalizedAgentResults.filter((result) => result.status === "completed");
209048
- const degraded = completed.length !== agentResults.length;
209679
+ const attempted = normalizedAgentResults.filter((result) => result.status !== "skipped");
209680
+ const degraded = completed.length !== attempted.length || enabledAgents.length === 0;
209049
209681
  let aggregate = aggregateAgentResults(normalizedAgentResults, {
209050
209682
  reviewMode
209051
209683
  });
@@ -209061,7 +209693,12 @@ async function runReview(tool, request, options = {}) {
209061
209693
  ])
209062
209694
  };
209063
209695
  }
209064
- if (completed.length === 0) {
209696
+ if (completed.length === 0 && (attempted.length > 0 || enabledAgents.length === 0)) {
209697
+ const noPrimaryAgents = enabledAgents.length === 0;
209698
+ if (noPrimaryAgents) {
209699
+ budgetTracker.markIncomplete("coverage_incomplete");
209700
+ warnings.push("No primary review agents are enabled; review coverage is incomplete.");
209701
+ }
209065
209702
  aggregate = {
209066
209703
  ...aggregate,
209067
209704
  findings: [
@@ -209070,9 +209707,9 @@ async function runReview(tool, request, options = {}) {
209070
209707
  id: `KYOSO-${aggregate.findings.length + 1}`,
209071
209708
  severity: "critical",
209072
209709
  category: "other",
209073
- title: "All backend agents failed",
209074
- evidence: normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
209075
- recommendation: "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
209710
+ title: noPrimaryAgents ? "No primary review agents enabled" : "All backend agents failed",
209711
+ evidence: noPrimaryAgents ? "Both configured primary reviewers are disabled." : normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
209712
+ recommendation: noPrimaryAgents ? "Enable at least one primary reviewer before running Kyoso." : "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
209076
209713
  sourceAgents: ["kyoso_policy"],
209077
209714
  confidence: "high"
209078
209715
  }
@@ -209085,7 +209722,7 @@ async function runReview(tool, request, options = {}) {
209085
209722
  findingCount: aggregate.findings.length,
209086
209723
  timestamp: new Date().toISOString()
209087
209724
  });
209088
- const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled ? "cross_agent" : undefined;
209725
+ const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled && enabledAgents.length > 1 ? "cross_agent" : undefined;
209089
209726
  if (verificationMode === "cross_agent") {
209090
209727
  warnings.push(...await runFindingVerification({
209091
209728
  tool,
@@ -209096,11 +209733,16 @@ async function runReview(tool, request, options = {}) {
209096
209733
  networkMode,
209097
209734
  manager,
209098
209735
  trace,
209099
- findings: aggregate.findings
209736
+ findings: aggregate.findings,
209737
+ budgetTracker
209100
209738
  }));
209101
209739
  }
209740
+ if (aggregate.findings.some((finding) => finding.verification?.status === "refuted")) {
209741
+ budgetTracker.markIncomplete("disputed_finding");
209742
+ }
209102
209743
  const cisa = tool === "security_review" ? computeCisaGate(aggregate.findings, normalizedAgentResults) : undefined;
209103
- const decision = decide({
209744
+ const budgetBeforeJudge = budgetTracker.snapshot();
209745
+ const decision = budgetBeforeJudge.completion.status === "incomplete" ? "block" : decide({
209104
209746
  tool,
209105
209747
  findings: aggregate.findings,
209106
209748
  cisa,
@@ -209110,6 +209752,9 @@ async function runReview(tool, request, options = {}) {
209110
209752
  const completedAt = new Date().toISOString();
209111
209753
  const resultWithoutMarkdown = {
209112
209754
  decision,
209755
+ completion: budgetBeforeJudge.completion,
209756
+ executionBudget: budgetBeforeJudge.executionBudget,
209757
+ requestFingerprint,
209113
209758
  degraded,
209114
209759
  agentsUsed,
209115
209760
  reviewMode,
@@ -209131,18 +209776,22 @@ async function runReview(tool, request, options = {}) {
209131
209776
  networkMode,
209132
209777
  workspaceMode: "temp_snapshot",
209133
209778
  configHash: loaded.configHash,
209134
- warnings: Array.from(new Set([...warnings, ...trace.warnings]))
209779
+ warnings: Array.from(new Set([...warnings, ...trace.warnings])),
209780
+ modelCalls: budgetBeforeJudge.modelCalls
209135
209781
  }
209136
209782
  };
209137
209783
  const summaryText = defaultSummaryText(resultWithoutMarkdown);
209138
- const judge = await runJudge({
209784
+ const judge = await runBudgetedJudge({
209139
209785
  tool,
209140
209786
  result: resultWithoutMarkdown,
209141
209787
  summaryText,
209142
209788
  agentFindings: buildJudgeAgentFindings(normalizedAgentResults),
209143
209789
  config: loaded.config.judge,
209144
209790
  requestedProvider: request.options?.judgeProvider,
209145
- env: options.env ?? process.env
209791
+ env: options.env ?? process.env,
209792
+ budgetTracker,
209793
+ trace,
209794
+ traceId
209146
209795
  });
209147
209796
  const judgeComments = new Map(judge.output.disagreementComments.map((comment) => [
209148
209797
  comment.topic,
@@ -209153,10 +209802,20 @@ async function runReview(tool, request, options = {}) {
209153
209802
  judgeComment: judgeComments.get(disagreement.topic) ?? disagreement.judgeComment
209154
209803
  }));
209155
209804
  const crossModelAnalysis = buildCrossModelAnalysis(judge, reviewMode);
209805
+ const budgetAfterJudge = budgetTracker.snapshot();
209806
+ const finalDecision = budgetAfterJudge.completion.status === "incomplete" ? "block" : decision;
209156
209807
  const resultAfterJudge = {
209157
209808
  ...resultWithoutMarkdown,
209809
+ decision: finalDecision,
209810
+ completion: budgetAfterJudge.completion,
209811
+ executionBudget: budgetAfterJudge.executionBudget,
209158
209812
  disagreements,
209159
- ...crossModelAnalysis ? { crossModelAnalysis } : {}
209813
+ ...crossModelAnalysis ? { crossModelAnalysis } : {},
209814
+ audit: {
209815
+ ...resultWithoutMarkdown.audit,
209816
+ completedAt: new Date().toISOString(),
209817
+ modelCalls: budgetAfterJudge.modelCalls
209818
+ }
209160
209819
  };
209161
209820
  const judgeEvent = {
209162
209821
  type: "judge_completed",
@@ -209169,10 +209828,16 @@ async function runReview(tool, request, options = {}) {
209169
209828
  judgeEvent.error = judge.error;
209170
209829
  await trace.write(judgeEvent);
209171
209830
  resultAfterJudge.audit.completedAt = new Date().toISOString();
209831
+ await writeReviewBudgetCompleted({
209832
+ trace,
209833
+ traceId,
209834
+ budgetTracker,
209835
+ requestFingerprint
209836
+ });
209172
209837
  await trace.write({
209173
209838
  type: "decision_completed",
209174
209839
  traceId,
209175
- decision,
209840
+ decision: finalDecision,
209176
209841
  timestamp: new Date().toISOString()
209177
209842
  });
209178
209843
  await trace.write({
@@ -209184,7 +209849,7 @@ async function runReview(tool, request, options = {}) {
209184
209849
  tool,
209185
209850
  trace,
209186
209851
  result: resultAfterJudge,
209187
- summaryText: judge.output.summaryText
209852
+ summaryText: resultAfterJudge.completion.status === "incomplete" ? defaultSummaryText(resultAfterJudge) : judge.output.summaryText
209188
209853
  });
209189
209854
  } finally {
209190
209855
  await trace.finalize();
@@ -209195,37 +209860,180 @@ async function runReview(tool, request, options = {}) {
209195
209860
  async function runFindingVerification(input2) {
209196
209861
  const allowDemotionRequested = input2.config.verification.allowDemotion;
209197
209862
  const selection = selectVerificationTargets(input2.findings, input2.config.verification.maxFindings);
209198
- markVerificationOverflow(selection.overflow);
209199
- if (selection.selected.length === 0)
209200
- return [];
209201
209863
  const warnings = [];
209202
- const groups = groupVerificationTargetsByVerifier(selection.selected);
209864
+ if (selection.overflow.length > 0) {
209865
+ markVerificationOverflow(selection.overflow, "verification_max_findings");
209866
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209867
+ }
209868
+ if (selection.selected.length === 0)
209869
+ return warnings;
209870
+ const potentialGroups = groupVerificationTargetsByVerifier(selection.selected);
209203
209871
  await input2.trace.write({
209204
209872
  type: "verification_started",
209205
209873
  traceId: input2.traceId,
209206
209874
  targetCount: selection.selected.length,
209207
209875
  notVerifiedCount: selection.overflow.length,
209208
- verifierCount: groups.length,
209876
+ verifierCount: potentialGroups.length,
209209
209877
  timeoutMs: input2.config.verification.timeoutMs,
209210
209878
  allowDemotionRequested,
209211
209879
  timestamp: new Date().toISOString()
209212
209880
  });
209213
- const agentInputs = groups.map(({ verifier, findings }) => ({
209881
+ if (input2.budgetTracker.budget.skipOptionalPhasesWhenTokenUsageUnknown && input2.budgetTracker.isTokenUsageUnknown()) {
209882
+ markVerificationOverflow(selection.selected, "token_usage_unknown");
209883
+ input2.budgetTracker.markIncomplete("token_usage_unknown");
209884
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209885
+ warnings.push("Finding verification was skipped because primary-agent token usage was not reported.");
209886
+ for (const group of potentialGroups) {
209887
+ input2.budgetTracker.recordSkipped({
209888
+ kind: "verifier",
209889
+ agent: group.verifier,
209890
+ reason: "token_usage_unknown"
209891
+ });
209892
+ await input2.trace.write({
209893
+ type: "model_call_skipped",
209894
+ traceId: input2.traceId,
209895
+ kind: "verifier",
209896
+ agent: group.verifier,
209897
+ reason: "token_usage_unknown",
209898
+ timestamp: new Date().toISOString()
209899
+ });
209900
+ }
209901
+ await input2.trace.write({
209902
+ type: "verification_completed",
209903
+ traceId: input2.traceId,
209904
+ counts: countVerificationStatuses(input2.findings),
209905
+ timestamp: new Date().toISOString()
209906
+ });
209907
+ return warnings;
209908
+ }
209909
+ const groups = new Map;
209910
+ const unavailableVerifiers = new Map;
209911
+ for (const target of selection.selected) {
209912
+ const existing = groups.get(target.verifier);
209913
+ if (existing) {
209914
+ existing.targets.push(target);
209915
+ continue;
209916
+ }
209917
+ const unavailable = unavailableVerifiers.get(target.verifier);
209918
+ if (unavailable) {
209919
+ markVerificationOverflow([target], unavailable === "model_call_budget" ? "budget_exhausted" : "deadline");
209920
+ continue;
209921
+ }
209922
+ const reservationResult = input2.budgetTracker.reserve({
209923
+ kind: "verifier",
209924
+ agent: target.verifier
209925
+ });
209926
+ if ("failure" in reservationResult) {
209927
+ unavailableVerifiers.set(target.verifier, reservationResult.failure.reason);
209928
+ markVerificationOverflow([target], reservationResult.failure.reason === "model_call_budget" ? "budget_exhausted" : "deadline");
209929
+ input2.budgetTracker.recordSkipped({
209930
+ kind: "verifier",
209931
+ agent: target.verifier,
209932
+ reason: reservationResult.failure.reason
209933
+ });
209934
+ input2.budgetTracker.markIncomplete(reservationResult.failure.reason);
209935
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209936
+ await input2.trace.write({
209937
+ type: "review_budget_exhausted",
209938
+ traceId: input2.traceId,
209939
+ phase: "verification",
209940
+ kind: "verifier",
209941
+ agent: target.verifier,
209942
+ reason: reservationResult.failure.reason,
209943
+ timestamp: new Date().toISOString()
209944
+ });
209945
+ await input2.trace.write({
209946
+ type: "model_call_skipped",
209947
+ traceId: input2.traceId,
209948
+ kind: "verifier",
209949
+ agent: target.verifier,
209950
+ reason: reservationResult.failure.reason,
209951
+ timestamp: new Date().toISOString()
209952
+ });
209953
+ continue;
209954
+ }
209955
+ const group = {
209956
+ verifier: target.verifier,
209957
+ targets: [target],
209958
+ reservation: reservationResult.reservation
209959
+ };
209960
+ groups.set(target.verifier, group);
209961
+ await input2.trace.write({
209962
+ type: "model_call_reserved",
209963
+ traceId: input2.traceId,
209964
+ kind: "verifier",
209965
+ agent: target.verifier,
209966
+ timestamp: new Date().toISOString()
209967
+ });
209968
+ }
209969
+ const scheduledGroups = [];
209970
+ for (const group of groups.values()) {
209971
+ const timeoutMs = Math.min(input2.config.verification.timeoutMs, input2.budgetTracker.remainingWallTimeMs());
209972
+ if (timeoutMs > 0) {
209973
+ scheduledGroups.push(group);
209974
+ continue;
209975
+ }
209976
+ input2.budgetTracker.skip(group.reservation, "deadline");
209977
+ input2.budgetTracker.markIncomplete("deadline");
209978
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209979
+ markVerificationOverflow(group.targets, "deadline");
209980
+ await input2.trace.write({
209981
+ type: "review_budget_exhausted",
209982
+ traceId: input2.traceId,
209983
+ phase: "verification",
209984
+ kind: "verifier",
209985
+ agent: group.verifier,
209986
+ reason: "deadline",
209987
+ timestamp: new Date().toISOString()
209988
+ });
209989
+ await input2.trace.write({
209990
+ type: "model_call_skipped",
209991
+ traceId: input2.traceId,
209992
+ kind: "verifier",
209993
+ agent: group.verifier,
209994
+ reason: "deadline",
209995
+ timestamp: new Date().toISOString()
209996
+ });
209997
+ }
209998
+ if (scheduledGroups.length === 0) {
209999
+ await input2.trace.write({
210000
+ type: "verification_completed",
210001
+ traceId: input2.traceId,
210002
+ counts: countVerificationStatuses(input2.findings),
210003
+ timestamp: new Date().toISOString()
210004
+ });
210005
+ return warnings;
210006
+ }
210007
+ const agentInputs = scheduledGroups.map((group) => ({
209214
210008
  traceId: input2.traceId,
209215
- agent: verifier,
210009
+ agent: group.verifier,
209216
210010
  role: "finding_verifier",
209217
210011
  tool: input2.tool,
209218
- prompt: buildFindingVerifierPrompt(input2.tool, input2.request, verifier, findings),
210012
+ prompt: buildFindingVerifierPrompt(input2.tool, input2.request, group.verifier, group.targets.map((target) => target.finding)),
209219
210013
  workspaceDir: input2.workspaceDir,
209220
- timeoutMs: input2.config.verification.timeoutMs,
209221
- networkMode: input2.networkMode
210014
+ timeoutMs: Math.min(input2.config.verification.timeoutMs, input2.budgetTracker.remainingWallTimeMs()),
210015
+ deadlineAtEpochMs: input2.budgetTracker.deadlineAtEpochMs,
210016
+ maxOutputBytes: input2.budgetTracker.budget.maxAgentOutputBytes,
210017
+ networkMode: input2.networkMode,
210018
+ onStarted: () => {
210019
+ input2.budgetTracker.markStarted(group.reservation);
210020
+ return Promise.resolve();
210021
+ }
209222
210022
  }));
209223
210023
  let results;
209224
210024
  try {
209225
210025
  results = await input2.manager.runAll(agentInputs);
209226
210026
  } catch (error51) {
209227
- for (const group of groups) {
209228
- applyVerificationVerdicts(selection.selected, group.verifier, undefined);
210027
+ for (const group of scheduledGroups) {
210028
+ applyVerificationVerdicts(group.targets, group.verifier, undefined);
210029
+ await finalizeModelCallResult({
210030
+ budgetTracker: input2.budgetTracker,
210031
+ reservation: group.reservation,
210032
+ result: failedVerifierResult(group.verifier, "AGENT_MANAGER_FAILED"),
210033
+ trace: input2.trace,
210034
+ traceId: input2.traceId
210035
+ });
210036
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209229
210037
  }
209230
210038
  const message = `Finding verification failed: ${sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51))}`;
209231
210039
  warnings.push(message);
@@ -209238,9 +210046,32 @@ async function runFindingVerification(input2) {
209238
210046
  });
209239
210047
  return warnings;
209240
210048
  }
209241
- for (const result of results) {
210049
+ const resultByAgent = new Map(results.map((result) => [result.agent, result]));
210050
+ for (const group of scheduledGroups) {
210051
+ const result = resultByAgent.get(group.verifier);
210052
+ if (!result) {
210053
+ applyVerificationVerdicts(group.targets, group.verifier, undefined);
210054
+ await finalizeModelCallResult({
210055
+ budgetTracker: input2.budgetTracker,
210056
+ reservation: group.reservation,
210057
+ result: failedVerifierResult(group.verifier, "AGENT_RESULT_MISSING"),
210058
+ trace: input2.trace,
210059
+ traceId: input2.traceId
210060
+ });
210061
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
210062
+ warnings.push(`Finding verification by ${group.verifier} did not return a result.`);
210063
+ continue;
210064
+ }
210065
+ await finalizeModelCallResult({
210066
+ budgetTracker: input2.budgetTracker,
210067
+ reservation: group.reservation,
210068
+ result,
210069
+ trace: input2.trace,
210070
+ traceId: input2.traceId
210071
+ });
209242
210072
  if (result.status !== "completed") {
209243
- applyVerificationVerdicts(selection.selected, result.agent, undefined);
210073
+ applyVerificationVerdicts(group.targets, result.agent, undefined);
210074
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209244
210075
  const message = `Finding verification by ${result.agent} ${result.status}: ${result.error?.message ?? result.error?.code ?? "no detail"}`;
209245
210076
  warnings.push(sanitizeTextForDisplay(message));
209246
210077
  await input2.trace.write({
@@ -209254,8 +210085,9 @@ async function runFindingVerification(input2) {
209254
210085
  continue;
209255
210086
  }
209256
210087
  const verdicts = parseVerificationVerdicts(result.rawText);
209257
- applyVerificationVerdicts(selection.selected, result.agent, verdicts);
210088
+ applyVerificationVerdicts(group.targets, result.agent, verdicts);
209258
210089
  if (!verdicts) {
210090
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
209259
210091
  const message = `Finding verification by ${result.agent} returned malformed verdict JSON.`;
209260
210092
  warnings.push(message);
209261
210093
  await input2.trace.write({
@@ -209275,6 +210107,20 @@ async function runFindingVerification(input2) {
209275
210107
  });
209276
210108
  return warnings;
209277
210109
  }
210110
+ function failedVerifierResult(agent, code) {
210111
+ const timestamp = new Date().toISOString();
210112
+ return {
210113
+ agent,
210114
+ role: "finding_verifier",
210115
+ status: "failed",
210116
+ startedAt: timestamp,
210117
+ completedAt: timestamp,
210118
+ error: {
210119
+ code,
210120
+ message: "The agent manager did not return a verification result."
210121
+ }
210122
+ };
210123
+ }
209278
210124
  function buildJudgeAgentFindings(results) {
209279
210125
  return results.flatMap((result) => {
209280
210126
  if (!result.normalized)
@@ -209311,13 +210157,182 @@ function buildCrossModelAnalysis(judge, reviewMode) {
209311
210157
  provider: judge.provider
209312
210158
  };
209313
210159
  }
210160
+ async function runBudgetedJudge(input2) {
210161
+ const configuredProvider = input2.requestedProvider ?? input2.config.provider;
210162
+ const provider = resolveJudgeProvider(configuredProvider, input2.env);
210163
+ if (input2.config.mode === "deterministic_only" || provider === "deterministic_fallback") {
210164
+ return runJudge(input2);
210165
+ }
210166
+ const fallback = () => runJudge({
210167
+ ...input2,
210168
+ config: { ...input2.config, mode: "deterministic_only" }
210169
+ });
210170
+ if (input2.budgetTracker.snapshot().completion.status === "incomplete") {
210171
+ await recordSkippedJudgeCall(input2, "review_incomplete");
210172
+ return fallback();
210173
+ }
210174
+ if (input2.budgetTracker.budget.skipOptionalPhasesWhenTokenUsageUnknown && input2.budgetTracker.isTokenUsageUnknown()) {
210175
+ await recordSkippedJudgeCall(input2, "token_usage_unknown");
210176
+ return fallback();
210177
+ }
210178
+ const reservationResult = input2.budgetTracker.reserve({ kind: "judge" });
210179
+ if ("failure" in reservationResult) {
210180
+ await recordSkippedJudgeCall(input2, reservationResult.failure.reason);
210181
+ await input2.trace.write({
210182
+ type: "review_budget_exhausted",
210183
+ traceId: input2.traceId,
210184
+ phase: "judge",
210185
+ kind: "judge",
210186
+ reason: reservationResult.failure.reason,
210187
+ timestamp: new Date().toISOString()
210188
+ });
210189
+ return fallback();
210190
+ }
210191
+ const reservation = reservationResult.reservation;
210192
+ await input2.trace.write({
210193
+ type: "model_call_reserved",
210194
+ traceId: input2.traceId,
210195
+ kind: "judge",
210196
+ timestamp: new Date().toISOString()
210197
+ });
210198
+ const timeoutMs = Math.min(input2.config.timeoutMs, input2.budgetTracker.remainingWallTimeMs());
210199
+ if (timeoutMs <= 0) {
210200
+ input2.budgetTracker.skip(reservation, "deadline");
210201
+ await input2.trace.write({
210202
+ type: "review_budget_exhausted",
210203
+ traceId: input2.traceId,
210204
+ phase: "judge",
210205
+ kind: "judge",
210206
+ reason: "deadline",
210207
+ timestamp: new Date().toISOString()
210208
+ });
210209
+ await input2.trace.write({
210210
+ type: "model_call_skipped",
210211
+ traceId: input2.traceId,
210212
+ kind: "judge",
210213
+ reason: "deadline",
210214
+ timestamp: new Date().toISOString()
210215
+ });
210216
+ return fallback();
210217
+ }
210218
+ input2.budgetTracker.markStarted(reservation);
210219
+ const judge = await runJudge({ ...input2, timeoutMs });
210220
+ const usage = normalizeModelTokenUsage(judge.usage);
210221
+ input2.budgetTracker.complete(reservation, {
210222
+ ...usage ? { usage } : {}
210223
+ });
210224
+ await input2.trace.write({
210225
+ type: "model_call_completed",
210226
+ traceId: input2.traceId,
210227
+ kind: "judge",
210228
+ provider: judge.provider,
210229
+ resultStatus: judge.status,
210230
+ ...usage ? { usage } : {},
210231
+ timestamp: new Date().toISOString()
210232
+ });
210233
+ return judge;
210234
+ }
210235
+ async function recordSkippedJudgeCall(input2, reason) {
210236
+ input2.budgetTracker.recordSkipped({ kind: "judge", reason });
210237
+ await input2.trace.write({
210238
+ type: "model_call_skipped",
210239
+ traceId: input2.traceId,
210240
+ kind: "judge",
210241
+ reason,
210242
+ timestamp: new Date().toISOString()
210243
+ });
210244
+ }
209314
210245
  async function runAgents(input2) {
209315
210246
  const agentRoles = resolveAgentRoles(input2.config);
210247
+ const enabledAgents = ["codex", "claude"].filter((agent) => input2.config.agents[agent].enabled);
210248
+ if (enabledAgents.length === 0)
210249
+ return [];
210250
+ const reservationResult = input2.budgetTracker.reserveMany(enabledAgents.map((agent) => ({ kind: "primary", agent })));
210251
+ if ("failure" in reservationResult) {
210252
+ input2.budgetTracker.markIncomplete(reservationResult.failure.reason);
210253
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
210254
+ await input2.trace.write({
210255
+ type: "review_budget_exhausted",
210256
+ traceId: input2.traceId,
210257
+ phase: "primary",
210258
+ reason: reservationResult.failure.reason,
210259
+ requiredCalls: enabledAgents.length,
210260
+ timestamp: new Date().toISOString()
210261
+ });
210262
+ for (const agent of enabledAgents) {
210263
+ input2.budgetTracker.recordSkipped({
210264
+ kind: "primary",
210265
+ agent,
210266
+ reason: reservationResult.failure.reason
210267
+ });
210268
+ await input2.trace.write({
210269
+ type: "model_call_skipped",
210270
+ traceId: input2.traceId,
210271
+ kind: "primary",
210272
+ agent,
210273
+ reason: reservationResult.failure.reason,
210274
+ timestamp: new Date().toISOString()
210275
+ });
210276
+ }
210277
+ return enabledAgents.map((agent) => {
210278
+ const role = agentRoles[agent] ?? input2.config.agents[agent].role;
210279
+ const timestamp = new Date().toISOString();
210280
+ return {
210281
+ agent,
210282
+ role,
210283
+ status: "skipped",
210284
+ startedAt: timestamp,
210285
+ completedAt: timestamp,
210286
+ error: {
210287
+ code: reservationResult.failure.reason === "deadline" ? "REVIEW_DEADLINE_EXCEEDED" : "MODEL_CALL_BUDGET_EXHAUSTED",
210288
+ message: reservationResult.failure.reason === "deadline" ? "Review deadline was reached before primary agents could start." : "The review model-call budget cannot reserve all primary agents."
210289
+ }
210290
+ };
210291
+ });
210292
+ }
210293
+ const reservations = new Map(reservationResult.reservations.map((reservation) => [
210294
+ reservation.agent,
210295
+ reservation
210296
+ ]));
210297
+ for (const reservation of reservationResult.reservations) {
210298
+ await input2.trace.write({
210299
+ type: "model_call_reserved",
210300
+ traceId: input2.traceId,
210301
+ kind: reservation.kind,
210302
+ agent: reservation.agent,
210303
+ timestamp: new Date().toISOString()
210304
+ });
210305
+ }
210306
+ if (input2.budgetTracker.remainingWallTimeMs() <= 0) {
210307
+ input2.budgetTracker.markIncomplete("deadline");
210308
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
210309
+ await input2.trace.write({
210310
+ type: "review_budget_exhausted",
210311
+ traceId: input2.traceId,
210312
+ phase: "primary",
210313
+ reason: "deadline",
210314
+ timestamp: new Date().toISOString()
210315
+ });
210316
+ return await skipReservedPrimaryAgents({
210317
+ trace: input2.trace,
210318
+ traceId: input2.traceId,
210319
+ budgetTracker: input2.budgetTracker,
210320
+ config: input2.config,
210321
+ agents: enabledAgents,
210322
+ agentRoles,
210323
+ reservations,
210324
+ reason: "deadline"
210325
+ });
210326
+ }
209316
210327
  const startedWrites = [];
209317
210328
  let acceptingStartedEvents = true;
209318
- const agentInputs = ["codex", "claude"].filter((agent) => input2.config.agents[agent].enabled).map((agent) => {
210329
+ const agentInputs = enabledAgents.map((agent) => {
209319
210330
  const agentConfig = input2.config.agents[agent];
209320
210331
  const role = agentRoles[agent] ?? agentConfig.role;
210332
+ const reservation = reservations.get(agent);
210333
+ if (!reservation) {
210334
+ throw new Error(`Missing primary budget reservation for ${agent}.`);
210335
+ }
209321
210336
  return {
209322
210337
  traceId: input2.traceId,
209323
210338
  agent,
@@ -209325,9 +210340,12 @@ async function runAgents(input2) {
209325
210340
  tool: input2.tool,
209326
210341
  prompt: buildAgentPrompt(input2.tool, input2.request, agent, role),
209327
210342
  workspaceDir: input2.workspaceDir,
209328
- timeoutMs: input2.request.options?.maxAgentTimeoutMs ?? agentConfig.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
210343
+ timeoutMs: Math.min(input2.request.options?.maxAgentTimeoutMs ?? Number.POSITIVE_INFINITY, agentConfig.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS, input2.budgetTracker.remainingWallTimeMs()),
210344
+ deadlineAtEpochMs: input2.budgetTracker.deadlineAtEpochMs,
210345
+ maxOutputBytes: input2.budgetTracker.budget.maxAgentOutputBytes,
209329
210346
  networkMode: input2.networkMode,
209330
210347
  onStarted: () => {
210348
+ input2.budgetTracker.markStarted(reservation);
209331
210349
  if (!acceptingStartedEvents)
209332
210350
  return Promise.resolve();
209333
210351
  const event = {
@@ -209355,10 +210373,61 @@ async function runAgents(input2) {
209355
210373
  }
209356
210374
  };
209357
210375
  });
209358
- const results = await input2.manager.runAll(agentInputs);
210376
+ let results;
210377
+ try {
210378
+ results = await input2.manager.runAll(agentInputs);
210379
+ } catch (error51) {
210380
+ const detail = sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51));
210381
+ input2.warnings.push(`Primary-agent execution failed: ${detail}`);
210382
+ results = agentInputs.map((agentInput) => ({
210383
+ agent: agentInput.agent,
210384
+ role: agentInput.role,
210385
+ status: "failed",
210386
+ startedAt: new Date().toISOString(),
210387
+ completedAt: new Date().toISOString(),
210388
+ error: {
210389
+ code: "AGENT_MANAGER_FAILED",
210390
+ message: "The agent manager did not return a review result."
210391
+ }
210392
+ }));
210393
+ }
209359
210394
  acceptingStartedEvents = false;
209360
210395
  await Promise.all(startedWrites);
209361
- await Promise.all(results.map((result) => {
210396
+ const resultByAgent = new Map(results.map((result) => [result.agent, result]));
210397
+ const orderedResults = enabledAgents.map((agent) => {
210398
+ const existing = resultByAgent.get(agent);
210399
+ if (existing)
210400
+ return existing;
210401
+ const role = agentRoles[agent] ?? input2.config.agents[agent].role;
210402
+ const timestamp = new Date().toISOString();
210403
+ return {
210404
+ agent,
210405
+ role,
210406
+ status: "failed",
210407
+ startedAt: timestamp,
210408
+ completedAt: timestamp,
210409
+ error: {
210410
+ code: "AGENT_RESULT_MISSING",
210411
+ message: "The agent manager did not return a review result."
210412
+ }
210413
+ };
210414
+ });
210415
+ for (const result of orderedResults) {
210416
+ const reservation = reservations.get(result.agent);
210417
+ if (!reservation)
210418
+ continue;
210419
+ await finalizeModelCallResult({
210420
+ budgetTracker: input2.budgetTracker,
210421
+ reservation,
210422
+ result,
210423
+ trace: input2.trace,
210424
+ traceId: input2.traceId
210425
+ });
210426
+ if (result.status !== "completed") {
210427
+ input2.budgetTracker.markIncomplete("coverage_incomplete");
210428
+ }
210429
+ }
210430
+ await Promise.all(orderedResults.map((result) => {
209362
210431
  const event = {
209363
210432
  type: "agent_completed",
209364
210433
  traceId: input2.traceId,
@@ -209378,8 +210447,92 @@ async function runAgents(input2) {
209378
210447
  }
209379
210448
  return input2.trace.write(event);
209380
210449
  }));
210450
+ return orderedResults;
210451
+ }
210452
+ async function skipReservedPrimaryAgents(input2) {
210453
+ const results = [];
210454
+ for (const agent of input2.agents) {
210455
+ const reservation = input2.reservations.get(agent);
210456
+ if (reservation)
210457
+ input2.budgetTracker.skip(reservation, input2.reason);
210458
+ await input2.trace.write({
210459
+ type: "model_call_skipped",
210460
+ traceId: input2.traceId,
210461
+ kind: "primary",
210462
+ agent,
210463
+ reason: input2.reason,
210464
+ timestamp: new Date().toISOString()
210465
+ });
210466
+ const timestamp = new Date().toISOString();
210467
+ results.push({
210468
+ agent,
210469
+ role: input2.agentRoles[agent] ?? input2.config.agents[agent].role,
210470
+ status: "skipped",
210471
+ startedAt: timestamp,
210472
+ completedAt: timestamp,
210473
+ error: {
210474
+ code: "REVIEW_DEADLINE_EXCEEDED",
210475
+ message: "Review deadline was reached before the agent could start."
210476
+ }
210477
+ });
210478
+ }
209381
210479
  return results;
209382
210480
  }
210481
+ async function finalizeModelCallResult(input2) {
210482
+ const reason = input2.result.error?.code ?? input2.result.status;
210483
+ const hasStarted = input2.budgetTracker.hasStarted(input2.reservation);
210484
+ const canSkip = input2.result.status === "skipped" || isPreflightAgentFailure(input2.result) || !hasStarted && input2.result.error?.code === "REVIEW_DEADLINE_EXCEEDED";
210485
+ if (canSkip && !hasStarted) {
210486
+ input2.budgetTracker.skip(input2.reservation, reason);
210487
+ if (input2.result.error?.code === "REVIEW_DEADLINE_EXCEEDED") {
210488
+ input2.budgetTracker.markIncomplete("deadline");
210489
+ }
210490
+ await input2.trace.write({
210491
+ type: "model_call_skipped",
210492
+ traceId: input2.traceId,
210493
+ kind: input2.reservation.kind,
210494
+ agent: input2.reservation.agent,
210495
+ reason,
210496
+ timestamp: new Date().toISOString()
210497
+ });
210498
+ return;
210499
+ }
210500
+ input2.budgetTracker.markStarted(input2.reservation);
210501
+ const usage = normalizeModelTokenUsage(input2.result.usage);
210502
+ const outputBytes = input2.result.outputBytes ?? (input2.result.rawText ? Buffer.byteLength(input2.result.rawText, "utf8") : undefined);
210503
+ input2.budgetTracker.complete(input2.reservation, {
210504
+ ...outputBytes === undefined ? {} : { outputBytes },
210505
+ ...usage ? { usage } : {},
210506
+ ...input2.result.stopReason ? { stopReason: input2.result.stopReason } : {}
210507
+ });
210508
+ if (input2.result.error?.code === "AGENT_OUTPUT_LIMIT") {
210509
+ input2.budgetTracker.markIncomplete("agent_output_limit");
210510
+ }
210511
+ if (input2.result.error?.code === "REVIEW_DEADLINE_EXCEEDED") {
210512
+ input2.budgetTracker.markIncomplete("deadline");
210513
+ }
210514
+ await input2.trace.write({
210515
+ type: "model_call_completed",
210516
+ traceId: input2.traceId,
210517
+ kind: input2.reservation.kind,
210518
+ agent: input2.reservation.agent,
210519
+ resultStatus: input2.result.status,
210520
+ ...input2.result.error?.code ? { errorCode: input2.result.error.code } : {},
210521
+ ...outputBytes === undefined ? {} : { outputBytes },
210522
+ ...usage ? { usage } : {},
210523
+ ...input2.result.stopReason ? { stopReason: input2.result.stopReason } : {},
210524
+ timestamp: new Date().toISOString()
210525
+ });
210526
+ }
210527
+ function isPreflightAgentFailure(result) {
210528
+ return result.status === "failed" && [
210529
+ "AGENT_CONFIG_INVALID",
210530
+ "OPENROUTER_KEY_MISSING",
210531
+ "AGENT_SPAWN_FAILED",
210532
+ "AGENT_MANAGER_FAILED",
210533
+ "AGENT_RESULT_MISSING"
210534
+ ].includes(result.error?.code ?? "");
210535
+ }
209383
210536
  function resolveAgentRoles(config2) {
209384
210537
  const enabledAgents = ["codex", "claude"].filter((agent) => config2.agents[agent].enabled);
209385
210538
  const singleAgentMode = enabledAgents.length === 1;
@@ -209395,14 +210548,30 @@ function defaultAgentManager(config2, parentEnv) {
209395
210548
  }
209396
210549
  return new SubprocessAcpAgentManager(config2, parentEnv);
209397
210550
  }
209398
- function normalizeAgentRunResult(result) {
209399
- if (result.status === "completed" && result.rawText && !result.normalized) {
209400
- return {
209401
- ...result,
209402
- normalized: normalizeAgentOutput(result.agent, result.role, result.rawText)
209403
- };
209404
- }
209405
- return result;
210551
+ function normalizeAgentRunResult(result, maxFindingsPerAgent) {
210552
+ const normalizedResult = result.status === "completed" && result.rawText && !result.normalized ? {
210553
+ ...result,
210554
+ normalized: normalizeAgentOutput(result.agent, result.role, result.rawText)
210555
+ } : result;
210556
+ const normalized = normalizedResult.normalized;
210557
+ const findings = normalized?.findings;
210558
+ if (!normalized || !findings || findings.length <= maxFindingsPerAgent) {
210559
+ return { result: normalizedResult, findingsCapped: false };
210560
+ }
210561
+ const limitedFindings = findings.map((finding, index) => ({ finding, index })).sort((left, right) => {
210562
+ const severity = compareSeverity(left.finding.severity, right.finding.severity);
210563
+ return severity === 0 ? left.index - right.index : severity;
210564
+ }).slice(0, maxFindingsPerAgent).map(({ finding }) => finding);
210565
+ return {
210566
+ result: {
210567
+ ...normalizedResult,
210568
+ normalized: {
210569
+ ...normalized,
210570
+ findings: limitedFindings
210571
+ }
210572
+ },
210573
+ findingsCapped: true
210574
+ };
209406
210575
  }
209407
210576
  function agentOpinionSummary(result, includeRawText = false) {
209408
210577
  const opinion = {
@@ -209424,8 +210593,12 @@ async function buildSecretBlockResult(input2) {
209424
210593
  });
209425
210594
  const cisa = input2.tool === "security_review" ? computeCisaGate([finding], []) : undefined;
209426
210595
  const completedAt = new Date().toISOString();
210596
+ const budget = input2.budgetTracker.snapshot();
209427
210597
  const resultWithoutMarkdown = {
209428
210598
  decision: "block",
210599
+ completion: budget.completion,
210600
+ executionBudget: budget.executionBudget,
210601
+ requestFingerprint: input2.requestFingerprint,
209429
210602
  degraded: false,
209430
210603
  agentsUsed: [],
209431
210604
  reviewMode: "multi_agent",
@@ -209461,9 +210634,16 @@ async function buildSecretBlockResult(input2) {
209461
210634
  networkMode: input2.networkMode,
209462
210635
  workspaceMode: "temp_snapshot",
209463
210636
  configHash: input2.configHash,
209464
- warnings: input2.warnings
210637
+ warnings: input2.warnings,
210638
+ modelCalls: budget.modelCalls
209465
210639
  }
209466
210640
  };
210641
+ await writeReviewBudgetCompleted({
210642
+ trace: input2.trace,
210643
+ traceId: input2.traceId,
210644
+ budgetTracker: input2.budgetTracker,
210645
+ requestFingerprint: input2.requestFingerprint
210646
+ });
209467
210647
  await input2.trace.write({
209468
210648
  type: "decision_completed",
209469
210649
  traceId: input2.traceId,
@@ -209506,8 +210686,12 @@ function reindexFindings(findings) {
209506
210686
  }
209507
210687
  async function buildPolicyBlockResult(input2) {
209508
210688
  const completedAt = new Date().toISOString();
210689
+ const budget = input2.budgetTracker.snapshot();
209509
210690
  const resultWithoutMarkdown = {
209510
210691
  decision: "block",
210692
+ completion: budget.completion,
210693
+ executionBudget: budget.executionBudget,
210694
+ requestFingerprint: input2.requestFingerprint,
209511
210695
  degraded: false,
209512
210696
  agentsUsed: [],
209513
210697
  reviewMode: "multi_agent",
@@ -209526,9 +210710,16 @@ async function buildPolicyBlockResult(input2) {
209526
210710
  networkMode: input2.networkMode,
209527
210711
  workspaceMode: "temp_snapshot",
209528
210712
  configHash: input2.configHash,
209529
- warnings: [input2.warning]
210713
+ warnings: [input2.warning],
210714
+ modelCalls: budget.modelCalls
209530
210715
  }
209531
210716
  };
210717
+ await writeReviewBudgetCompleted({
210718
+ trace: input2.trace,
210719
+ traceId: input2.traceId,
210720
+ budgetTracker: input2.budgetTracker,
210721
+ requestFingerprint: input2.requestFingerprint
210722
+ });
209532
210723
  await input2.trace.write({
209533
210724
  type: "decision_completed",
209534
210725
  traceId: input2.traceId,
@@ -209565,6 +210756,33 @@ async function finalizeReviewResult(input2) {
209565
210756
  })
209566
210757
  };
209567
210758
  }
210759
+ async function writeReviewBudgetPlanned(input2) {
210760
+ const snapshot = input2.budgetTracker.snapshot();
210761
+ await input2.trace.write({
210762
+ type: "review_budget_planned",
210763
+ traceId: input2.traceId,
210764
+ requestFingerprint: input2.requestFingerprint,
210765
+ maxModelCalls: snapshot.executionBudget.maxModelCalls,
210766
+ maxTotalWallTimeMs: snapshot.executionBudget.wallTime.limitMs,
210767
+ maxAgentOutputBytes: snapshot.executionBudget.maxAgentOutputBytes,
210768
+ maxFindingsPerAgent: snapshot.executionBudget.maxFindingsPerAgent,
210769
+ skipOptionalPhasesWhenTokenUsageUnknown: snapshot.executionBudget.skipOptionalPhasesWhenTokenUsageUnknown,
210770
+ timestamp: new Date().toISOString()
210771
+ });
210772
+ }
210773
+ async function writeReviewBudgetCompleted(input2) {
210774
+ const snapshot = input2.budgetTracker.snapshot();
210775
+ await input2.trace.write({
210776
+ type: "review_budget_completed",
210777
+ traceId: input2.traceId,
210778
+ requestFingerprint: input2.requestFingerprint,
210779
+ completion: snapshot.completion,
210780
+ modelCalls: snapshot.executionBudget.modelCalls,
210781
+ wallTime: snapshot.executionBudget.wallTime,
210782
+ tokenUsage: snapshot.executionBudget.tokenUsage,
210783
+ timestamp: new Date().toISOString()
210784
+ });
210785
+ }
209568
210786
  function mergeDenyPatterns(configDeny, requestDeny) {
209569
210787
  return Array.from(new Set([...configDeny, ...requestDeny ?? []]));
209570
210788
  }
@@ -209617,6 +210835,13 @@ var kyosoReviewRequestSchema = object({
209617
210835
  options: object({
209618
210836
  network: _enum2(["model_only", "unrestricted"]).optional(),
209619
210837
  maxAgentTimeoutMs: number2().int().positive().optional(),
210838
+ reviewBudget: object({
210839
+ maxModelCalls: number2().int().positive().optional(),
210840
+ maxTotalWallTimeMs: number2().int().positive().optional(),
210841
+ maxAgentOutputBytes: number2().int().positive().optional(),
210842
+ maxFindingsPerAgent: number2().int().positive().optional(),
210843
+ skipOptionalPhasesWhenTokenUsageUnknown: boolean2().optional()
210844
+ }).strict().optional(),
209620
210845
  includeAgentRawOutputs: boolean2().optional(),
209621
210846
  judgeProvider: _enum2(["auto", "openai", "anthropic", "none"]).optional(),
209622
210847
  allowSecretRedaction: boolean2().optional()