@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/index.js CHANGED
@@ -183779,6 +183779,14 @@ function date4(params) {
183779
183779
 
183780
183780
  // node_modules/zod/v4/classic/external.js
183781
183781
  config(en_default());
183782
+ // src/core/constants.ts
183783
+ var DEFAULT_AGENT_TIMEOUT_MS = 120000;
183784
+ var MAX_AGENT_OUTPUT_BYTES = 1048576;
183785
+ var JUDGE_MAX_OUTPUT_TOKENS = 4096;
183786
+ var RAW_OUTPUT_MAX_CHARS = 16384;
183787
+ var TRACE_DIR = ".kyoso/traces";
183788
+ var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
183789
+
183782
183790
  // src/config/schema.ts
183783
183791
  var CODEX_OPENROUTER_PROVIDER = "openrouter";
183784
183792
  var CODEX_DEFAULT_PROVIDER = "default";
@@ -183823,6 +183831,13 @@ var codexAgentSchema = baseAgentSchema.extend({
183823
183831
  }
183824
183832
  });
183825
183833
  });
183834
+ var reviewBudgetSchema = exports_external.object({
183835
+ maxModelCalls: exports_external.number().int().positive(),
183836
+ maxTotalWallTimeMs: exports_external.number().int().positive(),
183837
+ maxAgentOutputBytes: exports_external.number().int().positive().max(MAX_AGENT_OUTPUT_BYTES),
183838
+ maxFindingsPerAgent: exports_external.number().int().positive(),
183839
+ skipOptionalPhasesWhenTokenUsageUnknown: exports_external.boolean()
183840
+ });
183826
183841
  var kyosoConfigSchema = exports_external.object({
183827
183842
  entrypoints: exports_external.object({
183828
183843
  mcp: exports_external.boolean(),
@@ -183880,6 +183895,7 @@ var kyosoConfigSchema = exports_external.object({
183880
183895
  timeoutMs: exports_external.number().int().positive().default(90000),
183881
183896
  allowDemotion: exports_external.boolean().default(false)
183882
183897
  }),
183898
+ reviewBudget: reviewBudgetSchema,
183883
183899
  audit: exports_external.object({
183884
183900
  enabled: exports_external.boolean(),
183885
183901
  format: exports_external.literal("jsonl"),
@@ -183887,6 +183903,15 @@ var kyosoConfigSchema = exports_external.object({
183887
183903
  includeRawAgentOutput: exports_external.boolean(),
183888
183904
  includeFileContents: exports_external.boolean()
183889
183905
  })
183906
+ }).superRefine((config2, context) => {
183907
+ const enabledPrimaryReviewers = Object.values(config2.agents).filter((agent) => agent.enabled).length;
183908
+ if (config2.reviewBudget.maxModelCalls >= enabledPrimaryReviewers)
183909
+ return;
183910
+ context.addIssue({
183911
+ code: exports_external.ZodIssueCode.custom,
183912
+ path: ["reviewBudget", "maxModelCalls"],
183913
+ message: "must be greater than or equal to the number of enabled primary reviewers."
183914
+ });
183890
183915
  });
183891
183916
  function agentConfigLeafPaths(agent) {
183892
183917
  const paths = [
@@ -183945,6 +183970,11 @@ var kyosoConfigKnownLeafPaths = [
183945
183970
  "verification.maxFindings",
183946
183971
  "verification.timeoutMs",
183947
183972
  "verification.allowDemotion",
183973
+ "reviewBudget.maxModelCalls",
183974
+ "reviewBudget.maxTotalWallTimeMs",
183975
+ "reviewBudget.maxAgentOutputBytes",
183976
+ "reviewBudget.maxFindingsPerAgent",
183977
+ "reviewBudget.skipOptionalPhasesWhenTokenUsageUnknown",
183948
183978
  "audit.enabled",
183949
183979
  "audit.format",
183950
183980
  "audit.directory",
@@ -183964,6 +183994,7 @@ var kyosoConfigSecuritySensitivePrefixes = [
183964
183994
  "secrets",
183965
183995
  "securityReview",
183966
183996
  "verification",
183997
+ "reviewBudget",
183967
183998
  "workspace"
183968
183999
  ];
183969
184000
 
@@ -184077,7 +184108,7 @@ var defaultConfig = {
184077
184108
  }
184078
184109
  },
184079
184110
  judge: {
184080
- mode: "deterministic_plus_llm",
184111
+ mode: "deterministic_only",
184081
184112
  provider: "auto",
184082
184113
  timeoutMs: 60000
184083
184114
  },
@@ -184087,6 +184118,13 @@ var defaultConfig = {
184087
184118
  timeoutMs: 90000,
184088
184119
  allowDemotion: false
184089
184120
  },
184121
+ reviewBudget: {
184122
+ maxModelCalls: 4,
184123
+ maxTotalWallTimeMs: 480000,
184124
+ maxAgentOutputBytes: 65536,
184125
+ maxFindingsPerAgent: 10,
184126
+ skipOptionalPhasesWhenTokenUsageUnknown: true
184127
+ },
184090
184128
  audit: {
184091
184129
  enabled: true,
184092
184130
  format: "jsonl",
@@ -184151,7 +184189,7 @@ function collectProjectScopeViolations(config2) {
184151
184189
  const violations = [];
184152
184190
  for (const leaf of leaves) {
184153
184191
  const path = leaf.path.join(".");
184154
- const globalOnlyReason = PROJECT_GLOBAL_ONLY_REASONS[path];
184192
+ const globalOnlyReason = projectGlobalOnlyReason(leaf.path);
184155
184193
  if (globalOnlyReason) {
184156
184194
  violations.push({ path, reason: globalOnlyReason });
184157
184195
  continue;
@@ -184166,6 +184204,15 @@ function collectProjectScopeViolations(config2) {
184166
184204
  }
184167
184205
  return violations.sort((left, right) => left.path.localeCompare(right.path));
184168
184206
  }
184207
+ function projectGlobalOnlyReason(path) {
184208
+ const exactReason = PROJECT_GLOBAL_ONLY_REASONS[path.join(".")];
184209
+ if (exactReason)
184210
+ return exactReason;
184211
+ if (path[0] === "reviewBudget") {
184212
+ return "must be a user-global review budget ceiling";
184213
+ }
184214
+ return;
184215
+ }
184169
184216
  function isAllowedProjectPath(path) {
184170
184217
  const [top, second, third, fourth] = path;
184171
184218
  if (isAllowedConfigOverridePath(path))
@@ -184296,12 +184343,6 @@ function isRecord(value) {
184296
184343
  // src/security/redact.ts
184297
184344
  var REDACTION = "[KYOSO_REDACTED]";
184298
184345
 
184299
- // src/core/constants.ts
184300
- var DEFAULT_AGENT_TIMEOUT_MS = 120000;
184301
- var RAW_OUTPUT_MAX_CHARS = 16384;
184302
- var TRACE_DIR = ".kyoso/traces";
184303
- var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
184304
-
184305
184346
  // src/security/sanitizeText.ts
184306
184347
  var SENSITIVE_TEXT_PATTERNS = [
184307
184348
  /\bsk-(?:proj-)?[A-Za-z0-9_-]{8,}\b/g,
@@ -189791,6 +189832,33 @@ var legacyClientNotificationMethods = new Set([
189791
189832
  CLIENT_METHODS.elicitation_complete
189792
189833
  ]);
189793
189834
 
189835
+ // src/core/tokenUsage.ts
189836
+ var TOKEN_USAGE_KEYS = [
189837
+ "totalTokens",
189838
+ "inputTokens",
189839
+ "outputTokens",
189840
+ "thoughtTokens",
189841
+ "cachedReadTokens",
189842
+ "cachedWriteTokens"
189843
+ ];
189844
+ function normalizeModelTokenUsage(usage) {
189845
+ if (!isRecord6(usage))
189846
+ return;
189847
+ const normalized = {};
189848
+ for (const key of TOKEN_USAGE_KEYS) {
189849
+ const value = usage[key];
189850
+ if (isTokenCount(value))
189851
+ normalized[key] = value;
189852
+ }
189853
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
189854
+ }
189855
+ function isTokenCount(value) {
189856
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
189857
+ }
189858
+ function isRecord6(value) {
189859
+ return typeof value === "object" && value !== null && !Array.isArray(value);
189860
+ }
189861
+
189794
189862
  // src/utils/env.ts
189795
189863
  import { stderr as stderr2 } from "node:process";
189796
189864
  var MINIMAL_ENV_KEYS = [
@@ -190136,7 +190204,7 @@ function isSeverity(value) {
190136
190204
  return typeof value === "string" && severities.includes(value);
190137
190205
  }
190138
190206
  function normalizeCisaSecureByDesign(value) {
190139
- if (!isRecord6(value))
190207
+ if (!isRecord7(value))
190140
190208
  return;
190141
190209
  const normalized = {};
190142
190210
  const customerSecurityOutcomes = normalizeGateStatus(value.customerSecurityOutcomes);
@@ -190177,7 +190245,7 @@ function normalizeFindingFiles(value) {
190177
190245
  if (!Array.isArray(value))
190178
190246
  return;
190179
190247
  const files = value.flatMap((item) => {
190180
- if (!isRecord6(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
190248
+ if (!isRecord7(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
190181
190249
  return [];
190182
190250
  }
190183
190251
  const file2 = {
@@ -190196,7 +190264,7 @@ function normalizeFindingFiles(value) {
190196
190264
  function normalizeLineNumber(value) {
190197
190265
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
190198
190266
  }
190199
- function isRecord6(value) {
190267
+ function isRecord7(value) {
190200
190268
  return typeof value === "object" && value !== null && !Array.isArray(value);
190201
190269
  }
190202
190270
 
@@ -190256,6 +190324,20 @@ class SubprocessAcpAgentManager extends BaseAcpAgentManager {
190256
190324
  }
190257
190325
  async function runSubprocessAgent(agent, agentConfig, input, env) {
190258
190326
  const startedAt = new Date().toISOString();
190327
+ const effectiveTimeoutMs = resolveEffectiveTimeoutMs(input);
190328
+ if (effectiveTimeoutMs <= 0) {
190329
+ return {
190330
+ agent,
190331
+ role: input.role,
190332
+ status: "timeout",
190333
+ startedAt,
190334
+ completedAt: startedAt,
190335
+ error: {
190336
+ code: "REVIEW_DEADLINE_EXCEEDED",
190337
+ message: "Review deadline was reached before the agent could start."
190338
+ }
190339
+ };
190340
+ }
190259
190341
  return new Promise((resolveResult) => {
190260
190342
  const child = spawn(agentConfig.command, agentConfig.args, {
190261
190343
  cwd: input.workspaceDir,
@@ -190284,6 +190366,7 @@ async function runSubprocessAgent(agent, agentConfig, input, env) {
190284
190366
  const timeout = setTimeout(() => {
190285
190367
  abortController.abort(new Error("Kyoso agent timeout"));
190286
190368
  terminateChild(child);
190369
+ const deadlineReached = input.deadlineAtEpochMs !== undefined && Date.now() >= input.deadlineAtEpochMs;
190287
190370
  resolveOnce({
190288
190371
  agent,
190289
190372
  role: input.role,
@@ -190291,11 +190374,11 @@ async function runSubprocessAgent(agent, agentConfig, input, env) {
190291
190374
  startedAt,
190292
190375
  completedAt: new Date().toISOString(),
190293
190376
  error: {
190294
- code: "AGENT_TIMEOUT",
190295
- message: `Agent timed out after ${input.timeoutMs}ms`
190377
+ code: deadlineReached ? "REVIEW_DEADLINE_EXCEEDED" : "AGENT_TIMEOUT",
190378
+ message: deadlineReached ? "Review deadline reached before the agent completed." : `Agent timed out after ${effectiveTimeoutMs}ms`
190296
190379
  }
190297
190380
  });
190298
- }, input.timeoutMs);
190381
+ }, effectiveTimeoutMs);
190299
190382
  child.stderr.on("data", (chunk) => {
190300
190383
  stderr3 += chunk.toString("utf8");
190301
190384
  });
@@ -190311,19 +190394,48 @@ async function runSubprocessAgent(agent, agentConfig, input, env) {
190311
190394
  error: failure
190312
190395
  });
190313
190396
  });
190314
- runAcpClientWorkflow(child, input, abortController.signal, resolveEffortConfigOption(agent, agentConfig.effort)).then(({ rawText, warnings }) => {
190397
+ runAcpClientWorkflow(child, input, abortController, resolveEffortConfigOption(agent, agentConfig.effort)).then(({ rawText, warnings, usage, outputBytes, stopReason }) => {
190315
190398
  stdout = rawText;
190399
+ const completed = stopReason === "end_turn";
190316
190400
  resolveOnce({
190317
190401
  agent,
190318
190402
  role: input.role,
190319
- status: "completed",
190403
+ status: completed ? "completed" : "failed",
190320
190404
  rawText,
190321
190405
  normalized: normalizeAgentOutput(agent, input.role, rawText),
190322
190406
  startedAt,
190323
190407
  completedAt: new Date().toISOString(),
190324
- ...warnings.length > 0 ? { warnings } : {}
190408
+ outputBytes,
190409
+ stopReason,
190410
+ ...usage ? { usage } : {},
190411
+ ...warnings.length > 0 ? { warnings } : {},
190412
+ ...completed ? {} : {
190413
+ error: {
190414
+ code: "AGENT_STOPPED_EARLY",
190415
+ message: `Agent stopped before completing the review: ${stopReason}.`
190416
+ }
190417
+ }
190325
190418
  });
190326
190419
  }).catch((error51) => {
190420
+ const outputLimitError = findOutputLimitError(error51, abortController);
190421
+ if (outputLimitError) {
190422
+ stdout = outputLimitError.rawText;
190423
+ resolveOnce({
190424
+ agent,
190425
+ role: input.role,
190426
+ status: "failed",
190427
+ rawText: stdout,
190428
+ outputBytes: outputLimitError.outputBytes,
190429
+ stopReason: "cancelled",
190430
+ startedAt,
190431
+ completedAt: new Date().toISOString(),
190432
+ error: {
190433
+ code: "AGENT_OUTPUT_LIMIT",
190434
+ message: `Agent output exceeded ${outputLimitError.maxOutputBytes} bytes and was cancelled.`
190435
+ }
190436
+ });
190437
+ return;
190438
+ }
190327
190439
  if (abortController.signal.aborted)
190328
190440
  return;
190329
190441
  const failureText = [stderr3, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
@@ -190356,7 +190468,7 @@ async function runSubprocessAgent(agent, agentConfig, input, env) {
190356
190468
  });
190357
190469
  });
190358
190470
  }
190359
- async function runAcpClientWorkflow(child, input, signal, configOption) {
190471
+ async function runAcpClientWorkflow(child, input, abortController, configOption) {
190360
190472
  if (!child.stdin || !child.stdout) {
190361
190473
  throw new Error("Agent process did not expose stdio streams.");
190362
190474
  }
@@ -190406,8 +190518,8 @@ async function runAcpClientWorkflow(child, input, signal, configOption) {
190406
190518
  }).withSession(async (session) => {
190407
190519
  const warnings = [];
190408
190520
  if (configOption) {
190409
- await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: signal }).catch((error51) => {
190410
- if (signal.aborted)
190521
+ await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: abortController.signal }).catch((error51) => {
190522
+ if (abortController.signal.aborted)
190411
190523
  return;
190412
190524
  const sanitizedValue = sanitizeTextForDisplay(configOption.value);
190413
190525
  const detail = sanitizeTextForDisplay(formatAgentErrorDetail(error51));
@@ -190417,14 +190529,75 @@ async function runAcpClientWorkflow(child, input, signal, configOption) {
190417
190529
  });
190418
190530
  }
190419
190531
  const promptResponse = session.prompt(input.prompt, {
190420
- cancellationSignal: signal
190532
+ cancellationSignal: abortController.signal
190421
190533
  });
190422
- const text = await session.readText();
190423
- await promptResponse;
190424
- return { rawText: text, warnings };
190534
+ promptResponse.catch(() => {
190535
+ return;
190536
+ });
190537
+ let rawText = "";
190538
+ let outputBytes = 0;
190539
+ for (;; ) {
190540
+ const message = await session.nextUpdate();
190541
+ if (message.kind === "stop") {
190542
+ const usage = normalizeUsage(message.response.usage);
190543
+ return {
190544
+ rawText,
190545
+ warnings,
190546
+ ...usage ? { usage } : {},
190547
+ outputBytes,
190548
+ stopReason: message.stopReason
190549
+ };
190550
+ }
190551
+ const update = message.update;
190552
+ if (update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "agent_thought_chunk" || update.content.type !== "text") {
190553
+ continue;
190554
+ }
190555
+ const chunkBytes = Buffer.byteLength(update.content.text, "utf8");
190556
+ const nextOutputBytes = outputBytes + chunkBytes;
190557
+ if (input.maxOutputBytes !== undefined && nextOutputBytes > input.maxOutputBytes) {
190558
+ await ctx.notify(methods.agent.session.cancel, {
190559
+ sessionId: session.sessionId
190560
+ }).catch(() => {
190561
+ return;
190562
+ });
190563
+ const error51 = new AgentOutputLimitError(rawText, nextOutputBytes, input.maxOutputBytes);
190564
+ abortController.abort(error51);
190565
+ throw error51;
190566
+ }
190567
+ if (update.sessionUpdate === "agent_message_chunk") {
190568
+ rawText += update.content.text;
190569
+ }
190570
+ outputBytes = nextOutputBytes;
190571
+ }
190425
190572
  });
190426
190573
  });
190427
190574
  }
190575
+
190576
+ class AgentOutputLimitError extends Error {
190577
+ rawText;
190578
+ outputBytes;
190579
+ maxOutputBytes;
190580
+ constructor(rawText, outputBytes, maxOutputBytes) {
190581
+ super(`Agent output exceeded ${maxOutputBytes} bytes.`);
190582
+ this.rawText = rawText;
190583
+ this.outputBytes = outputBytes;
190584
+ this.maxOutputBytes = maxOutputBytes;
190585
+ this.name = "AgentOutputLimitError";
190586
+ }
190587
+ }
190588
+ function findOutputLimitError(error51, abortController) {
190589
+ if (error51 instanceof AgentOutputLimitError)
190590
+ return error51;
190591
+ const reason = abortController.signal.reason;
190592
+ return reason instanceof AgentOutputLimitError ? reason : undefined;
190593
+ }
190594
+ function resolveEffectiveTimeoutMs(input) {
190595
+ const deadlineRemaining = input.deadlineAtEpochMs === undefined ? Number.POSITIVE_INFINITY : input.deadlineAtEpochMs - Date.now();
190596
+ return Math.max(0, Math.min(input.timeoutMs, deadlineRemaining));
190597
+ }
190598
+ function normalizeUsage(usage) {
190599
+ return normalizeModelTokenUsage(usage);
190600
+ }
190428
190601
  function resolveEffortConfigOption(agent, effort) {
190429
190602
  if (!effort)
190430
190603
  return;
@@ -190646,9 +190819,10 @@ ${JSON.stringify(opinion)}
190646
190819
  role: input.role,
190647
190820
  status: "completed",
190648
190821
  rawText,
190649
- normalized: scenario === "success" ? opinion : undefined,
190822
+ normalized: scenario === "success" || scenario === "unknown_usage" ? opinion : undefined,
190650
190823
  startedAt,
190651
- completedAt: new Date().toISOString()
190824
+ completedAt: new Date().toISOString(),
190825
+ ...scenario === "unknown_usage" ? {} : { usage: fakeUsage() }
190652
190826
  };
190653
190827
  }
190654
190828
  }
@@ -190682,9 +190856,13 @@ function verifierResult(input, startedAt, scenario) {
190682
190856
  status: "completed",
190683
190857
  rawText,
190684
190858
  startedAt,
190685
- completedAt: new Date().toISOString()
190859
+ completedAt: new Date().toISOString(),
190860
+ usage: fakeUsage()
190686
190861
  };
190687
190862
  }
190863
+ function fakeUsage() {
190864
+ return { totalTokens: 20, inputTokens: 12, outputTokens: 8 };
190865
+ }
190688
190866
  function findingIdsFromPrompt(prompt) {
190689
190867
  return Array.from(prompt.matchAll(/^Finding ID: (.+)$/gm)).map((match) => match[1] ?? "");
190690
190868
  }
@@ -191607,6 +191785,16 @@ async function optionalLstat2(path) {
191607
191785
  }
191608
191786
 
191609
191787
  // src/audit/sanitize.ts
191788
+ var USAGE_METADATA_KEYS = new Set([
191789
+ "tokenUsage",
191790
+ "totalTokens",
191791
+ "inputTokens",
191792
+ "outputTokens",
191793
+ "thoughtTokens",
191794
+ "cachedReadTokens",
191795
+ "cachedWriteTokens",
191796
+ "skipOptionalPhasesWhenTokenUsageUnknown"
191797
+ ]);
191610
191798
  function sanitizeForAudit(value, options = {}) {
191611
191799
  if (typeof value === "string")
191612
191800
  return sanitizeText(value);
@@ -191615,7 +191803,7 @@ function sanitizeForAudit(value, options = {}) {
191615
191803
  if (typeof value === "object" && value !== null) {
191616
191804
  const result = {};
191617
191805
  for (const [key, nested] of Object.entries(value)) {
191618
- if (/raw|content|env|credential|token|secret|password/i.test(key) && !(options.includeRawAgentOutput && key === "rawText")) {
191806
+ if (/raw|content|env|credential|token|secret|password/i.test(key) && !(options.includeRawAgentOutput && key === "rawText") && !isUsageMetadata(key, nested)) {
191619
191807
  continue;
191620
191808
  }
191621
191809
  result[key] = sanitizeForAudit(nested, options);
@@ -191624,6 +191812,17 @@ function sanitizeForAudit(value, options = {}) {
191624
191812
  }
191625
191813
  return value;
191626
191814
  }
191815
+ function isUsageMetadata(key, value) {
191816
+ if (!USAGE_METADATA_KEYS.has(key))
191817
+ return false;
191818
+ if (key === "tokenUsage") {
191819
+ return typeof value === "object" && value !== null && !Array.isArray(value);
191820
+ }
191821
+ if (key === "skipOptionalPhasesWhenTokenUsageUnknown") {
191822
+ return typeof value === "boolean";
191823
+ }
191824
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
191825
+ }
191627
191826
 
191628
191827
  // src/audit/trace.ts
191629
191828
  var AUDIT_WARNING_WRITE_FAILED = "AUDIT_WRITE_FAILED: Audit trace writing failed; no further audit events will be written.";
@@ -191889,6 +192088,8 @@ function renderMarkdownResult(tool, result, options = {}) {
191889
192088
  "",
191890
192089
  `**Decision:** ${result.decision}`,
191891
192090
  `**Mode:** ${tool}`,
192091
+ `**Completion:** ${formatCompletion(result)}`,
192092
+ `**Request fingerprint:** ${shortFingerprint(result.requestFingerprint)}`,
191892
192093
  `**Agents:** ${result.agentOpinions.map((opinion) => `${title(opinion.agent)} ${opinion.status}`).join(", ")}`,
191893
192094
  `**Review mode:** ${formatReviewMode(result)}`,
191894
192095
  ...result.verificationMode ? [
@@ -191900,6 +192101,7 @@ function renderMarkdownResult(tool, result, options = {}) {
191900
192101
  "",
191901
192102
  options.summaryText ?? defaultSummaryText(result)
191902
192103
  ];
192104
+ lines.push(...formatExecutionBudget(result));
191903
192105
  if (result.cisaSecureByDesign) {
191904
192106
  lines.push("", "## CISA Secure by Design Gate", "", "| Dimension | Status | Notes |", "|---|---|---|", `| Customer Security Outcomes | ${result.cisaSecureByDesign.customerSecurityOutcomes} | ${notes(result.cisaSecureByDesign.notes)} |`, `| Secure by Default | ${result.cisaSecureByDesign.secureByDefault} | ${notes(result.cisaSecureByDesign.notes)} |`, `| Transparency & Accountability | ${result.cisaSecureByDesign.transparencyAndAccountability} | ${notes(result.cisaSecureByDesign.notes)} |`, `| Governance | ${result.cisaSecureByDesign.governance} | ${notes(result.cisaSecureByDesign.notes)} |`);
191905
192107
  }
@@ -191952,8 +192154,37 @@ function renderMarkdownResult(tool, result, options = {}) {
191952
192154
  `);
191953
192155
  }
191954
192156
  function defaultSummaryText(result) {
192157
+ if (result.completion.status === "incomplete") {
192158
+ const reasons = result.completion.reasons.length > 0 ? result.completion.reasons.join(", ") : "unspecified coverage gap";
192159
+ return `Review incomplete (${reasons}). Decision is block because review coverage is incomplete, not because a code finding was established.`;
192160
+ }
191955
192161
  return result.findings.length === 0 ? "No blocking findings were detected from the supplied context." : `${result.findings.length} finding(s) require attention.`;
191956
192162
  }
192163
+ function formatExecutionBudget(result) {
192164
+ const budget = result.executionBudget;
192165
+ const agentOutputs = Object.entries(budget.agentOutputBytes);
192166
+ const outputLines = agentOutputs.length > 0 ? agentOutputs.map(([agent, bytes]) => `- ${title(agent)}: ${bytes} bytes`) : ["- None reported."];
192167
+ const totalTokens = budget.tokenUsage.totals.totalTokens;
192168
+ return [
192169
+ "",
192170
+ "## Execution Budget",
192171
+ "",
192172
+ `- Model calls: ${budget.modelCalls.planned} planned / ${budget.modelCalls.consumed} consumed / ${budget.modelCalls.skipped} skipped`,
192173
+ `- Wall time: ${budget.wallTime.consumedMs}ms consumed / ${budget.wallTime.limitMs}ms limit`,
192174
+ `- Token usage: ${budget.tokenUsage.status} (${budget.tokenUsage.reportedCalls} reported, ${budget.tokenUsage.unknownCalls} unknown${totalTokens === undefined ? "" : `, ${totalTokens} total`})`,
192175
+ "- Agent output:",
192176
+ ...outputLines
192177
+ ];
192178
+ }
192179
+ function formatCompletion(result) {
192180
+ if (result.completion.status === "complete")
192181
+ return "complete";
192182
+ const reasons = result.completion.reasons.join(", ") || "unspecified";
192183
+ return `incomplete (${reasons}; retryable=${String(result.completion.retryable)})`;
192184
+ }
192185
+ function shortFingerprint(value) {
192186
+ return value.length <= 20 ? value : `${value.slice(0, 20)}…`;
192187
+ }
191957
192188
  function title(value) {
191958
192189
  return value.slice(0, 1).toUpperCase() + value.slice(1);
191959
192190
  }
@@ -192042,7 +192273,7 @@ function parseJudgeOutput(text, fallbackSummaryText) {
192042
192273
  const parsed = JSON.parse(json2);
192043
192274
  const summaryText = typeof parsed.summaryText === "string" && parsed.summaryText.trim().length > 0 ? sanitizeText(parsed.summaryText) : fallbackSummaryText;
192044
192275
  const disagreementComments = Array.isArray(parsed.disagreementComments) ? parsed.disagreementComments.flatMap((item) => {
192045
- if (!isRecord7(item) || typeof item.topic !== "string" || typeof item.judgeComment !== "string") {
192276
+ if (!isRecord8(item) || typeof item.topic !== "string" || typeof item.judgeComment !== "string") {
192046
192277
  return [];
192047
192278
  }
192048
192279
  return [
@@ -192058,7 +192289,7 @@ function parseJudgeOutput(text, fallbackSummaryText) {
192058
192289
  return { summaryText, disagreementComments, analysis };
192059
192290
  }
192060
192291
  function parseAnalysis(value) {
192061
- if (!isRecord7(value))
192292
+ if (!isRecord8(value))
192062
192293
  return;
192063
192294
  if (!Array.isArray(value.blindSpots) || !Array.isArray(value.contradictions) || !Array.isArray(value.partialCoverage)) {
192064
192295
  return;
@@ -192066,7 +192297,7 @@ function parseAnalysis(value) {
192066
192297
  return {
192067
192298
  blindSpots: value.blindSpots.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => typeof item === "string" ? [sanitizeAnalysisText(item)] : []),
192068
192299
  contradictions: value.contradictions.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => {
192069
- if (!isRecord7(item) || typeof item.topic !== "string" || typeof item.detail !== "string") {
192300
+ if (!isRecord8(item) || typeof item.topic !== "string" || typeof item.detail !== "string") {
192070
192301
  return [];
192071
192302
  }
192072
192303
  return [
@@ -192077,7 +192308,7 @@ function parseAnalysis(value) {
192077
192308
  ];
192078
192309
  }),
192079
192310
  partialCoverage: value.partialCoverage.slice(0, ANALYSIS_MAX_ITEMS).flatMap((item) => {
192080
- if (!isRecord7(item) || typeof item.note !== "string")
192311
+ if (!isRecord8(item) || typeof item.note !== "string")
192081
192312
  return [];
192082
192313
  const findingId = typeof item.findingId === "string" ? sanitizeAnalysisText(item.findingId) : undefined;
192083
192314
  return [
@@ -192124,7 +192355,7 @@ function extractFirstJsonObject2(text) {
192124
192355
  }
192125
192356
  return;
192126
192357
  }
192127
- function isRecord7(value) {
192358
+ function isRecord8(value) {
192128
192359
  return typeof value === "object" && value !== null && !Array.isArray(value);
192129
192360
  }
192130
192361
 
@@ -192142,7 +192373,7 @@ async function runAnthropicJudge(input, timeoutMs) {
192142
192373
  },
192143
192374
  body: JSON.stringify({
192144
192375
  model: input.env.KYOSO_ANTHROPIC_JUDGE_MODEL ?? "claude-haiku-4-5",
192145
- max_tokens: 4096,
192376
+ max_tokens: JUDGE_MAX_OUTPUT_TOKENS,
192146
192377
  temperature: 0,
192147
192378
  messages: [
192148
192379
  {
@@ -192158,7 +192389,21 @@ async function runAnthropicJudge(input, timeoutMs) {
192158
192389
  const content = payload.content?.find((item) => item.type === "text" && item.text)?.text;
192159
192390
  if (!content)
192160
192391
  throw new Error("Anthropic judge response did not include text content.");
192161
- return parseJudgeOutput(content, input.summaryText);
192392
+ const usage = normalizeUsage2(payload.usage);
192393
+ return {
192394
+ output: parseJudgeOutput(content, input.summaryText),
192395
+ ...usage ? { usage } : {}
192396
+ };
192397
+ }
192398
+ function normalizeUsage2(usage) {
192399
+ if (!usage)
192400
+ return;
192401
+ return normalizeModelTokenUsage({
192402
+ inputTokens: usage.input_tokens,
192403
+ outputTokens: usage.output_tokens,
192404
+ cachedReadTokens: usage.cache_read_input_tokens,
192405
+ cachedWriteTokens: usage.cache_creation_input_tokens
192406
+ });
192162
192407
  }
192163
192408
  async function fetchWithTimeout(url2, init, timeoutMs) {
192164
192409
  const controller = new AbortController;
@@ -192202,6 +192447,7 @@ async function runOpenAiJudge(input, timeoutMs) {
192202
192447
  content: buildJudgePrompt(input.tool, input.result, input.summaryText, input.agentFindings)
192203
192448
  }
192204
192449
  ],
192450
+ max_completion_tokens: JUDGE_MAX_OUTPUT_TOKENS,
192205
192451
  temperature: 0
192206
192452
  })
192207
192453
  }, timeoutMs);
@@ -192211,7 +192457,22 @@ async function runOpenAiJudge(input, timeoutMs) {
192211
192457
  const content = payload.choices?.[0]?.message?.content;
192212
192458
  if (!content)
192213
192459
  throw new Error("OpenAI judge response did not include content.");
192214
- return parseJudgeOutput(content, input.summaryText);
192460
+ const usage = normalizeUsage3(payload.usage);
192461
+ return {
192462
+ output: parseJudgeOutput(content, input.summaryText),
192463
+ ...usage ? { usage } : {}
192464
+ };
192465
+ }
192466
+ function normalizeUsage3(usage) {
192467
+ if (!usage)
192468
+ return;
192469
+ return normalizeModelTokenUsage({
192470
+ totalTokens: usage.total_tokens,
192471
+ inputTokens: usage.prompt_tokens,
192472
+ outputTokens: usage.completion_tokens,
192473
+ cachedReadTokens: usage.prompt_tokens_details?.cached_tokens,
192474
+ thoughtTokens: usage.completion_tokens_details?.reasoning_tokens
192475
+ });
192215
192476
  }
192216
192477
  async function fetchWithTimeout2(url2, init, timeoutMs) {
192217
192478
  const controller = new AbortController;
@@ -192253,8 +192514,13 @@ async function runJudge(input) {
192253
192514
  return { provider, status: "deterministic_fallback", output: fallback };
192254
192515
  }
192255
192516
  try {
192256
- const output = provider === "openai" ? await runOpenAiJudge(input, input.config.timeoutMs) : await runAnthropicJudge(input, input.config.timeoutMs);
192257
- return { provider, status: "completed", output };
192517
+ const output = provider === "openai" ? await runOpenAiJudge(input, input.timeoutMs ?? input.config.timeoutMs) : await runAnthropicJudge(input, input.timeoutMs ?? input.config.timeoutMs);
192518
+ return {
192519
+ provider,
192520
+ status: "completed",
192521
+ output: output.output,
192522
+ ...output.usage ? { usage: output.usage } : {}
192523
+ };
192258
192524
  } catch (error51) {
192259
192525
  return {
192260
192526
  provider,
@@ -192527,6 +192793,304 @@ function newTraceId() {
192527
192793
  return `tr_${randomUUID()}`;
192528
192794
  }
192529
192795
 
192796
+ // src/core/requestFingerprint.ts
192797
+ import { createHash as createHash3 } from "node:crypto";
192798
+ var REVIEW_CONTRACT_VERSION = "2026-07-15-v1";
192799
+ function createRequestFingerprint(input) {
192800
+ const reviewers = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => ({
192801
+ agent,
192802
+ role: input.roles[agent] ?? input.config.agents[agent].role,
192803
+ model: input.config.agents[agent].model ?? null,
192804
+ provider: agent === "codex" ? input.config.agents.codex.provider ?? "default" : "default"
192805
+ }));
192806
+ const request = structuredClone(input.request);
192807
+ if (request.options)
192808
+ delete request.options.includeAgentRawOutputs;
192809
+ const payload = {
192810
+ reviewContractVersion: REVIEW_CONTRACT_VERSION,
192811
+ tool: input.tool,
192812
+ request,
192813
+ reviewers,
192814
+ verification: input.config.verification,
192815
+ judge: {
192816
+ ...input.config.judge,
192817
+ requestedProvider: input.request.options?.judgeProvider ?? null
192818
+ },
192819
+ executionBudget: input.budget
192820
+ };
192821
+ return `sha256:${createHash3("sha256").update(canonicalJson(payload), "utf8").digest("hex")}`;
192822
+ }
192823
+ function canonicalJson(value) {
192824
+ return JSON.stringify(canonicalize(value));
192825
+ }
192826
+ function canonicalize(value) {
192827
+ if (Array.isArray(value))
192828
+ return value.map(canonicalize);
192829
+ if (!isRecord9(value))
192830
+ return value;
192831
+ return Object.fromEntries(Object.entries(value).filter(([, child]) => child !== undefined).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, canonicalize(child)]));
192832
+ }
192833
+ function isRecord9(value) {
192834
+ return typeof value === "object" && value !== null && !Array.isArray(value);
192835
+ }
192836
+
192837
+ // src/core/reviewBudget.ts
192838
+ var REVIEW_BUDGET_KEYS = new Set([
192839
+ "maxModelCalls",
192840
+ "maxTotalWallTimeMs",
192841
+ "maxAgentOutputBytes",
192842
+ "maxFindingsPerAgent",
192843
+ "skipOptionalPhasesWhenTokenUsageUnknown"
192844
+ ]);
192845
+ var MODEL_CALL_KINDS = ["primary", "verifier", "judge"];
192846
+ function resolveReviewBudget(ceiling, requested) {
192847
+ if (requested === undefined)
192848
+ return ceiling;
192849
+ if (!isRecord10(requested)) {
192850
+ throw new KyosoRequestError("options.reviewBudget must be an object.", "REVIEW_BUDGET_INVALID");
192851
+ }
192852
+ for (const [key, value] of Object.entries(requested)) {
192853
+ if (!REVIEW_BUDGET_KEYS.has(key)) {
192854
+ throw new KyosoRequestError(`options.reviewBudget.${key} is not supported.`, "REVIEW_BUDGET_INVALID");
192855
+ }
192856
+ if (key === "skipOptionalPhasesWhenTokenUsageUnknown") {
192857
+ if (typeof value !== "boolean") {
192858
+ throw new KyosoRequestError(`options.reviewBudget.${key} must be a boolean.`, "REVIEW_BUDGET_INVALID");
192859
+ }
192860
+ continue;
192861
+ }
192862
+ if (!isPositiveInteger(value)) {
192863
+ throw new KyosoRequestError(`options.reviewBudget.${key} must be a positive integer.`, "REVIEW_BUDGET_INVALID");
192864
+ }
192865
+ }
192866
+ const numericKeys = [
192867
+ "maxModelCalls",
192868
+ "maxTotalWallTimeMs",
192869
+ "maxAgentOutputBytes",
192870
+ "maxFindingsPerAgent"
192871
+ ];
192872
+ for (const key of numericKeys) {
192873
+ const value = requested[key];
192874
+ if (value === undefined)
192875
+ continue;
192876
+ if (value > ceiling[key]) {
192877
+ throw new KyosoRequestError(`options.reviewBudget.${key} cannot exceed the user-global ceiling.`, "REVIEW_BUDGET_EXCEEDS_CEILING");
192878
+ }
192879
+ }
192880
+ if (ceiling.skipOptionalPhasesWhenTokenUsageUnknown && requested.skipOptionalPhasesWhenTokenUsageUnknown === false) {
192881
+ throw new KyosoRequestError("options.reviewBudget.skipOptionalPhasesWhenTokenUsageUnknown cannot relax the user-global ceiling.", "REVIEW_BUDGET_EXCEEDS_CEILING");
192882
+ }
192883
+ return {
192884
+ maxModelCalls: requested.maxModelCalls ?? ceiling.maxModelCalls,
192885
+ maxTotalWallTimeMs: requested.maxTotalWallTimeMs ?? ceiling.maxTotalWallTimeMs,
192886
+ maxAgentOutputBytes: requested.maxAgentOutputBytes ?? ceiling.maxAgentOutputBytes,
192887
+ maxFindingsPerAgent: requested.maxFindingsPerAgent ?? ceiling.maxFindingsPerAgent,
192888
+ skipOptionalPhasesWhenTokenUsageUnknown: ceiling.skipOptionalPhasesWhenTokenUsageUnknown || requested.skipOptionalPhasesWhenTokenUsageUnknown === true
192889
+ };
192890
+ }
192891
+
192892
+ class ReviewBudgetTracker {
192893
+ budget;
192894
+ startedAtEpochMs;
192895
+ deadlineAtEpochMs;
192896
+ reservations = new Map;
192897
+ skippedCalls = [];
192898
+ incompleteReasons = new Set;
192899
+ nextReservationId = 1;
192900
+ constructor(budget, startedAtEpochMs = Date.now()) {
192901
+ this.budget = budget;
192902
+ this.startedAtEpochMs = startedAtEpochMs;
192903
+ this.deadlineAtEpochMs = startedAtEpochMs + budget.maxTotalWallTimeMs;
192904
+ }
192905
+ remainingWallTimeMs(now = Date.now()) {
192906
+ return Math.max(0, this.deadlineAtEpochMs - now);
192907
+ }
192908
+ hasDeadlineExpired(now = Date.now()) {
192909
+ return this.remainingWallTimeMs(now) === 0;
192910
+ }
192911
+ reserveMany(inputs) {
192912
+ if (this.hasDeadlineExpired())
192913
+ return { failure: { reason: "deadline" } };
192914
+ if (this.usedCapacity() + inputs.length > this.budget.maxModelCalls) {
192915
+ return { failure: { reason: "model_call_budget" } };
192916
+ }
192917
+ const reservations = inputs.map((input) => {
192918
+ const reservation = {
192919
+ id: this.nextReservationId,
192920
+ kind: input.kind,
192921
+ ...input.agent ? { agent: input.agent } : {},
192922
+ status: "reserved"
192923
+ };
192924
+ this.reservations.set(reservation.id, reservation);
192925
+ this.nextReservationId += 1;
192926
+ return reservation;
192927
+ });
192928
+ return {
192929
+ reservations: reservations.map(({ id, kind, agent }) => ({
192930
+ id,
192931
+ kind,
192932
+ ...agent ? { agent } : {}
192933
+ }))
192934
+ };
192935
+ }
192936
+ reserve(input) {
192937
+ const result = this.reserveMany([input]);
192938
+ if ("failure" in result)
192939
+ return result;
192940
+ const reservation = result.reservations[0];
192941
+ if (!reservation) {
192942
+ return { failure: { reason: "model_call_budget" } };
192943
+ }
192944
+ return { reservation };
192945
+ }
192946
+ markStarted(reservation) {
192947
+ const current = this.reservations.get(reservation.id);
192948
+ if (!current || current.status !== "reserved")
192949
+ return;
192950
+ current.status = "started";
192951
+ }
192952
+ hasStarted(reservation) {
192953
+ const current = this.reservations.get(reservation.id);
192954
+ return current?.status === "started" || current?.status === "completed";
192955
+ }
192956
+ complete(reservation, values = {}) {
192957
+ const current = this.reservations.get(reservation.id);
192958
+ if (!current || current.status === "skipped" || current.status === "completed") {
192959
+ return;
192960
+ }
192961
+ current.status = "completed";
192962
+ current.outputBytes = values.outputBytes;
192963
+ current.usage = normalizeModelTokenUsage(values.usage);
192964
+ current.stopReason = values.stopReason;
192965
+ }
192966
+ skip(reservation, reason) {
192967
+ const current = this.reservations.get(reservation.id);
192968
+ if (!current || current.status !== "reserved")
192969
+ return;
192970
+ current.status = "skipped";
192971
+ current.reason = reason;
192972
+ }
192973
+ recordSkipped(input) {
192974
+ this.skippedCalls.push({
192975
+ kind: input.kind,
192976
+ ...input.agent ? { agent: input.agent } : {},
192977
+ status: "skipped",
192978
+ reason: input.reason
192979
+ });
192980
+ }
192981
+ markIncomplete(reason) {
192982
+ this.incompleteReasons.add(reason);
192983
+ }
192984
+ isTokenUsageUnknown() {
192985
+ return Array.from(this.reservations.values()).some((reservation) => reservation.status === "completed" && reservation.usage === undefined);
192986
+ }
192987
+ snapshot(now = Date.now()) {
192988
+ const calls = this.modelCalls();
192989
+ const byKind = Object.fromEntries(MODEL_CALL_KINDS.map((kind) => [
192990
+ kind,
192991
+ { planned: 0, consumed: 0, skipped: 0 }
192992
+ ]));
192993
+ let planned = 0;
192994
+ let consumed = 0;
192995
+ let skipped = 0;
192996
+ const agentOutputBytes = {};
192997
+ const usageTotals = {};
192998
+ let reportedCalls = 0;
192999
+ let unknownCalls = 0;
193000
+ for (const reservation of this.reservations.values()) {
193001
+ planned += 1;
193002
+ byKind[reservation.kind].planned += 1;
193003
+ if (reservation.status === "completed") {
193004
+ consumed += 1;
193005
+ byKind[reservation.kind].consumed += 1;
193006
+ if (reservation.agent && reservation.outputBytes !== undefined) {
193007
+ agentOutputBytes[reservation.agent] = (agentOutputBytes[reservation.agent] ?? 0) + reservation.outputBytes;
193008
+ }
193009
+ if (reservation.usage) {
193010
+ reportedCalls += 1;
193011
+ addUsage(usageTotals, reservation.usage);
193012
+ } else {
193013
+ unknownCalls += 1;
193014
+ }
193015
+ }
193016
+ if (reservation.status === "skipped") {
193017
+ skipped += 1;
193018
+ byKind[reservation.kind].skipped += 1;
193019
+ }
193020
+ }
193021
+ for (const call of this.skippedCalls) {
193022
+ skipped += 1;
193023
+ byKind[call.kind].skipped += 1;
193024
+ }
193025
+ const tokenStatus = consumed === 0 || reportedCalls === 0 ? "unknown" : unknownCalls === 0 ? "reported" : "partial";
193026
+ const completionReasons = Array.from(this.incompleteReasons).sort();
193027
+ const consumedMs = Math.max(0, now - this.startedAtEpochMs);
193028
+ return {
193029
+ completion: {
193030
+ status: completionReasons.length > 0 ? "incomplete" : "complete",
193031
+ reasons: completionReasons,
193032
+ retryable: false
193033
+ },
193034
+ executionBudget: {
193035
+ maxModelCalls: this.budget.maxModelCalls,
193036
+ modelCalls: { planned, consumed, skipped, byKind },
193037
+ wallTime: {
193038
+ limitMs: this.budget.maxTotalWallTimeMs,
193039
+ consumedMs,
193040
+ remainingMs: this.remainingWallTimeMs(now)
193041
+ },
193042
+ maxAgentOutputBytes: this.budget.maxAgentOutputBytes,
193043
+ maxFindingsPerAgent: this.budget.maxFindingsPerAgent,
193044
+ skipOptionalPhasesWhenTokenUsageUnknown: this.budget.skipOptionalPhasesWhenTokenUsageUnknown,
193045
+ agentOutputBytes,
193046
+ tokenUsage: {
193047
+ status: tokenStatus,
193048
+ reportedCalls,
193049
+ unknownCalls,
193050
+ totals: usageTotals
193051
+ }
193052
+ },
193053
+ modelCalls: calls
193054
+ };
193055
+ }
193056
+ usedCapacity() {
193057
+ return Array.from(this.reservations.values()).filter((reservation) => reservation.status === "reserved" || reservation.status === "started" || reservation.status === "completed").length;
193058
+ }
193059
+ modelCalls() {
193060
+ const reservations = Array.from(this.reservations.values()).filter((reservation) => reservation.status === "completed" || reservation.status === "skipped").map((reservation) => ({
193061
+ kind: reservation.kind,
193062
+ ...reservation.agent ? { agent: reservation.agent } : {},
193063
+ status: reservation.status === "completed" ? "completed" : "skipped",
193064
+ ...reservation.reason ? { reason: reservation.reason } : {},
193065
+ ...reservation.outputBytes !== undefined ? { outputBytes: reservation.outputBytes } : {},
193066
+ ...reservation.usage ? { usage: reservation.usage } : {},
193067
+ ...reservation.stopReason ? { stopReason: reservation.stopReason } : {}
193068
+ }));
193069
+ return [...reservations, ...this.skippedCalls];
193070
+ }
193071
+ }
193072
+ function addUsage(total, usage) {
193073
+ for (const key of [
193074
+ "totalTokens",
193075
+ "inputTokens",
193076
+ "outputTokens",
193077
+ "thoughtTokens",
193078
+ "cachedReadTokens",
193079
+ "cachedWriteTokens"
193080
+ ]) {
193081
+ const value = usage[key];
193082
+ if (value === undefined)
193083
+ continue;
193084
+ total[key] = (total[key] ?? 0) + value;
193085
+ }
193086
+ }
193087
+ function isPositiveInteger(value) {
193088
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
193089
+ }
193090
+ function isRecord10(value) {
193091
+ return typeof value === "object" && value !== null && !Array.isArray(value);
193092
+ }
193093
+
192530
193094
  // src/core/verification.ts
192531
193095
  var REAL_AGENTS = ["codex", "claude"];
192532
193096
  var VERIFIABLE_SEVERITIES = new Set(["critical", "high"]);
@@ -192568,9 +193132,12 @@ function groupVerificationTargetsByVerifier(targets) {
192568
193132
  findings
192569
193133
  }));
192570
193134
  }
192571
- function markVerificationOverflow(targets) {
193135
+ function markVerificationOverflow(targets, reason) {
192572
193136
  for (const target of targets) {
192573
- target.finding.verification = { status: "not_verified" };
193137
+ target.finding.verification = {
193138
+ status: "not_verified",
193139
+ ...reason ? { note: reason } : {}
193140
+ };
192574
193141
  }
192575
193142
  }
192576
193143
  function parseVerificationVerdicts(rawText) {
@@ -192582,7 +193149,7 @@ function parseVerificationVerdicts(rawText) {
192582
193149
  if (!Array.isArray(parsed.verdicts))
192583
193150
  return;
192584
193151
  return parsed.verdicts.flatMap((item) => {
192585
- if (!isRecord8(item))
193152
+ if (!isRecord11(item))
192586
193153
  return [];
192587
193154
  if (typeof item.findingId !== "string")
192588
193155
  return [];
@@ -192660,15 +193227,23 @@ function verificationNote(reasoning) {
192660
193227
  function isVerdict(value) {
192661
193228
  return value === "confirmed" || value === "refuted" || value === "uncertain";
192662
193229
  }
192663
- function isRecord8(value) {
193230
+ function isRecord11(value) {
192664
193231
  return typeof value === "object" && value !== null && !Array.isArray(value);
192665
193232
  }
192666
193233
 
192667
193234
  // src/core/runReview.ts
193235
+ function requestForRecursionFingerprint(request) {
193236
+ try {
193237
+ return scanAndRedactSecrets(request).redactedRequest;
193238
+ } catch {
193239
+ return { goal: "" };
193240
+ }
193241
+ }
192668
193242
  async function runReview(tool, request, options = {}) {
192669
193243
  const cwd = options.cwd ?? process.cwd();
192670
193244
  const traceId = newTraceId();
192671
- const startedAt = new Date().toISOString();
193245
+ const startedAtEpochMs = Date.now();
193246
+ const startedAt = new Date(startedAtEpochMs).toISOString();
192672
193247
  const auditEnv = { ...process.env, ...options.env };
192673
193248
  const traceWriterFactory = options.traceWriterFactory ?? createTraceWriter;
192674
193249
  let snapshot;
@@ -192677,6 +193252,14 @@ async function runReview(tool, request, options = {}) {
192677
193252
  } catch (error51) {
192678
193253
  if (error51 instanceof KyosoRequestError) {
192679
193254
  const config2 = kyosoConfigSchema.parse(defaultConfig);
193255
+ const budgetTracker = new ReviewBudgetTracker(config2.reviewBudget, startedAtEpochMs);
193256
+ const requestFingerprint = createRequestFingerprint({
193257
+ tool,
193258
+ request: requestForRecursionFingerprint(request),
193259
+ config: config2,
193260
+ roles: resolveAgentRoles(config2),
193261
+ budget: config2.reviewBudget
193262
+ });
192680
193263
  const trace2 = traceWriterFactory({
192681
193264
  enabled: config2.audit.enabled,
192682
193265
  directory: config2.audit.directory,
@@ -192691,6 +193274,12 @@ async function runReview(tool, request, options = {}) {
192691
193274
  tool,
192692
193275
  timestamp: new Date().toISOString()
192693
193276
  });
193277
+ await writeReviewBudgetPlanned({
193278
+ trace: trace2,
193279
+ traceId,
193280
+ budgetTracker,
193281
+ requestFingerprint
193282
+ });
192694
193283
  return await buildPolicyBlockResult({
192695
193284
  tool,
192696
193285
  trace: trace2,
@@ -192698,6 +193287,8 @@ async function runReview(tool, request, options = {}) {
192698
193287
  startedAt,
192699
193288
  networkMode: config2.network.defaultMode,
192700
193289
  warning: error51.message,
193290
+ budgetTracker,
193291
+ requestFingerprint,
192701
193292
  finding: {
192702
193293
  id: "KYOSO-1",
192703
193294
  severity: "critical",
@@ -192763,6 +193354,8 @@ async function runReview(tool, request, options = {}) {
192763
193354
  timestamp: new Date().toISOString()
192764
193355
  });
192765
193356
  validateReviewRequest(tool, request);
193357
+ const reviewBudget = resolveReviewBudget(loaded.config.reviewBudget, request.options?.reviewBudget);
193358
+ const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs);
192766
193359
  assertTrustedWorkspaceRoot(request.workspace?.root, loaded.config.workspace.root, cwd);
192767
193360
  const networkMode = resolveNetworkMode(request.options?.network, loaded.config.network.defaultMode, options.mcpNetworkMode);
192768
193361
  if (networkMode === "unrestricted" && !loaded.config.network.allowUnrestricted) {
@@ -192781,6 +193374,19 @@ async function runReview(tool, request, options = {}) {
192781
193374
  });
192782
193375
  const allowSecretOverride = loaded.config.secrets.allowOverride && request.options?.allowSecretRedaction === true;
192783
193376
  if (secretScan.detected && loaded.config.secrets.blockOnDetectedSecret && !allowSecretOverride) {
193377
+ const requestFingerprint2 = createRequestFingerprint({
193378
+ tool,
193379
+ request: secretScan.redactedRequest,
193380
+ config: loaded.config,
193381
+ roles: resolveAgentRoles(loaded.config),
193382
+ budget: reviewBudget
193383
+ });
193384
+ await writeReviewBudgetPlanned({
193385
+ trace,
193386
+ traceId,
193387
+ budgetTracker,
193388
+ requestFingerprint: requestFingerprint2
193389
+ });
192784
193390
  return await buildSecretBlockResult({
192785
193391
  tool,
192786
193392
  trace,
@@ -192789,7 +193395,9 @@ async function runReview(tool, request, options = {}) {
192789
193395
  configHash: loaded.configHash,
192790
193396
  networkMode,
192791
193397
  secretScan,
192792
- warnings
193398
+ warnings,
193399
+ budgetTracker,
193400
+ requestFingerprint: requestFingerprint2
192793
193401
  });
192794
193402
  }
192795
193403
  const denyPatterns = mergeDenyPatterns(loaded.config.workspace.deny, secretScan.redactedRequest.workspace?.denyRead);
@@ -192802,6 +193410,19 @@ async function runReview(tool, request, options = {}) {
192802
193410
  });
192803
193411
  warnings.push(...built.warnings);
192804
193412
  const agentRoles = resolveAgentRoles(loaded.config);
193413
+ const requestFingerprint = createRequestFingerprint({
193414
+ tool,
193415
+ request: built.request,
193416
+ config: loaded.config,
193417
+ roles: agentRoles,
193418
+ budget: reviewBudget
193419
+ });
193420
+ await writeReviewBudgetPlanned({
193421
+ trace,
193422
+ traceId,
193423
+ budgetTracker,
193424
+ requestFingerprint
193425
+ });
192805
193426
  snapshot = await createSnapshot(traceId, tool, built.request, {
192806
193427
  denyPatterns,
192807
193428
  allowPatterns,
@@ -192824,14 +193445,22 @@ async function runReview(tool, request, options = {}) {
192824
193445
  networkMode,
192825
193446
  manager,
192826
193447
  trace,
192827
- warnings
193448
+ warnings,
193449
+ budgetTracker
192828
193450
  });
192829
193451
  warnings.push(...agentResults.flatMap((result) => (result.warnings ?? []).map((warning) => `Agent ${result.agent} ${warning}`)));
192830
- const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
192831
- const agentsUsed = normalizedAgentResults.map((result) => result.agent);
192832
- const reviewMode = agentsUsed.length === 1 ? "single_agent" : "multi_agent";
193452
+ const normalized = agentResults.map((result) => normalizeAgentRunResult(result, reviewBudget.maxFindingsPerAgent));
193453
+ const normalizedAgentResults = normalized.map((item) => item.result);
193454
+ for (const item of normalized.filter((item2) => item2.findingsCapped)) {
193455
+ budgetTracker.markIncomplete("coverage_incomplete");
193456
+ warnings.push(`Agent ${item.result.agent} findings were capped at ${reviewBudget.maxFindingsPerAgent}; review coverage is incomplete.`);
193457
+ }
193458
+ const enabledAgents = ["codex", "claude"].filter((agent) => loaded.config.agents[agent].enabled);
193459
+ const agentsUsed = normalizedAgentResults.filter((result) => result.status !== "skipped").map((result) => result.agent);
193460
+ const reviewMode = enabledAgents.length === 1 ? "single_agent" : "multi_agent";
192833
193461
  const completed = normalizedAgentResults.filter((result) => result.status === "completed");
192834
- const degraded = completed.length !== agentResults.length;
193462
+ const attempted = normalizedAgentResults.filter((result) => result.status !== "skipped");
193463
+ const degraded = completed.length !== attempted.length || enabledAgents.length === 0;
192835
193464
  let aggregate = aggregateAgentResults(normalizedAgentResults, {
192836
193465
  reviewMode
192837
193466
  });
@@ -192847,7 +193476,12 @@ async function runReview(tool, request, options = {}) {
192847
193476
  ])
192848
193477
  };
192849
193478
  }
192850
- if (completed.length === 0) {
193479
+ if (completed.length === 0 && (attempted.length > 0 || enabledAgents.length === 0)) {
193480
+ const noPrimaryAgents = enabledAgents.length === 0;
193481
+ if (noPrimaryAgents) {
193482
+ budgetTracker.markIncomplete("coverage_incomplete");
193483
+ warnings.push("No primary review agents are enabled; review coverage is incomplete.");
193484
+ }
192851
193485
  aggregate = {
192852
193486
  ...aggregate,
192853
193487
  findings: [
@@ -192856,9 +193490,9 @@ async function runReview(tool, request, options = {}) {
192856
193490
  id: `KYOSO-${aggregate.findings.length + 1}`,
192857
193491
  severity: "critical",
192858
193492
  category: "other",
192859
- title: "All backend agents failed",
192860
- evidence: normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
192861
- recommendation: "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
193493
+ title: noPrimaryAgents ? "No primary review agents enabled" : "All backend agents failed",
193494
+ evidence: noPrimaryAgents ? "Both configured primary reviewers are disabled." : normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
193495
+ recommendation: noPrimaryAgents ? "Enable at least one primary reviewer before running Kyoso." : "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
192862
193496
  sourceAgents: ["kyoso_policy"],
192863
193497
  confidence: "high"
192864
193498
  }
@@ -192871,7 +193505,7 @@ async function runReview(tool, request, options = {}) {
192871
193505
  findingCount: aggregate.findings.length,
192872
193506
  timestamp: new Date().toISOString()
192873
193507
  });
192874
- const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled ? "cross_agent" : undefined;
193508
+ const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled && enabledAgents.length > 1 ? "cross_agent" : undefined;
192875
193509
  if (verificationMode === "cross_agent") {
192876
193510
  warnings.push(...await runFindingVerification({
192877
193511
  tool,
@@ -192882,11 +193516,16 @@ async function runReview(tool, request, options = {}) {
192882
193516
  networkMode,
192883
193517
  manager,
192884
193518
  trace,
192885
- findings: aggregate.findings
193519
+ findings: aggregate.findings,
193520
+ budgetTracker
192886
193521
  }));
192887
193522
  }
193523
+ if (aggregate.findings.some((finding) => finding.verification?.status === "refuted")) {
193524
+ budgetTracker.markIncomplete("disputed_finding");
193525
+ }
192888
193526
  const cisa = tool === "security_review" ? computeCisaGate(aggregate.findings, normalizedAgentResults) : undefined;
192889
- const decision = decide({
193527
+ const budgetBeforeJudge = budgetTracker.snapshot();
193528
+ const decision = budgetBeforeJudge.completion.status === "incomplete" ? "block" : decide({
192890
193529
  tool,
192891
193530
  findings: aggregate.findings,
192892
193531
  cisa,
@@ -192896,6 +193535,9 @@ async function runReview(tool, request, options = {}) {
192896
193535
  const completedAt = new Date().toISOString();
192897
193536
  const resultWithoutMarkdown = {
192898
193537
  decision,
193538
+ completion: budgetBeforeJudge.completion,
193539
+ executionBudget: budgetBeforeJudge.executionBudget,
193540
+ requestFingerprint,
192899
193541
  degraded,
192900
193542
  agentsUsed,
192901
193543
  reviewMode,
@@ -192917,18 +193559,22 @@ async function runReview(tool, request, options = {}) {
192917
193559
  networkMode,
192918
193560
  workspaceMode: "temp_snapshot",
192919
193561
  configHash: loaded.configHash,
192920
- warnings: Array.from(new Set([...warnings, ...trace.warnings]))
193562
+ warnings: Array.from(new Set([...warnings, ...trace.warnings])),
193563
+ modelCalls: budgetBeforeJudge.modelCalls
192921
193564
  }
192922
193565
  };
192923
193566
  const summaryText = defaultSummaryText(resultWithoutMarkdown);
192924
- const judge = await runJudge({
193567
+ const judge = await runBudgetedJudge({
192925
193568
  tool,
192926
193569
  result: resultWithoutMarkdown,
192927
193570
  summaryText,
192928
193571
  agentFindings: buildJudgeAgentFindings(normalizedAgentResults),
192929
193572
  config: loaded.config.judge,
192930
193573
  requestedProvider: request.options?.judgeProvider,
192931
- env: options.env ?? process.env
193574
+ env: options.env ?? process.env,
193575
+ budgetTracker,
193576
+ trace,
193577
+ traceId
192932
193578
  });
192933
193579
  const judgeComments = new Map(judge.output.disagreementComments.map((comment) => [
192934
193580
  comment.topic,
@@ -192939,10 +193585,20 @@ async function runReview(tool, request, options = {}) {
192939
193585
  judgeComment: judgeComments.get(disagreement.topic) ?? disagreement.judgeComment
192940
193586
  }));
192941
193587
  const crossModelAnalysis = buildCrossModelAnalysis(judge, reviewMode);
193588
+ const budgetAfterJudge = budgetTracker.snapshot();
193589
+ const finalDecision = budgetAfterJudge.completion.status === "incomplete" ? "block" : decision;
192942
193590
  const resultAfterJudge = {
192943
193591
  ...resultWithoutMarkdown,
193592
+ decision: finalDecision,
193593
+ completion: budgetAfterJudge.completion,
193594
+ executionBudget: budgetAfterJudge.executionBudget,
192944
193595
  disagreements,
192945
- ...crossModelAnalysis ? { crossModelAnalysis } : {}
193596
+ ...crossModelAnalysis ? { crossModelAnalysis } : {},
193597
+ audit: {
193598
+ ...resultWithoutMarkdown.audit,
193599
+ completedAt: new Date().toISOString(),
193600
+ modelCalls: budgetAfterJudge.modelCalls
193601
+ }
192946
193602
  };
192947
193603
  const judgeEvent = {
192948
193604
  type: "judge_completed",
@@ -192955,10 +193611,16 @@ async function runReview(tool, request, options = {}) {
192955
193611
  judgeEvent.error = judge.error;
192956
193612
  await trace.write(judgeEvent);
192957
193613
  resultAfterJudge.audit.completedAt = new Date().toISOString();
193614
+ await writeReviewBudgetCompleted({
193615
+ trace,
193616
+ traceId,
193617
+ budgetTracker,
193618
+ requestFingerprint
193619
+ });
192958
193620
  await trace.write({
192959
193621
  type: "decision_completed",
192960
193622
  traceId,
192961
- decision,
193623
+ decision: finalDecision,
192962
193624
  timestamp: new Date().toISOString()
192963
193625
  });
192964
193626
  await trace.write({
@@ -192970,7 +193632,7 @@ async function runReview(tool, request, options = {}) {
192970
193632
  tool,
192971
193633
  trace,
192972
193634
  result: resultAfterJudge,
192973
- summaryText: judge.output.summaryText
193635
+ summaryText: resultAfterJudge.completion.status === "incomplete" ? defaultSummaryText(resultAfterJudge) : judge.output.summaryText
192974
193636
  });
192975
193637
  } finally {
192976
193638
  await trace.finalize();
@@ -192981,37 +193643,180 @@ async function runReview(tool, request, options = {}) {
192981
193643
  async function runFindingVerification(input) {
192982
193644
  const allowDemotionRequested = input.config.verification.allowDemotion;
192983
193645
  const selection = selectVerificationTargets(input.findings, input.config.verification.maxFindings);
192984
- markVerificationOverflow(selection.overflow);
192985
- if (selection.selected.length === 0)
192986
- return [];
192987
193646
  const warnings = [];
192988
- const groups = groupVerificationTargetsByVerifier(selection.selected);
193647
+ if (selection.overflow.length > 0) {
193648
+ markVerificationOverflow(selection.overflow, "verification_max_findings");
193649
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193650
+ }
193651
+ if (selection.selected.length === 0)
193652
+ return warnings;
193653
+ const potentialGroups = groupVerificationTargetsByVerifier(selection.selected);
192989
193654
  await input.trace.write({
192990
193655
  type: "verification_started",
192991
193656
  traceId: input.traceId,
192992
193657
  targetCount: selection.selected.length,
192993
193658
  notVerifiedCount: selection.overflow.length,
192994
- verifierCount: groups.length,
193659
+ verifierCount: potentialGroups.length,
192995
193660
  timeoutMs: input.config.verification.timeoutMs,
192996
193661
  allowDemotionRequested,
192997
193662
  timestamp: new Date().toISOString()
192998
193663
  });
192999
- const agentInputs = groups.map(({ verifier, findings }) => ({
193664
+ if (input.budgetTracker.budget.skipOptionalPhasesWhenTokenUsageUnknown && input.budgetTracker.isTokenUsageUnknown()) {
193665
+ markVerificationOverflow(selection.selected, "token_usage_unknown");
193666
+ input.budgetTracker.markIncomplete("token_usage_unknown");
193667
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193668
+ warnings.push("Finding verification was skipped because primary-agent token usage was not reported.");
193669
+ for (const group of potentialGroups) {
193670
+ input.budgetTracker.recordSkipped({
193671
+ kind: "verifier",
193672
+ agent: group.verifier,
193673
+ reason: "token_usage_unknown"
193674
+ });
193675
+ await input.trace.write({
193676
+ type: "model_call_skipped",
193677
+ traceId: input.traceId,
193678
+ kind: "verifier",
193679
+ agent: group.verifier,
193680
+ reason: "token_usage_unknown",
193681
+ timestamp: new Date().toISOString()
193682
+ });
193683
+ }
193684
+ await input.trace.write({
193685
+ type: "verification_completed",
193686
+ traceId: input.traceId,
193687
+ counts: countVerificationStatuses(input.findings),
193688
+ timestamp: new Date().toISOString()
193689
+ });
193690
+ return warnings;
193691
+ }
193692
+ const groups = new Map;
193693
+ const unavailableVerifiers = new Map;
193694
+ for (const target of selection.selected) {
193695
+ const existing = groups.get(target.verifier);
193696
+ if (existing) {
193697
+ existing.targets.push(target);
193698
+ continue;
193699
+ }
193700
+ const unavailable = unavailableVerifiers.get(target.verifier);
193701
+ if (unavailable) {
193702
+ markVerificationOverflow([target], unavailable === "model_call_budget" ? "budget_exhausted" : "deadline");
193703
+ continue;
193704
+ }
193705
+ const reservationResult = input.budgetTracker.reserve({
193706
+ kind: "verifier",
193707
+ agent: target.verifier
193708
+ });
193709
+ if ("failure" in reservationResult) {
193710
+ unavailableVerifiers.set(target.verifier, reservationResult.failure.reason);
193711
+ markVerificationOverflow([target], reservationResult.failure.reason === "model_call_budget" ? "budget_exhausted" : "deadline");
193712
+ input.budgetTracker.recordSkipped({
193713
+ kind: "verifier",
193714
+ agent: target.verifier,
193715
+ reason: reservationResult.failure.reason
193716
+ });
193717
+ input.budgetTracker.markIncomplete(reservationResult.failure.reason);
193718
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193719
+ await input.trace.write({
193720
+ type: "review_budget_exhausted",
193721
+ traceId: input.traceId,
193722
+ phase: "verification",
193723
+ kind: "verifier",
193724
+ agent: target.verifier,
193725
+ reason: reservationResult.failure.reason,
193726
+ timestamp: new Date().toISOString()
193727
+ });
193728
+ await input.trace.write({
193729
+ type: "model_call_skipped",
193730
+ traceId: input.traceId,
193731
+ kind: "verifier",
193732
+ agent: target.verifier,
193733
+ reason: reservationResult.failure.reason,
193734
+ timestamp: new Date().toISOString()
193735
+ });
193736
+ continue;
193737
+ }
193738
+ const group = {
193739
+ verifier: target.verifier,
193740
+ targets: [target],
193741
+ reservation: reservationResult.reservation
193742
+ };
193743
+ groups.set(target.verifier, group);
193744
+ await input.trace.write({
193745
+ type: "model_call_reserved",
193746
+ traceId: input.traceId,
193747
+ kind: "verifier",
193748
+ agent: target.verifier,
193749
+ timestamp: new Date().toISOString()
193750
+ });
193751
+ }
193752
+ const scheduledGroups = [];
193753
+ for (const group of groups.values()) {
193754
+ const timeoutMs = Math.min(input.config.verification.timeoutMs, input.budgetTracker.remainingWallTimeMs());
193755
+ if (timeoutMs > 0) {
193756
+ scheduledGroups.push(group);
193757
+ continue;
193758
+ }
193759
+ input.budgetTracker.skip(group.reservation, "deadline");
193760
+ input.budgetTracker.markIncomplete("deadline");
193761
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193762
+ markVerificationOverflow(group.targets, "deadline");
193763
+ await input.trace.write({
193764
+ type: "review_budget_exhausted",
193765
+ traceId: input.traceId,
193766
+ phase: "verification",
193767
+ kind: "verifier",
193768
+ agent: group.verifier,
193769
+ reason: "deadline",
193770
+ timestamp: new Date().toISOString()
193771
+ });
193772
+ await input.trace.write({
193773
+ type: "model_call_skipped",
193774
+ traceId: input.traceId,
193775
+ kind: "verifier",
193776
+ agent: group.verifier,
193777
+ reason: "deadline",
193778
+ timestamp: new Date().toISOString()
193779
+ });
193780
+ }
193781
+ if (scheduledGroups.length === 0) {
193782
+ await input.trace.write({
193783
+ type: "verification_completed",
193784
+ traceId: input.traceId,
193785
+ counts: countVerificationStatuses(input.findings),
193786
+ timestamp: new Date().toISOString()
193787
+ });
193788
+ return warnings;
193789
+ }
193790
+ const agentInputs = scheduledGroups.map((group) => ({
193000
193791
  traceId: input.traceId,
193001
- agent: verifier,
193792
+ agent: group.verifier,
193002
193793
  role: "finding_verifier",
193003
193794
  tool: input.tool,
193004
- prompt: buildFindingVerifierPrompt(input.tool, input.request, verifier, findings),
193795
+ prompt: buildFindingVerifierPrompt(input.tool, input.request, group.verifier, group.targets.map((target) => target.finding)),
193005
193796
  workspaceDir: input.workspaceDir,
193006
- timeoutMs: input.config.verification.timeoutMs,
193007
- networkMode: input.networkMode
193797
+ timeoutMs: Math.min(input.config.verification.timeoutMs, input.budgetTracker.remainingWallTimeMs()),
193798
+ deadlineAtEpochMs: input.budgetTracker.deadlineAtEpochMs,
193799
+ maxOutputBytes: input.budgetTracker.budget.maxAgentOutputBytes,
193800
+ networkMode: input.networkMode,
193801
+ onStarted: () => {
193802
+ input.budgetTracker.markStarted(group.reservation);
193803
+ return Promise.resolve();
193804
+ }
193008
193805
  }));
193009
193806
  let results;
193010
193807
  try {
193011
193808
  results = await input.manager.runAll(agentInputs);
193012
193809
  } catch (error51) {
193013
- for (const group of groups) {
193014
- applyVerificationVerdicts(selection.selected, group.verifier, undefined);
193810
+ for (const group of scheduledGroups) {
193811
+ applyVerificationVerdicts(group.targets, group.verifier, undefined);
193812
+ await finalizeModelCallResult({
193813
+ budgetTracker: input.budgetTracker,
193814
+ reservation: group.reservation,
193815
+ result: failedVerifierResult(group.verifier, "AGENT_MANAGER_FAILED"),
193816
+ trace: input.trace,
193817
+ traceId: input.traceId
193818
+ });
193819
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193015
193820
  }
193016
193821
  const message = `Finding verification failed: ${sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51))}`;
193017
193822
  warnings.push(message);
@@ -193024,9 +193829,32 @@ async function runFindingVerification(input) {
193024
193829
  });
193025
193830
  return warnings;
193026
193831
  }
193027
- for (const result of results) {
193832
+ const resultByAgent = new Map(results.map((result) => [result.agent, result]));
193833
+ for (const group of scheduledGroups) {
193834
+ const result = resultByAgent.get(group.verifier);
193835
+ if (!result) {
193836
+ applyVerificationVerdicts(group.targets, group.verifier, undefined);
193837
+ await finalizeModelCallResult({
193838
+ budgetTracker: input.budgetTracker,
193839
+ reservation: group.reservation,
193840
+ result: failedVerifierResult(group.verifier, "AGENT_RESULT_MISSING"),
193841
+ trace: input.trace,
193842
+ traceId: input.traceId
193843
+ });
193844
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193845
+ warnings.push(`Finding verification by ${group.verifier} did not return a result.`);
193846
+ continue;
193847
+ }
193848
+ await finalizeModelCallResult({
193849
+ budgetTracker: input.budgetTracker,
193850
+ reservation: group.reservation,
193851
+ result,
193852
+ trace: input.trace,
193853
+ traceId: input.traceId
193854
+ });
193028
193855
  if (result.status !== "completed") {
193029
- applyVerificationVerdicts(selection.selected, result.agent, undefined);
193856
+ applyVerificationVerdicts(group.targets, result.agent, undefined);
193857
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193030
193858
  const message = `Finding verification by ${result.agent} ${result.status}: ${result.error?.message ?? result.error?.code ?? "no detail"}`;
193031
193859
  warnings.push(sanitizeTextForDisplay(message));
193032
193860
  await input.trace.write({
@@ -193040,8 +193868,9 @@ async function runFindingVerification(input) {
193040
193868
  continue;
193041
193869
  }
193042
193870
  const verdicts = parseVerificationVerdicts(result.rawText);
193043
- applyVerificationVerdicts(selection.selected, result.agent, verdicts);
193871
+ applyVerificationVerdicts(group.targets, result.agent, verdicts);
193044
193872
  if (!verdicts) {
193873
+ input.budgetTracker.markIncomplete("coverage_incomplete");
193045
193874
  const message = `Finding verification by ${result.agent} returned malformed verdict JSON.`;
193046
193875
  warnings.push(message);
193047
193876
  await input.trace.write({
@@ -193061,6 +193890,20 @@ async function runFindingVerification(input) {
193061
193890
  });
193062
193891
  return warnings;
193063
193892
  }
193893
+ function failedVerifierResult(agent, code) {
193894
+ const timestamp = new Date().toISOString();
193895
+ return {
193896
+ agent,
193897
+ role: "finding_verifier",
193898
+ status: "failed",
193899
+ startedAt: timestamp,
193900
+ completedAt: timestamp,
193901
+ error: {
193902
+ code,
193903
+ message: "The agent manager did not return a verification result."
193904
+ }
193905
+ };
193906
+ }
193064
193907
  function buildJudgeAgentFindings(results) {
193065
193908
  return results.flatMap((result) => {
193066
193909
  if (!result.normalized)
@@ -193097,13 +193940,182 @@ function buildCrossModelAnalysis(judge, reviewMode) {
193097
193940
  provider: judge.provider
193098
193941
  };
193099
193942
  }
193943
+ async function runBudgetedJudge(input) {
193944
+ const configuredProvider = input.requestedProvider ?? input.config.provider;
193945
+ const provider = resolveJudgeProvider(configuredProvider, input.env);
193946
+ if (input.config.mode === "deterministic_only" || provider === "deterministic_fallback") {
193947
+ return runJudge(input);
193948
+ }
193949
+ const fallback = () => runJudge({
193950
+ ...input,
193951
+ config: { ...input.config, mode: "deterministic_only" }
193952
+ });
193953
+ if (input.budgetTracker.snapshot().completion.status === "incomplete") {
193954
+ await recordSkippedJudgeCall(input, "review_incomplete");
193955
+ return fallback();
193956
+ }
193957
+ if (input.budgetTracker.budget.skipOptionalPhasesWhenTokenUsageUnknown && input.budgetTracker.isTokenUsageUnknown()) {
193958
+ await recordSkippedJudgeCall(input, "token_usage_unknown");
193959
+ return fallback();
193960
+ }
193961
+ const reservationResult = input.budgetTracker.reserve({ kind: "judge" });
193962
+ if ("failure" in reservationResult) {
193963
+ await recordSkippedJudgeCall(input, reservationResult.failure.reason);
193964
+ await input.trace.write({
193965
+ type: "review_budget_exhausted",
193966
+ traceId: input.traceId,
193967
+ phase: "judge",
193968
+ kind: "judge",
193969
+ reason: reservationResult.failure.reason,
193970
+ timestamp: new Date().toISOString()
193971
+ });
193972
+ return fallback();
193973
+ }
193974
+ const reservation = reservationResult.reservation;
193975
+ await input.trace.write({
193976
+ type: "model_call_reserved",
193977
+ traceId: input.traceId,
193978
+ kind: "judge",
193979
+ timestamp: new Date().toISOString()
193980
+ });
193981
+ const timeoutMs = Math.min(input.config.timeoutMs, input.budgetTracker.remainingWallTimeMs());
193982
+ if (timeoutMs <= 0) {
193983
+ input.budgetTracker.skip(reservation, "deadline");
193984
+ await input.trace.write({
193985
+ type: "review_budget_exhausted",
193986
+ traceId: input.traceId,
193987
+ phase: "judge",
193988
+ kind: "judge",
193989
+ reason: "deadline",
193990
+ timestamp: new Date().toISOString()
193991
+ });
193992
+ await input.trace.write({
193993
+ type: "model_call_skipped",
193994
+ traceId: input.traceId,
193995
+ kind: "judge",
193996
+ reason: "deadline",
193997
+ timestamp: new Date().toISOString()
193998
+ });
193999
+ return fallback();
194000
+ }
194001
+ input.budgetTracker.markStarted(reservation);
194002
+ const judge = await runJudge({ ...input, timeoutMs });
194003
+ const usage = normalizeModelTokenUsage(judge.usage);
194004
+ input.budgetTracker.complete(reservation, {
194005
+ ...usage ? { usage } : {}
194006
+ });
194007
+ await input.trace.write({
194008
+ type: "model_call_completed",
194009
+ traceId: input.traceId,
194010
+ kind: "judge",
194011
+ provider: judge.provider,
194012
+ resultStatus: judge.status,
194013
+ ...usage ? { usage } : {},
194014
+ timestamp: new Date().toISOString()
194015
+ });
194016
+ return judge;
194017
+ }
194018
+ async function recordSkippedJudgeCall(input, reason) {
194019
+ input.budgetTracker.recordSkipped({ kind: "judge", reason });
194020
+ await input.trace.write({
194021
+ type: "model_call_skipped",
194022
+ traceId: input.traceId,
194023
+ kind: "judge",
194024
+ reason,
194025
+ timestamp: new Date().toISOString()
194026
+ });
194027
+ }
193100
194028
  async function runAgents(input) {
193101
194029
  const agentRoles = resolveAgentRoles(input.config);
194030
+ const enabledAgents = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled);
194031
+ if (enabledAgents.length === 0)
194032
+ return [];
194033
+ const reservationResult = input.budgetTracker.reserveMany(enabledAgents.map((agent) => ({ kind: "primary", agent })));
194034
+ if ("failure" in reservationResult) {
194035
+ input.budgetTracker.markIncomplete(reservationResult.failure.reason);
194036
+ input.budgetTracker.markIncomplete("coverage_incomplete");
194037
+ await input.trace.write({
194038
+ type: "review_budget_exhausted",
194039
+ traceId: input.traceId,
194040
+ phase: "primary",
194041
+ reason: reservationResult.failure.reason,
194042
+ requiredCalls: enabledAgents.length,
194043
+ timestamp: new Date().toISOString()
194044
+ });
194045
+ for (const agent of enabledAgents) {
194046
+ input.budgetTracker.recordSkipped({
194047
+ kind: "primary",
194048
+ agent,
194049
+ reason: reservationResult.failure.reason
194050
+ });
194051
+ await input.trace.write({
194052
+ type: "model_call_skipped",
194053
+ traceId: input.traceId,
194054
+ kind: "primary",
194055
+ agent,
194056
+ reason: reservationResult.failure.reason,
194057
+ timestamp: new Date().toISOString()
194058
+ });
194059
+ }
194060
+ return enabledAgents.map((agent) => {
194061
+ const role = agentRoles[agent] ?? input.config.agents[agent].role;
194062
+ const timestamp = new Date().toISOString();
194063
+ return {
194064
+ agent,
194065
+ role,
194066
+ status: "skipped",
194067
+ startedAt: timestamp,
194068
+ completedAt: timestamp,
194069
+ error: {
194070
+ code: reservationResult.failure.reason === "deadline" ? "REVIEW_DEADLINE_EXCEEDED" : "MODEL_CALL_BUDGET_EXHAUSTED",
194071
+ message: reservationResult.failure.reason === "deadline" ? "Review deadline was reached before primary agents could start." : "The review model-call budget cannot reserve all primary agents."
194072
+ }
194073
+ };
194074
+ });
194075
+ }
194076
+ const reservations = new Map(reservationResult.reservations.map((reservation) => [
194077
+ reservation.agent,
194078
+ reservation
194079
+ ]));
194080
+ for (const reservation of reservationResult.reservations) {
194081
+ await input.trace.write({
194082
+ type: "model_call_reserved",
194083
+ traceId: input.traceId,
194084
+ kind: reservation.kind,
194085
+ agent: reservation.agent,
194086
+ timestamp: new Date().toISOString()
194087
+ });
194088
+ }
194089
+ if (input.budgetTracker.remainingWallTimeMs() <= 0) {
194090
+ input.budgetTracker.markIncomplete("deadline");
194091
+ input.budgetTracker.markIncomplete("coverage_incomplete");
194092
+ await input.trace.write({
194093
+ type: "review_budget_exhausted",
194094
+ traceId: input.traceId,
194095
+ phase: "primary",
194096
+ reason: "deadline",
194097
+ timestamp: new Date().toISOString()
194098
+ });
194099
+ return await skipReservedPrimaryAgents({
194100
+ trace: input.trace,
194101
+ traceId: input.traceId,
194102
+ budgetTracker: input.budgetTracker,
194103
+ config: input.config,
194104
+ agents: enabledAgents,
194105
+ agentRoles,
194106
+ reservations,
194107
+ reason: "deadline"
194108
+ });
194109
+ }
193102
194110
  const startedWrites = [];
193103
194111
  let acceptingStartedEvents = true;
193104
- const agentInputs = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => {
194112
+ const agentInputs = enabledAgents.map((agent) => {
193105
194113
  const agentConfig = input.config.agents[agent];
193106
194114
  const role = agentRoles[agent] ?? agentConfig.role;
194115
+ const reservation = reservations.get(agent);
194116
+ if (!reservation) {
194117
+ throw new Error(`Missing primary budget reservation for ${agent}.`);
194118
+ }
193107
194119
  return {
193108
194120
  traceId: input.traceId,
193109
194121
  agent,
@@ -193111,9 +194123,12 @@ async function runAgents(input) {
193111
194123
  tool: input.tool,
193112
194124
  prompt: buildAgentPrompt(input.tool, input.request, agent, role),
193113
194125
  workspaceDir: input.workspaceDir,
193114
- timeoutMs: input.request.options?.maxAgentTimeoutMs ?? agentConfig.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
194126
+ timeoutMs: Math.min(input.request.options?.maxAgentTimeoutMs ?? Number.POSITIVE_INFINITY, agentConfig.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS, input.budgetTracker.remainingWallTimeMs()),
194127
+ deadlineAtEpochMs: input.budgetTracker.deadlineAtEpochMs,
194128
+ maxOutputBytes: input.budgetTracker.budget.maxAgentOutputBytes,
193115
194129
  networkMode: input.networkMode,
193116
194130
  onStarted: () => {
194131
+ input.budgetTracker.markStarted(reservation);
193117
194132
  if (!acceptingStartedEvents)
193118
194133
  return Promise.resolve();
193119
194134
  const event = {
@@ -193141,10 +194156,61 @@ async function runAgents(input) {
193141
194156
  }
193142
194157
  };
193143
194158
  });
193144
- const results = await input.manager.runAll(agentInputs);
194159
+ let results;
194160
+ try {
194161
+ results = await input.manager.runAll(agentInputs);
194162
+ } catch (error51) {
194163
+ const detail = sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51));
194164
+ input.warnings.push(`Primary-agent execution failed: ${detail}`);
194165
+ results = agentInputs.map((agentInput) => ({
194166
+ agent: agentInput.agent,
194167
+ role: agentInput.role,
194168
+ status: "failed",
194169
+ startedAt: new Date().toISOString(),
194170
+ completedAt: new Date().toISOString(),
194171
+ error: {
194172
+ code: "AGENT_MANAGER_FAILED",
194173
+ message: "The agent manager did not return a review result."
194174
+ }
194175
+ }));
194176
+ }
193145
194177
  acceptingStartedEvents = false;
193146
194178
  await Promise.all(startedWrites);
193147
- await Promise.all(results.map((result) => {
194179
+ const resultByAgent = new Map(results.map((result) => [result.agent, result]));
194180
+ const orderedResults = enabledAgents.map((agent) => {
194181
+ const existing = resultByAgent.get(agent);
194182
+ if (existing)
194183
+ return existing;
194184
+ const role = agentRoles[agent] ?? input.config.agents[agent].role;
194185
+ const timestamp = new Date().toISOString();
194186
+ return {
194187
+ agent,
194188
+ role,
194189
+ status: "failed",
194190
+ startedAt: timestamp,
194191
+ completedAt: timestamp,
194192
+ error: {
194193
+ code: "AGENT_RESULT_MISSING",
194194
+ message: "The agent manager did not return a review result."
194195
+ }
194196
+ };
194197
+ });
194198
+ for (const result of orderedResults) {
194199
+ const reservation = reservations.get(result.agent);
194200
+ if (!reservation)
194201
+ continue;
194202
+ await finalizeModelCallResult({
194203
+ budgetTracker: input.budgetTracker,
194204
+ reservation,
194205
+ result,
194206
+ trace: input.trace,
194207
+ traceId: input.traceId
194208
+ });
194209
+ if (result.status !== "completed") {
194210
+ input.budgetTracker.markIncomplete("coverage_incomplete");
194211
+ }
194212
+ }
194213
+ await Promise.all(orderedResults.map((result) => {
193148
194214
  const event = {
193149
194215
  type: "agent_completed",
193150
194216
  traceId: input.traceId,
@@ -193164,8 +194230,92 @@ async function runAgents(input) {
193164
194230
  }
193165
194231
  return input.trace.write(event);
193166
194232
  }));
194233
+ return orderedResults;
194234
+ }
194235
+ async function skipReservedPrimaryAgents(input) {
194236
+ const results = [];
194237
+ for (const agent of input.agents) {
194238
+ const reservation = input.reservations.get(agent);
194239
+ if (reservation)
194240
+ input.budgetTracker.skip(reservation, input.reason);
194241
+ await input.trace.write({
194242
+ type: "model_call_skipped",
194243
+ traceId: input.traceId,
194244
+ kind: "primary",
194245
+ agent,
194246
+ reason: input.reason,
194247
+ timestamp: new Date().toISOString()
194248
+ });
194249
+ const timestamp = new Date().toISOString();
194250
+ results.push({
194251
+ agent,
194252
+ role: input.agentRoles[agent] ?? input.config.agents[agent].role,
194253
+ status: "skipped",
194254
+ startedAt: timestamp,
194255
+ completedAt: timestamp,
194256
+ error: {
194257
+ code: "REVIEW_DEADLINE_EXCEEDED",
194258
+ message: "Review deadline was reached before the agent could start."
194259
+ }
194260
+ });
194261
+ }
193167
194262
  return results;
193168
194263
  }
194264
+ async function finalizeModelCallResult(input) {
194265
+ const reason = input.result.error?.code ?? input.result.status;
194266
+ const hasStarted = input.budgetTracker.hasStarted(input.reservation);
194267
+ const canSkip = input.result.status === "skipped" || isPreflightAgentFailure(input.result) || !hasStarted && input.result.error?.code === "REVIEW_DEADLINE_EXCEEDED";
194268
+ if (canSkip && !hasStarted) {
194269
+ input.budgetTracker.skip(input.reservation, reason);
194270
+ if (input.result.error?.code === "REVIEW_DEADLINE_EXCEEDED") {
194271
+ input.budgetTracker.markIncomplete("deadline");
194272
+ }
194273
+ await input.trace.write({
194274
+ type: "model_call_skipped",
194275
+ traceId: input.traceId,
194276
+ kind: input.reservation.kind,
194277
+ agent: input.reservation.agent,
194278
+ reason,
194279
+ timestamp: new Date().toISOString()
194280
+ });
194281
+ return;
194282
+ }
194283
+ input.budgetTracker.markStarted(input.reservation);
194284
+ const usage = normalizeModelTokenUsage(input.result.usage);
194285
+ const outputBytes = input.result.outputBytes ?? (input.result.rawText ? Buffer.byteLength(input.result.rawText, "utf8") : undefined);
194286
+ input.budgetTracker.complete(input.reservation, {
194287
+ ...outputBytes === undefined ? {} : { outputBytes },
194288
+ ...usage ? { usage } : {},
194289
+ ...input.result.stopReason ? { stopReason: input.result.stopReason } : {}
194290
+ });
194291
+ if (input.result.error?.code === "AGENT_OUTPUT_LIMIT") {
194292
+ input.budgetTracker.markIncomplete("agent_output_limit");
194293
+ }
194294
+ if (input.result.error?.code === "REVIEW_DEADLINE_EXCEEDED") {
194295
+ input.budgetTracker.markIncomplete("deadline");
194296
+ }
194297
+ await input.trace.write({
194298
+ type: "model_call_completed",
194299
+ traceId: input.traceId,
194300
+ kind: input.reservation.kind,
194301
+ agent: input.reservation.agent,
194302
+ resultStatus: input.result.status,
194303
+ ...input.result.error?.code ? { errorCode: input.result.error.code } : {},
194304
+ ...outputBytes === undefined ? {} : { outputBytes },
194305
+ ...usage ? { usage } : {},
194306
+ ...input.result.stopReason ? { stopReason: input.result.stopReason } : {},
194307
+ timestamp: new Date().toISOString()
194308
+ });
194309
+ }
194310
+ function isPreflightAgentFailure(result) {
194311
+ return result.status === "failed" && [
194312
+ "AGENT_CONFIG_INVALID",
194313
+ "OPENROUTER_KEY_MISSING",
194314
+ "AGENT_SPAWN_FAILED",
194315
+ "AGENT_MANAGER_FAILED",
194316
+ "AGENT_RESULT_MISSING"
194317
+ ].includes(result.error?.code ?? "");
194318
+ }
193169
194319
  function resolveAgentRoles(config2) {
193170
194320
  const enabledAgents = ["codex", "claude"].filter((agent) => config2.agents[agent].enabled);
193171
194321
  const singleAgentMode = enabledAgents.length === 1;
@@ -193181,14 +194331,30 @@ function defaultAgentManager(config2, parentEnv) {
193181
194331
  }
193182
194332
  return new SubprocessAcpAgentManager(config2, parentEnv);
193183
194333
  }
193184
- function normalizeAgentRunResult(result) {
193185
- if (result.status === "completed" && result.rawText && !result.normalized) {
193186
- return {
193187
- ...result,
193188
- normalized: normalizeAgentOutput(result.agent, result.role, result.rawText)
193189
- };
193190
- }
193191
- return result;
194334
+ function normalizeAgentRunResult(result, maxFindingsPerAgent) {
194335
+ const normalizedResult = result.status === "completed" && result.rawText && !result.normalized ? {
194336
+ ...result,
194337
+ normalized: normalizeAgentOutput(result.agent, result.role, result.rawText)
194338
+ } : result;
194339
+ const normalized = normalizedResult.normalized;
194340
+ const findings = normalized?.findings;
194341
+ if (!normalized || !findings || findings.length <= maxFindingsPerAgent) {
194342
+ return { result: normalizedResult, findingsCapped: false };
194343
+ }
194344
+ const limitedFindings = findings.map((finding, index) => ({ finding, index })).sort((left, right) => {
194345
+ const severity = compareSeverity(left.finding.severity, right.finding.severity);
194346
+ return severity === 0 ? left.index - right.index : severity;
194347
+ }).slice(0, maxFindingsPerAgent).map(({ finding }) => finding);
194348
+ return {
194349
+ result: {
194350
+ ...normalizedResult,
194351
+ normalized: {
194352
+ ...normalized,
194353
+ findings: limitedFindings
194354
+ }
194355
+ },
194356
+ findingsCapped: true
194357
+ };
193192
194358
  }
193193
194359
  function agentOpinionSummary(result, includeRawText = false) {
193194
194360
  const opinion = {
@@ -193210,8 +194376,12 @@ async function buildSecretBlockResult(input) {
193210
194376
  });
193211
194377
  const cisa = input.tool === "security_review" ? computeCisaGate([finding], []) : undefined;
193212
194378
  const completedAt = new Date().toISOString();
194379
+ const budget = input.budgetTracker.snapshot();
193213
194380
  const resultWithoutMarkdown = {
193214
194381
  decision: "block",
194382
+ completion: budget.completion,
194383
+ executionBudget: budget.executionBudget,
194384
+ requestFingerprint: input.requestFingerprint,
193215
194385
  degraded: false,
193216
194386
  agentsUsed: [],
193217
194387
  reviewMode: "multi_agent",
@@ -193247,9 +194417,16 @@ async function buildSecretBlockResult(input) {
193247
194417
  networkMode: input.networkMode,
193248
194418
  workspaceMode: "temp_snapshot",
193249
194419
  configHash: input.configHash,
193250
- warnings: input.warnings
194420
+ warnings: input.warnings,
194421
+ modelCalls: budget.modelCalls
193251
194422
  }
193252
194423
  };
194424
+ await writeReviewBudgetCompleted({
194425
+ trace: input.trace,
194426
+ traceId: input.traceId,
194427
+ budgetTracker: input.budgetTracker,
194428
+ requestFingerprint: input.requestFingerprint
194429
+ });
193253
194430
  await input.trace.write({
193254
194431
  type: "decision_completed",
193255
194432
  traceId: input.traceId,
@@ -193292,8 +194469,12 @@ function reindexFindings(findings) {
193292
194469
  }
193293
194470
  async function buildPolicyBlockResult(input) {
193294
194471
  const completedAt = new Date().toISOString();
194472
+ const budget = input.budgetTracker.snapshot();
193295
194473
  const resultWithoutMarkdown = {
193296
194474
  decision: "block",
194475
+ completion: budget.completion,
194476
+ executionBudget: budget.executionBudget,
194477
+ requestFingerprint: input.requestFingerprint,
193297
194478
  degraded: false,
193298
194479
  agentsUsed: [],
193299
194480
  reviewMode: "multi_agent",
@@ -193312,9 +194493,16 @@ async function buildPolicyBlockResult(input) {
193312
194493
  networkMode: input.networkMode,
193313
194494
  workspaceMode: "temp_snapshot",
193314
194495
  configHash: input.configHash,
193315
- warnings: [input.warning]
194496
+ warnings: [input.warning],
194497
+ modelCalls: budget.modelCalls
193316
194498
  }
193317
194499
  };
194500
+ await writeReviewBudgetCompleted({
194501
+ trace: input.trace,
194502
+ traceId: input.traceId,
194503
+ budgetTracker: input.budgetTracker,
194504
+ requestFingerprint: input.requestFingerprint
194505
+ });
193318
194506
  await input.trace.write({
193319
194507
  type: "decision_completed",
193320
194508
  traceId: input.traceId,
@@ -193351,6 +194539,33 @@ async function finalizeReviewResult(input) {
193351
194539
  })
193352
194540
  };
193353
194541
  }
194542
+ async function writeReviewBudgetPlanned(input) {
194543
+ const snapshot = input.budgetTracker.snapshot();
194544
+ await input.trace.write({
194545
+ type: "review_budget_planned",
194546
+ traceId: input.traceId,
194547
+ requestFingerprint: input.requestFingerprint,
194548
+ maxModelCalls: snapshot.executionBudget.maxModelCalls,
194549
+ maxTotalWallTimeMs: snapshot.executionBudget.wallTime.limitMs,
194550
+ maxAgentOutputBytes: snapshot.executionBudget.maxAgentOutputBytes,
194551
+ maxFindingsPerAgent: snapshot.executionBudget.maxFindingsPerAgent,
194552
+ skipOptionalPhasesWhenTokenUsageUnknown: snapshot.executionBudget.skipOptionalPhasesWhenTokenUsageUnknown,
194553
+ timestamp: new Date().toISOString()
194554
+ });
194555
+ }
194556
+ async function writeReviewBudgetCompleted(input) {
194557
+ const snapshot = input.budgetTracker.snapshot();
194558
+ await input.trace.write({
194559
+ type: "review_budget_completed",
194560
+ traceId: input.traceId,
194561
+ requestFingerprint: input.requestFingerprint,
194562
+ completion: snapshot.completion,
194563
+ modelCalls: snapshot.executionBudget.modelCalls,
194564
+ wallTime: snapshot.executionBudget.wallTime,
194565
+ tokenUsage: snapshot.executionBudget.tokenUsage,
194566
+ timestamp: new Date().toISOString()
194567
+ });
194568
+ }
193354
194569
  function mergeDenyPatterns(configDeny, requestDeny) {
193355
194570
  return Array.from(new Set([...configDeny, ...requestDeny ?? []]));
193356
194571
  }