auditor-lambda 0.3.20 → 0.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -32,9 +32,11 @@ import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/r
32
32
  import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
33
33
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
34
34
  import { runAuditCodeMcpServer } from "./mcp/server.js";
35
+ import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, } from "./quota/index.js";
35
36
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
36
37
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
37
38
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
39
+ const STEP_CONTRACT_VERSION = "audit-code-step/v1alpha1";
38
40
  const LARGE_FILE_PACKET_TARGET_LINES = 2500;
39
41
  const SMALL_MODEL_HINT_MAX_LINES = 500;
40
42
  const SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS = 3000;
@@ -72,6 +74,19 @@ function getFlag(argv, name, fallback) {
72
74
  function hasFlag(argv, name) {
73
75
  return argv.includes(name);
74
76
  }
77
+ function getOptionalBooleanFlag(argv, name) {
78
+ const raw = getFlag(argv, name);
79
+ if (raw === undefined) {
80
+ return undefined;
81
+ }
82
+ if (raw === "true") {
83
+ return true;
84
+ }
85
+ if (raw === "false") {
86
+ return false;
87
+ }
88
+ throw new Error(`${name} must be either true or false.`);
89
+ }
75
90
  function toBase64Url(value) {
76
91
  return Buffer.from(value, "utf8").toString("base64url");
77
92
  }
@@ -91,6 +106,12 @@ function safeArtifactStem(value) {
91
106
  function artifactNameForId(value, extension) {
92
107
  return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
93
108
  }
109
+ function quoteCommandArg(value) {
110
+ return /[\s"]/u.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
111
+ }
112
+ function renderCommand(argv) {
113
+ return argv.map((item) => quoteCommandArg(item)).join(" ");
114
+ }
94
115
  function taskResultPath(taskResultsDir, taskId) {
95
116
  return join(taskResultsDir, artifactNameForId(taskId, "json"));
96
117
  }
@@ -158,6 +179,27 @@ function getTimeoutMs(argv, sessionConfig) {
158
179
  function getExplicitProvider(argv) {
159
180
  return getFlag(argv, "--provider");
160
181
  }
182
+ function getHostModel(argv) {
183
+ return getFlag(argv, "--host-model") ?? null;
184
+ }
185
+ function getQuotaProbeMode(argv, sessionConfig) {
186
+ const raw = getFlag(argv, "--quota-probe") ?? sessionConfig.quota?.probe ?? "auto";
187
+ if (raw === "auto" || raw === "never" || raw === "force")
188
+ return raw;
189
+ return "auto";
190
+ }
191
+ function detectRateLimitError(errorText) {
192
+ const lower = errorText.toLowerCase();
193
+ return lower.includes("429") || lower.includes("rate limit") || lower.includes("rate_limit");
194
+ }
195
+ function defaultCooldownUntil(resetAtHeader) {
196
+ if (resetAtHeader) {
197
+ const t = new Date(resetAtHeader).getTime();
198
+ if (!Number.isNaN(t))
199
+ return new Date(t).toISOString();
200
+ }
201
+ return new Date(Date.now() + 60_000).toISOString();
202
+ }
161
203
  function resolveRunProviderName(argv, sessionConfig) {
162
204
  return resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig);
163
205
  }
@@ -324,6 +366,309 @@ async function addFileLineCountHints(root, tasks) {
324
366
  file_line_counts: Object.fromEntries(task.file_paths.map((path) => [path, lineIndex[path] ?? 0])),
325
367
  }));
326
368
  }
369
+ function activeReviewRunFromTask(artifactsDir, task) {
370
+ if (task.preferred_executor !== "agent" || !task.audit_results_path) {
371
+ return null;
372
+ }
373
+ const paths = getRunPaths(artifactsDir, task.run_id);
374
+ return {
375
+ run_id: task.run_id,
376
+ task_path: paths.taskPath,
377
+ prompt_path: paths.promptPath,
378
+ pending_audit_tasks_path: task.pending_audit_tasks_path,
379
+ audit_results_path: task.audit_results_path,
380
+ worker_command: task.worker_command,
381
+ };
382
+ }
383
+ async function loadCurrentActiveReviewRun(artifactsDir) {
384
+ try {
385
+ const task = await readJsonFile(join(artifactsDir, "dispatch", "current-task.json"));
386
+ return activeReviewRunFromTask(artifactsDir, task);
387
+ }
388
+ catch (error) {
389
+ if (isFileMissingError(error)) {
390
+ return null;
391
+ }
392
+ throw error;
393
+ }
394
+ }
395
+ async function writeHandoffOnly(params) {
396
+ const handoff = buildAuditCodeHandoff({
397
+ root: params.root,
398
+ artifactsDir: params.artifactsDir,
399
+ state: params.audit_state,
400
+ bundle: params.bundle,
401
+ providerName: params.providerName,
402
+ progressSummary: params.progress_summary,
403
+ isConfigError: params.isConfigError,
404
+ activeReviewRun: params.activeReviewRun,
405
+ });
406
+ await writeAuditCodeHandoffArtifacts(handoff);
407
+ }
408
+ async function ensureSemanticReviewRun(params) {
409
+ const existingRun = await loadCurrentActiveReviewRun(params.artifactsDir);
410
+ if (existingRun) {
411
+ const blockedState = params.bundle.audit_state?.status === "blocked"
412
+ ? params.bundle.audit_state
413
+ : buildBlockedAuditState({
414
+ state: params.state,
415
+ obligationId: params.obligationId,
416
+ executor: "agent",
417
+ blocker: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
418
+ });
419
+ const blockedBundle = { ...params.bundle, audit_state: blockedState };
420
+ await writeCoreArtifacts(params.artifactsDir, blockedBundle);
421
+ await writeHandoffOnly({
422
+ root: params.root,
423
+ artifactsDir: params.artifactsDir,
424
+ bundle: blockedBundle,
425
+ audit_state: blockedState,
426
+ progress_summary: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
427
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
428
+ activeReviewRun: existingRun,
429
+ });
430
+ return {
431
+ state: blockedState,
432
+ bundle: blockedBundle,
433
+ activeReviewRun: existingRun,
434
+ };
435
+ }
436
+ const blockedState = buildBlockedAuditState({
437
+ state: params.state,
438
+ obligationId: params.obligationId,
439
+ executor: "agent",
440
+ blocker: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
441
+ });
442
+ await writeCoreArtifacts(params.artifactsDir, {
443
+ ...params.bundle,
444
+ audit_state: blockedState,
445
+ });
446
+ const runId = buildRunId(params.obligationId, 1);
447
+ const paths = getRunPaths(params.artifactsDir, runId);
448
+ const pendingTasks = await addFileLineCountHints(params.root, buildPendingAuditTasks(params.bundle));
449
+ const pendingTasksPath = join(paths.runDir, "pending-audit-tasks.json");
450
+ const auditResultsPath = join(paths.runDir, "audit-results.json");
451
+ const task = {
452
+ contract_version: "audit-code-worker/v1alpha1",
453
+ run_id: runId,
454
+ repo_root: params.root,
455
+ artifacts_dir: params.artifactsDir,
456
+ obligation_id: params.obligationId,
457
+ preferred_executor: "agent",
458
+ result_path: paths.resultPath,
459
+ worker_command: [
460
+ process.execPath,
461
+ params.selfCliPath,
462
+ "worker-run",
463
+ "--task",
464
+ paths.taskPath,
465
+ ],
466
+ audit_results_path: auditResultsPath,
467
+ pending_audit_tasks_path: pendingTasksPath,
468
+ timeout_ms: params.timeoutMs,
469
+ max_retries: 0,
470
+ };
471
+ const prompt = renderWorkerPrompt(task);
472
+ await writeWorkerTaskFiles(task, prompt, paths, params.artifactsDir, pendingTasks);
473
+ await writeJsonFile(pendingTasksPath, pendingTasks);
474
+ const activeReviewRun = activeReviewRunFromTask(params.artifactsDir, task);
475
+ if (!activeReviewRun) {
476
+ throw new Error("Internal error: failed to materialize active review run.");
477
+ }
478
+ const blockedBundle = {
479
+ ...params.bundle,
480
+ audit_state: blockedState,
481
+ };
482
+ await writeHandoffOnly({
483
+ root: params.root,
484
+ artifactsDir: params.artifactsDir,
485
+ bundle: blockedBundle,
486
+ audit_state: blockedState,
487
+ progress_summary: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
488
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
489
+ activeReviewRun,
490
+ });
491
+ return { state: blockedState, bundle: blockedBundle, activeReviewRun };
492
+ }
493
+ function nextStepCommand(root, artifactsDir, extraArgs = []) {
494
+ return renderCommand([
495
+ "audit-code",
496
+ "next-step",
497
+ "--root",
498
+ root,
499
+ "--artifacts-dir",
500
+ artifactsDir,
501
+ ...extraArgs,
502
+ ]);
503
+ }
504
+ function mergeAndIngestCommand(artifactsDir, runId) {
505
+ return renderCommand([
506
+ "audit-code",
507
+ "merge-and-ingest",
508
+ "--run-id",
509
+ runId,
510
+ "--artifacts-dir",
511
+ artifactsDir,
512
+ ]);
513
+ }
514
+ function renderCapabilityCheckPrompt(params) {
515
+ const yesCommand = nextStepCommand(params.root, params.artifactsDir, [
516
+ "--host-can-dispatch-subagents",
517
+ "true",
518
+ ]);
519
+ const noCommand = nextStepCommand(params.root, params.artifactsDir, [
520
+ "--host-can-dispatch-subagents",
521
+ "false",
522
+ ]);
523
+ return [
524
+ "# audit-code capability check",
525
+ "",
526
+ "Decide one thing from the active toolset: does this host expose a callable subagent/delegation tool for source-code review, such as Agent, Task, or an equivalent built-in subagent call?",
527
+ "",
528
+ "Do not run shell commands to answer this. Do not inspect packet prompts, schemas, or backend command catalogs.",
529
+ "",
530
+ "If callable subagents are available, run:",
531
+ "",
532
+ ` ${yesCommand}`,
533
+ "",
534
+ "If callable subagents are not available, run:",
535
+ "",
536
+ ` ${noCommand}`,
537
+ "",
538
+ "If the host can also restrict tools per subagent or select models per subagent, add the matching `--host-can-restrict-subagent-tools true` or `--host-can-select-subagent-model true` flags to the same command. Omit those flags when unsure.",
539
+ "",
540
+ "After the command writes the next step, read and follow only its `prompt_path`.",
541
+ "",
542
+ ].join("\n");
543
+ }
544
+ function renderDispatchReviewPrompt(params) {
545
+ const mergeCommand = mergeAndIngestCommand(params.artifactsDir, params.activeReviewRun.run_id);
546
+ const continueCommand = nextStepCommand(params.root, params.artifactsDir);
547
+ const modelLine = params.hostCanSelectSubagentModel
548
+ ? "When launching each subagent, map `entry.model_hint.tier` (`small`, `standard`, `deep`) to an available host model without asking the user for model names."
549
+ : "Ignore `entry.model_hint`; this host did not report per-subagent model selection.";
550
+ const toolsLine = params.hostCanRestrictSubagentTools
551
+ ? "Restrict review subagents to read/search plus the packet submit command named in their prompt. Do not give them source edit/write tools."
552
+ : "Do not ask the user about per-subagent tool restrictions; this host did not report a callable restriction facility.";
553
+ const fileLines = params.dispatchQuotaPath
554
+ ? [
555
+ "Dispatch is prepared. Read both of these files:",
556
+ "",
557
+ ` Dispatch plan: ${params.dispatchPlanPath}`,
558
+ ` Dispatch quota: ${params.dispatchQuotaPath}`,
559
+ "",
560
+ "The quota file contains a `wave_size` field. Dispatch at most `wave_size` subagents at a time. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
561
+ "",
562
+ "For each wave: launch up to `wave_size` subagents in parallel (one per plan entry), wait for all of them to finish, then start the next wave. Repeat until all entries are dispatched.",
563
+ ]
564
+ : [
565
+ "Dispatch is prepared. Read only this dispatch plan JSON:",
566
+ "",
567
+ ` ${params.dispatchPlanPath}`,
568
+ "",
569
+ "Launch one host subagent for each entry in the plan.",
570
+ ];
571
+ return [
572
+ "# audit-code dispatch review",
573
+ "",
574
+ ...fileLines,
575
+ "",
576
+ "Pass each packet prompt path literally to its subagent; do not load packet prompt files into this orchestrator context.",
577
+ "",
578
+ "Subagent prompt shape:",
579
+ "",
580
+ ' Read and follow the audit instructions in: <entry.prompt_path>',
581
+ "",
582
+ modelLine,
583
+ toolsLine,
584
+ "",
585
+ "Each subagent must submit its packet through the submit command printed in its packet prompt and stop after successful submission.",
586
+ "",
587
+ "After all waves complete, run exactly:",
588
+ "",
589
+ ` ${mergeCommand}`,
590
+ "",
591
+ "If merge-and-ingest fails, stop and report the exact command and error output. Do not manually merge results or edit audit state.",
592
+ "",
593
+ "If merge-and-ingest succeeds, run:",
594
+ "",
595
+ ` ${continueCommand}`,
596
+ "",
597
+ "Read and follow only the new step prompt path returned by that command.",
598
+ "",
599
+ ].join("\n");
600
+ }
601
+ function renderSingleTaskFallbackStepPrompt(params) {
602
+ return [
603
+ "# audit-code single-task fallback step",
604
+ "",
605
+ "Use this step only because the host reported no callable subagent facility.",
606
+ "",
607
+ "Read and follow exactly this generated single-task prompt:",
608
+ "",
609
+ ` ${params.singleTaskPromptPath}`,
610
+ "",
611
+ "Complete exactly one AuditResult for the task named there, write the JSON array to the prompt's audit_results_path, run the exact worker_command from that prompt, then stop.",
612
+ "",
613
+ "Do not run dispatch commands, do not prepare packets, do not run next-step again in this turn, and do not read a report after the worker command.",
614
+ "",
615
+ "The only backend command allowed after writing the result is:",
616
+ "",
617
+ ` ${renderCommand(params.activeReviewRun.worker_command)}`,
618
+ "",
619
+ ].join("\n");
620
+ }
621
+ function renderPresentReportPrompt(finalReportPath) {
622
+ return [
623
+ "# audit-code present report",
624
+ "",
625
+ "The deterministic audit is complete.",
626
+ "",
627
+ "Read this report and present the completed audit with work blocks first:",
628
+ "",
629
+ ` ${finalReportPath}`,
630
+ "",
631
+ "Do not run the orchestrator again for this completed audit.",
632
+ "",
633
+ ].join("\n");
634
+ }
635
+ function renderBlockedStepPrompt(reason) {
636
+ return [
637
+ "# audit-code blocked",
638
+ "",
639
+ "The audit cannot continue automatically from this step.",
640
+ "",
641
+ "Report this blocker verbatim and stop:",
642
+ "",
643
+ reason,
644
+ "",
645
+ ].join("\n");
646
+ }
647
+ async function writeCurrentStep(params) {
648
+ const stepsDir = join(params.artifactsDir, "steps");
649
+ await mkdir(stepsDir, { recursive: true });
650
+ const promptPath = join(stepsDir, "current-prompt.md");
651
+ const stepPath = join(stepsDir, "current-step.json");
652
+ await writeFile(promptPath, params.prompt, "utf8");
653
+ const step = {
654
+ contract_version: STEP_CONTRACT_VERSION,
655
+ step_kind: params.stepKind,
656
+ prompt_path: promptPath,
657
+ status: params.status,
658
+ run_id: params.runId,
659
+ allowed_commands: params.allowedCommands,
660
+ stop_condition: params.stopCondition,
661
+ repo_root: params.repoRoot,
662
+ artifacts_dir: params.artifactsDir,
663
+ artifact_paths: {
664
+ current_step: stepPath,
665
+ current_prompt: promptPath,
666
+ ...params.artifactPaths,
667
+ },
668
+ };
669
+ await writeJsonFile(stepPath, step);
670
+ return step;
671
+ }
327
672
  function formatAuditResultValidationError(issues) {
328
673
  return (`audit-results validation failed with ${issues.length} error(s):\n` +
329
674
  formatAuditResultIssues(issues));
@@ -659,6 +1004,255 @@ async function cmdAdvanceAudit(argv) {
659
1004
  await promoteFinalAuditReport({ artifactsDir, repoRoot: root });
660
1005
  }
661
1006
  }
1007
+ async function runDeterministicForNextStep(params) {
1008
+ let lastSummary = "";
1009
+ for (let index = 0; index < params.maxRuns; index++) {
1010
+ const bundle = await loadArtifactBundle(params.artifactsDir);
1011
+ const decision = decideNextStep(bundle);
1012
+ const state = decision.state;
1013
+ if (state.status === "complete") {
1014
+ await writeHandoffOnly({
1015
+ root: params.root,
1016
+ artifactsDir: params.artifactsDir,
1017
+ bundle,
1018
+ audit_state: state,
1019
+ progress_summary: decision.reason,
1020
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
1021
+ });
1022
+ const promoted = await promoteFinalAuditReport({
1023
+ artifactsDir: params.artifactsDir,
1024
+ repoRoot: params.root,
1025
+ });
1026
+ return {
1027
+ kind: "complete",
1028
+ state,
1029
+ bundle,
1030
+ finalReportPath: promoted.promoted
1031
+ ? join(params.root, "audit-report.md")
1032
+ : join(params.artifactsDir, "audit-report.md"),
1033
+ };
1034
+ }
1035
+ if (decision.selected_executor === "agent") {
1036
+ return {
1037
+ kind: "semantic_review",
1038
+ ...(await ensureSemanticReviewRun({
1039
+ root: params.root,
1040
+ artifactsDir: params.artifactsDir,
1041
+ bundle,
1042
+ state,
1043
+ obligationId: decision.selected_obligation,
1044
+ selfCliPath: params.selfCliPath,
1045
+ timeoutMs: params.timeoutMs,
1046
+ })),
1047
+ };
1048
+ }
1049
+ if (!decision.selected_executor) {
1050
+ await writeHandoffOnly({
1051
+ root: params.root,
1052
+ artifactsDir: params.artifactsDir,
1053
+ bundle,
1054
+ audit_state: state,
1055
+ progress_summary: lastSummary || decision.reason,
1056
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
1057
+ });
1058
+ return {
1059
+ kind: "blocked",
1060
+ state,
1061
+ bundle,
1062
+ reason: lastSummary || decision.reason,
1063
+ };
1064
+ }
1065
+ const result = await runAuditStep({
1066
+ root: params.root,
1067
+ artifactsDir: params.artifactsDir,
1068
+ });
1069
+ lastSummary = result.progress_summary;
1070
+ if (result.selected_executor !== "agent") {
1071
+ await clearDispatchFiles(params.artifactsDir);
1072
+ }
1073
+ if (!result.progress_made) {
1074
+ return {
1075
+ kind: "blocked",
1076
+ state: result.audit_state,
1077
+ bundle: result.updated_bundle,
1078
+ reason: result.progress_summary,
1079
+ };
1080
+ }
1081
+ }
1082
+ const bundle = await loadArtifactBundle(params.artifactsDir);
1083
+ const state = deriveAuditState(bundle);
1084
+ return {
1085
+ kind: "blocked",
1086
+ state,
1087
+ bundle,
1088
+ reason: `Reached max run limit (${params.maxRuns}) before a review, report, or blocker step was ready.`,
1089
+ };
1090
+ }
1091
+ async function cmdNextStep(argv) {
1092
+ const root = getRootDir(argv);
1093
+ const artifactsDir = getArtifactsDir(argv);
1094
+ await mkdir(artifactsDir, { recursive: true });
1095
+ await ensureSupervisorDirs(artifactsDir);
1096
+ const hostCanDispatchSubagents = getOptionalBooleanFlag(argv, "--host-can-dispatch-subagents");
1097
+ const hostCanRestrictSubagentTools = getOptionalBooleanFlag(argv, "--host-can-restrict-subagent-tools") ??
1098
+ false;
1099
+ const hostCanSelectSubagentModel = getOptionalBooleanFlag(argv, "--host-can-select-subagent-model") ?? false;
1100
+ let sessionConfig;
1101
+ try {
1102
+ sessionConfig = await loadSessionConfig(artifactsDir);
1103
+ }
1104
+ catch (error) {
1105
+ const reason = error instanceof Error ? error.message : String(error);
1106
+ await persistConfigErrorHandoff({
1107
+ root,
1108
+ artifactsDir,
1109
+ progressSummary: reason,
1110
+ });
1111
+ const step = await writeCurrentStep({
1112
+ artifactsDir,
1113
+ stepKind: "blocked",
1114
+ status: "blocked",
1115
+ runId: null,
1116
+ allowedCommands: [],
1117
+ stopCondition: "Report the configuration blocker and stop.",
1118
+ repoRoot: root,
1119
+ artifactPaths: {
1120
+ operator_handoff: join(artifactsDir, "operator-handoff.json"),
1121
+ },
1122
+ prompt: renderBlockedStepPrompt(reason),
1123
+ });
1124
+ console.log(JSON.stringify(step, null, 2));
1125
+ return;
1126
+ }
1127
+ const result = await runDeterministicForNextStep({
1128
+ root,
1129
+ artifactsDir,
1130
+ selfCliPath: resolve(argv[1] ?? process.argv[1] ?? ""),
1131
+ timeoutMs: getTimeoutMs(argv, sessionConfig),
1132
+ maxRuns: getMaxRuns(argv),
1133
+ });
1134
+ if (result.kind === "complete") {
1135
+ const step = await writeCurrentStep({
1136
+ artifactsDir,
1137
+ stepKind: "present_report",
1138
+ status: "complete",
1139
+ runId: null,
1140
+ allowedCommands: [],
1141
+ stopCondition: "Present the final report and stop.",
1142
+ repoRoot: root,
1143
+ artifactPaths: {
1144
+ final_report: result.finalReportPath,
1145
+ },
1146
+ prompt: renderPresentReportPrompt(result.finalReportPath),
1147
+ });
1148
+ console.log(JSON.stringify(step, null, 2));
1149
+ return;
1150
+ }
1151
+ if (result.kind === "blocked") {
1152
+ const step = await writeCurrentStep({
1153
+ artifactsDir,
1154
+ stepKind: "blocked",
1155
+ status: "blocked",
1156
+ runId: null,
1157
+ allowedCommands: [],
1158
+ stopCondition: "Report the blocker and stop.",
1159
+ repoRoot: root,
1160
+ artifactPaths: {
1161
+ operator_handoff: join(artifactsDir, "operator-handoff.json"),
1162
+ },
1163
+ prompt: renderBlockedStepPrompt(result.reason),
1164
+ });
1165
+ console.log(JSON.stringify(step, null, 2));
1166
+ return;
1167
+ }
1168
+ if (hostCanDispatchSubagents === undefined) {
1169
+ const yesCommand = nextStepCommand(root, artifactsDir, [
1170
+ "--host-can-dispatch-subagents",
1171
+ "true",
1172
+ ]);
1173
+ const noCommand = nextStepCommand(root, artifactsDir, [
1174
+ "--host-can-dispatch-subagents",
1175
+ "false",
1176
+ ]);
1177
+ const step = await writeCurrentStep({
1178
+ artifactsDir,
1179
+ stepKind: "capability_check",
1180
+ status: "ready",
1181
+ runId: result.activeReviewRun.run_id,
1182
+ allowedCommands: [yesCommand, noCommand],
1183
+ stopCondition: "Run exactly one next-step command with an explicit host dispatch capability.",
1184
+ repoRoot: root,
1185
+ artifactPaths: {
1186
+ active_review_task: result.activeReviewRun.task_path,
1187
+ active_review_prompt: result.activeReviewRun.prompt_path,
1188
+ pending_audit_tasks: result.activeReviewRun.pending_audit_tasks_path ?? null,
1189
+ single_task_prompt: join(artifactsDir, "dispatch", "current-single-task-prompt.md"),
1190
+ },
1191
+ prompt: renderCapabilityCheckPrompt({ root, artifactsDir }),
1192
+ });
1193
+ console.log(JSON.stringify(step, null, 2));
1194
+ return;
1195
+ }
1196
+ if (!hostCanDispatchSubagents) {
1197
+ const singleTaskPromptPath = join(artifactsDir, "dispatch", "current-single-task-prompt.md");
1198
+ const workerCommand = renderCommand(result.activeReviewRun.worker_command);
1199
+ const step = await writeCurrentStep({
1200
+ artifactsDir,
1201
+ stepKind: "single_task_fallback",
1202
+ status: "ready",
1203
+ runId: result.activeReviewRun.run_id,
1204
+ allowedCommands: [workerCommand],
1205
+ stopCondition: "Run the exact worker_command after one result, then stop without looping.",
1206
+ repoRoot: root,
1207
+ artifactPaths: {
1208
+ active_review_task: result.activeReviewRun.task_path,
1209
+ active_review_prompt: result.activeReviewRun.prompt_path,
1210
+ pending_audit_tasks: result.activeReviewRun.pending_audit_tasks_path ?? null,
1211
+ audit_results: result.activeReviewRun.audit_results_path,
1212
+ single_task_prompt: singleTaskPromptPath,
1213
+ },
1214
+ prompt: renderSingleTaskFallbackStepPrompt({
1215
+ singleTaskPromptPath,
1216
+ activeReviewRun: result.activeReviewRun,
1217
+ }),
1218
+ });
1219
+ console.log(JSON.stringify(step, null, 2));
1220
+ return;
1221
+ }
1222
+ const dispatch = await prepareDispatchArtifacts({
1223
+ runId: result.activeReviewRun.run_id,
1224
+ artifactsDir,
1225
+ root,
1226
+ });
1227
+ const mergeCommand = mergeAndIngestCommand(artifactsDir, result.activeReviewRun.run_id);
1228
+ const continueCommand = nextStepCommand(root, artifactsDir);
1229
+ const step = await writeCurrentStep({
1230
+ artifactsDir,
1231
+ stepKind: "dispatch_review",
1232
+ status: "ready",
1233
+ runId: result.activeReviewRun.run_id,
1234
+ allowedCommands: [mergeCommand, continueCommand],
1235
+ stopCondition: "Dispatch every packet, merge-and-ingest once, then run next-step again.",
1236
+ repoRoot: root,
1237
+ artifactPaths: {
1238
+ dispatch_plan: dispatch.dispatch_plan_path,
1239
+ dispatch_quota: dispatch.dispatch_quota_path,
1240
+ dispatch_warnings: dispatch.dispatch_warnings_path,
1241
+ active_review_task: result.activeReviewRun.task_path,
1242
+ pending_audit_tasks: result.activeReviewRun.pending_audit_tasks_path ?? null,
1243
+ },
1244
+ prompt: renderDispatchReviewPrompt({
1245
+ root,
1246
+ artifactsDir,
1247
+ activeReviewRun: result.activeReviewRun,
1248
+ dispatchPlanPath: dispatch.dispatch_plan_path,
1249
+ dispatchQuotaPath: dispatch.dispatch_quota_path,
1250
+ hostCanRestrictSubagentTools,
1251
+ hostCanSelectSubagentModel,
1252
+ }),
1253
+ });
1254
+ console.log(JSON.stringify(step, null, 2));
1255
+ }
662
1256
  async function cmdRunToCompletion(argv) {
663
1257
  const root = getRootDir(argv);
664
1258
  const artifactsDir = getArtifactsDir(argv);
@@ -684,6 +1278,7 @@ async function cmdRunToCompletion(argv) {
684
1278
  const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
685
1279
  const parallelWorkers = getParallelWorkers(argv, sessionConfig);
686
1280
  const timeoutMs = getTimeoutMs(argv, sessionConfig);
1281
+ const hostModel = getHostModel(argv);
687
1282
  const selfCliPath = resolve(argv[1] ?? process.argv[1] ?? "");
688
1283
  const batchResultsDir = getBatchResultsDir(argv);
689
1284
  if (batchResultsDir && getFlag(argv, "--results")) {
@@ -821,8 +1416,27 @@ async function cmdRunToCompletion(argv) {
821
1416
  return;
822
1417
  }
823
1418
  if (preferredExecutor === "agent" && parallelWorkers > 1) {
1419
+ const quotaState = await readQuotaState();
1420
+ const providerModelKey = buildProviderModelKey(provider.name, hostModel);
1421
+ const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
1422
+ const waveSchedule = scheduleWave({
1423
+ providerName: resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig),
1424
+ sessionConfig,
1425
+ hostModel,
1426
+ requestedConcurrency: parallelWorkers,
1427
+ quotaStateEntry,
1428
+ });
1429
+ const waveSize = waveSchedule.wave_size;
1430
+ if (waveSchedule.cooldown_until) {
1431
+ const waitMs = new Date(waveSchedule.cooldown_until).getTime() - Date.now();
1432
+ if (waitMs > 0) {
1433
+ const cappedWait = Math.min(waitMs, 120_000);
1434
+ process.stderr.write(`[quota] Cooldown active — waiting ${Math.ceil(cappedWait / 1000)}s before next wave.\n`);
1435
+ await new Promise((r) => setTimeout(r, cappedWait));
1436
+ }
1437
+ }
824
1438
  const allPendingTasks = buildPendingAuditTasks(bundle);
825
- const taskGroups = chunkArray(allPendingTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
1439
+ const taskGroups = chunkArray(allPendingTasks.slice(0, waveSize * agentBatchSize), agentBatchSize);
826
1440
  const workerSlots = [];
827
1441
  for (const rawGroup of taskGroups) {
828
1442
  const group = await addFileLineCountHints(root, rawGroup);
@@ -976,6 +1590,16 @@ async function cmdRunToCompletion(argv) {
976
1590
  });
977
1591
  artifactsWritten.add("run-ledger.json");
978
1592
  }
1593
+ // Record outcome for adaptive learning (best-effort — never blocks dispatch)
1594
+ {
1595
+ const hasRateLimit = batchErrors.some(detectRateLimitError);
1596
+ await recordWaveOutcome(providerModelKey, {
1597
+ concurrency: workerSlots.length,
1598
+ estimated_tokens: waveSize * agentBatchSize * 900,
1599
+ outcome: hasRateLimit ? "rate_limited" : batchErrors.length > 0 ? "timeout" : "success",
1600
+ cooldown_until: hasRateLimit ? defaultCooldownUntil(null) : null,
1601
+ }, sessionConfig.quota?.empirical_half_life_hours ?? 24).catch(() => undefined);
1602
+ }
979
1603
  if (batchErrors.length > 0) {
980
1604
  const bundleAfter = await loadArtifactBundle(artifactsDir);
981
1605
  const blockedState = buildBlockedAuditState({
@@ -1544,17 +2168,14 @@ function renderPacketGraphContext(packet) {
1544
2168
  lines.push("");
1545
2169
  return lines;
1546
2170
  }
1547
- async function cmdPrepareDispatch(argv) {
1548
- const runId = getFlag(argv, "--run-id");
1549
- if (!runId)
1550
- throw new Error("prepare-dispatch requires --run-id <run_id>");
1551
- const artifactsDir = getArtifactsDir(argv);
2171
+ async function prepareDispatchArtifacts(params) {
2172
+ const runId = params.runId;
2173
+ const artifactsDir = params.artifactsDir;
1552
2174
  const runDir = join(artifactsDir, "runs", runId);
1553
2175
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1554
2176
  const taskResultsDir = join(runDir, "task-results");
1555
2177
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1556
- const explicitRoot = getFlag(argv, "--root") ? getRootDir(argv) : undefined;
1557
- let reviewRoot = explicitRoot;
2178
+ let reviewRoot = params.root;
1558
2179
  try {
1559
2180
  const workerTask = await readJsonFile(join(runDir, "task.json"));
1560
2181
  reviewRoot ??= workerTask.repo_root;
@@ -1566,6 +2187,7 @@ async function cmdPrepareDispatch(argv) {
1566
2187
  }
1567
2188
  const tasks = await readJsonFile(tasksPath);
1568
2189
  const bundle = await loadArtifactBundle(artifactsDir);
2190
+ const sessionConfig = params.sessionConfig ?? (await loadSessionConfig(artifactsDir).catch(() => ({})));
1569
2191
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
1570
2192
  const lensDefs = await readJsonFile(lensDefsPath);
1571
2193
  await mkdir(taskResultsDir, { recursive: true });
@@ -1721,6 +2343,7 @@ async function cmdPrepareDispatch(argv) {
1721
2343
  largeFileMode
1722
2344
  ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
1723
2345
  : "Use your Read tool. Paths are repo-relative from the current working directory.",
2346
+ "Prefer host Read/Grep tools. On native Windows, do not use Unix pipelines like `grep ... | head`; if shell search is unavoidable, use `Select-String` as a fallback.",
1724
2347
  fileList,
1725
2348
  "",
1726
2349
  ...renderPacketGraphContext(packet),
@@ -1790,15 +2413,62 @@ async function cmdPrepareDispatch(argv) {
1790
2413
  run_id: runId,
1791
2414
  entries: resultMapEntries,
1792
2415
  });
2416
+ // Compute and write dispatch-quota.json
2417
+ const hostModel = params.hostModel ?? null;
2418
+ const avgPacketTokens = plan.length > 0
2419
+ ? Math.floor(plan.reduce((s, p) => s + p.complexity.estimated_tokens, 0) / plan.length)
2420
+ : 0;
2421
+ const quotaProviderName = resolveFreshSessionProviderName(undefined, sessionConfig);
2422
+ const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
2423
+ const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
2424
+ const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
2425
+ const waveSchedule = scheduleWave({
2426
+ providerName: quotaProviderName,
2427
+ sessionConfig,
2428
+ hostModel,
2429
+ requestedConcurrency: sessionConfig.parallel_workers ?? 1,
2430
+ estimatedPacketTokens: avgPacketTokens,
2431
+ quotaStateEntry,
2432
+ });
2433
+ const dispatchQuota = {
2434
+ contract_version: "audit-code-dispatch-quota/v1alpha1",
2435
+ run_id: runId,
2436
+ model: hostModel,
2437
+ resolved_limits: waveSchedule.resolved_limits,
2438
+ confidence: waveSchedule.confidence,
2439
+ source: waveSchedule.source,
2440
+ wave_size: waveSchedule.wave_size,
2441
+ estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
2442
+ cooldown_until: waveSchedule.cooldown_until,
2443
+ };
2444
+ const dispatchQuotaPath = join(runDir, "dispatch-quota.json");
2445
+ await writeJsonFile(dispatchQuotaPath, dispatchQuota);
2446
+ // Warn about packets that exceed the context budget only when we have reliable limit
2447
+ // information (confidence medium/high). Low-confidence limits are conservative defaults
2448
+ // and would produce misleading warnings since the real context window is unknown.
2449
+ if (waveSchedule.confidence !== "low") {
2450
+ const contextBudget = waveSchedule.resolved_limits.context_tokens - waveSchedule.resolved_limits.output_tokens;
2451
+ for (const p of plan) {
2452
+ if (p.complexity.estimated_tokens > contextBudget) {
2453
+ warnings.push({
2454
+ code: "oversized_packet",
2455
+ message: `Packet ${p.packet_id} estimated tokens (${p.complexity.estimated_tokens}) exceed ` +
2456
+ `context budget (${contextBudget}). This packet may fail at dispatch. ` +
2457
+ `Set quota.default_context_tokens or quota.models in session-config.json to override.`,
2458
+ });
2459
+ }
2460
+ }
2461
+ }
1793
2462
  const warningsPath = warnings.length > 0
1794
2463
  ? join(runDir, "dispatch-warnings.json")
1795
2464
  : null;
1796
2465
  if (warningsPath) {
1797
2466
  await writeJsonFile(warningsPath, warnings);
1798
2467
  }
1799
- console.log(JSON.stringify({
2468
+ return {
1800
2469
  run_id: runId,
1801
2470
  dispatch_plan_path: dispatchPlanPath,
2471
+ dispatch_quota_path: dispatchQuotaPath,
1802
2472
  packet_count: plan.length,
1803
2473
  task_count: orderedTasks.length,
1804
2474
  largest_packet: largestPacketId
@@ -1810,7 +2480,19 @@ async function cmdPrepareDispatch(argv) {
1810
2480
  : null,
1811
2481
  warning_count: warnings.length,
1812
2482
  dispatch_warnings_path: warningsPath,
1813
- }, null, 2));
2483
+ };
2484
+ }
2485
+ async function cmdPrepareDispatch(argv) {
2486
+ const runId = getFlag(argv, "--run-id");
2487
+ if (!runId)
2488
+ throw new Error("prepare-dispatch requires --run-id <run_id>");
2489
+ const result = await prepareDispatchArtifacts({
2490
+ runId,
2491
+ artifactsDir: getArtifactsDir(argv),
2492
+ root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
2493
+ hostModel: getHostModel(argv),
2494
+ });
2495
+ console.log(JSON.stringify(result, null, 2));
1814
2496
  }
1815
2497
  async function cmdSubmitPacket(argv) {
1816
2498
  const runId = resolveRunScopedArg(argv, "--run-id", "--run-id-b64");
@@ -2360,6 +3042,45 @@ async function cmdCleanup(argv) {
2360
3042
  async function cmdMcp(argv) {
2361
3043
  await runAuditCodeMcpServer(argv.slice(3));
2362
3044
  }
3045
+ async function cmdQuota(argv) {
3046
+ const artifactsDir = getArtifactsDir(argv);
3047
+ const sessionConfig = await loadSessionConfig(artifactsDir).catch(() => ({}));
3048
+ const explicitProvider = getExplicitProvider(argv);
3049
+ const hostModel = getHostModel(argv);
3050
+ const probeMode = getQuotaProbeMode(argv, sessionConfig);
3051
+ const providerName = resolveFreshSessionProviderName(explicitProvider, sessionConfig);
3052
+ const providerModelKey = buildProviderModelKey(providerName, hostModel);
3053
+ const { limits, source, confidence } = resolveLimits({ providerName, sessionConfig, hostModel });
3054
+ const probeResult = await probeProvider(providerName, probeMode);
3055
+ const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
3056
+ const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
3057
+ const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ?? 24;
3058
+ const waveSchedule = scheduleWave({
3059
+ providerName,
3060
+ sessionConfig,
3061
+ hostModel,
3062
+ requestedConcurrency: sessionConfig.parallel_workers ?? 1,
3063
+ quotaStateEntry,
3064
+ });
3065
+ console.log(JSON.stringify({
3066
+ provider: providerName,
3067
+ model: hostModel,
3068
+ provider_model_key: providerModelKey,
3069
+ resolved_limits: limits,
3070
+ confidence,
3071
+ source,
3072
+ probe: probeResult,
3073
+ learned_caps: quotaStateEntry
3074
+ ? {
3075
+ max_safe_concurrency: computeMaxSafeConcurrency(quotaStateEntry, halfLifeHours),
3076
+ cooldown_until: quotaStateEntry.cooldown_until,
3077
+ last_429_at: quotaStateEntry.last_429_at,
3078
+ }
3079
+ : null,
3080
+ wave_schedule: waveSchedule,
3081
+ quota_state_path: getQuotaStatePath(),
3082
+ }, null, 2));
3083
+ }
2363
3084
  async function main(argv) {
2364
3085
  const command = argv[2] ?? "sample-run";
2365
3086
  switch (command) {
@@ -2369,6 +3090,9 @@ async function main(argv) {
2369
3090
  case "advance-audit":
2370
3091
  await cmdAdvanceAudit(argv);
2371
3092
  return;
3093
+ case "next-step":
3094
+ await cmdNextStep(argv);
3095
+ return;
2372
3096
  case "run-to-completion":
2373
3097
  await cmdRunToCompletion(argv);
2374
3098
  return;
@@ -2423,9 +3147,12 @@ async function main(argv) {
2423
3147
  case "validate-result":
2424
3148
  await cmdValidateResult(argv);
2425
3149
  return;
3150
+ case "quota":
3151
+ await cmdQuota(argv);
3152
+ return;
2426
3153
  default:
2427
3154
  console.error(`Unknown command: ${command}`);
2428
- console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result");
3155
+ console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota");
2429
3156
  process.exitCode = 1;
2430
3157
  }
2431
3158
  }