auditor-lambda 0.3.20 → 0.3.21

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
@@ -35,6 +35,7 @@ import { runAuditCodeMcpServer } from "./mcp/server.js";
35
35
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
36
36
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
37
37
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
38
+ const STEP_CONTRACT_VERSION = "audit-code-step/v1alpha1";
38
39
  const LARGE_FILE_PACKET_TARGET_LINES = 2500;
39
40
  const SMALL_MODEL_HINT_MAX_LINES = 500;
40
41
  const SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS = 3000;
@@ -72,6 +73,19 @@ function getFlag(argv, name, fallback) {
72
73
  function hasFlag(argv, name) {
73
74
  return argv.includes(name);
74
75
  }
76
+ function getOptionalBooleanFlag(argv, name) {
77
+ const raw = getFlag(argv, name);
78
+ if (raw === undefined) {
79
+ return undefined;
80
+ }
81
+ if (raw === "true") {
82
+ return true;
83
+ }
84
+ if (raw === "false") {
85
+ return false;
86
+ }
87
+ throw new Error(`${name} must be either true or false.`);
88
+ }
75
89
  function toBase64Url(value) {
76
90
  return Buffer.from(value, "utf8").toString("base64url");
77
91
  }
@@ -91,6 +105,12 @@ function safeArtifactStem(value) {
91
105
  function artifactNameForId(value, extension) {
92
106
  return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
93
107
  }
108
+ function quoteCommandArg(value) {
109
+ return /[\s"]/u.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
110
+ }
111
+ function renderCommand(argv) {
112
+ return argv.map((item) => quoteCommandArg(item)).join(" ");
113
+ }
94
114
  function taskResultPath(taskResultsDir, taskId) {
95
115
  return join(taskResultsDir, artifactNameForId(taskId, "json"));
96
116
  }
@@ -324,6 +344,293 @@ async function addFileLineCountHints(root, tasks) {
324
344
  file_line_counts: Object.fromEntries(task.file_paths.map((path) => [path, lineIndex[path] ?? 0])),
325
345
  }));
326
346
  }
347
+ function activeReviewRunFromTask(artifactsDir, task) {
348
+ if (task.preferred_executor !== "agent" || !task.audit_results_path) {
349
+ return null;
350
+ }
351
+ const paths = getRunPaths(artifactsDir, task.run_id);
352
+ return {
353
+ run_id: task.run_id,
354
+ task_path: paths.taskPath,
355
+ prompt_path: paths.promptPath,
356
+ pending_audit_tasks_path: task.pending_audit_tasks_path,
357
+ audit_results_path: task.audit_results_path,
358
+ worker_command: task.worker_command,
359
+ };
360
+ }
361
+ async function loadCurrentActiveReviewRun(artifactsDir) {
362
+ try {
363
+ const task = await readJsonFile(join(artifactsDir, "dispatch", "current-task.json"));
364
+ return activeReviewRunFromTask(artifactsDir, task);
365
+ }
366
+ catch (error) {
367
+ if (isFileMissingError(error)) {
368
+ return null;
369
+ }
370
+ throw error;
371
+ }
372
+ }
373
+ async function writeHandoffOnly(params) {
374
+ const handoff = buildAuditCodeHandoff({
375
+ root: params.root,
376
+ artifactsDir: params.artifactsDir,
377
+ state: params.audit_state,
378
+ bundle: params.bundle,
379
+ providerName: params.providerName,
380
+ progressSummary: params.progress_summary,
381
+ isConfigError: params.isConfigError,
382
+ activeReviewRun: params.activeReviewRun,
383
+ });
384
+ await writeAuditCodeHandoffArtifacts(handoff);
385
+ }
386
+ async function ensureSemanticReviewRun(params) {
387
+ const existingRun = await loadCurrentActiveReviewRun(params.artifactsDir);
388
+ if (existingRun) {
389
+ const blockedState = params.bundle.audit_state?.status === "blocked"
390
+ ? params.bundle.audit_state
391
+ : buildBlockedAuditState({
392
+ state: params.state,
393
+ obligationId: params.obligationId,
394
+ executor: "agent",
395
+ blocker: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
396
+ });
397
+ const blockedBundle = { ...params.bundle, audit_state: blockedState };
398
+ await writeCoreArtifacts(params.artifactsDir, blockedBundle);
399
+ await writeHandoffOnly({
400
+ root: params.root,
401
+ artifactsDir: params.artifactsDir,
402
+ bundle: blockedBundle,
403
+ audit_state: blockedState,
404
+ progress_summary: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
405
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
406
+ activeReviewRun: existingRun,
407
+ });
408
+ return {
409
+ state: blockedState,
410
+ bundle: blockedBundle,
411
+ activeReviewRun: existingRun,
412
+ };
413
+ }
414
+ const blockedState = buildBlockedAuditState({
415
+ state: params.state,
416
+ obligationId: params.obligationId,
417
+ executor: "agent",
418
+ blocker: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
419
+ });
420
+ await writeCoreArtifacts(params.artifactsDir, {
421
+ ...params.bundle,
422
+ audit_state: blockedState,
423
+ });
424
+ const runId = buildRunId(params.obligationId, 1);
425
+ const paths = getRunPaths(params.artifactsDir, runId);
426
+ const pendingTasks = await addFileLineCountHints(params.root, buildPendingAuditTasks(params.bundle));
427
+ const pendingTasksPath = join(paths.runDir, "pending-audit-tasks.json");
428
+ const auditResultsPath = join(paths.runDir, "audit-results.json");
429
+ const task = {
430
+ contract_version: "audit-code-worker/v1alpha1",
431
+ run_id: runId,
432
+ repo_root: params.root,
433
+ artifacts_dir: params.artifactsDir,
434
+ obligation_id: params.obligationId,
435
+ preferred_executor: "agent",
436
+ result_path: paths.resultPath,
437
+ worker_command: [
438
+ process.execPath,
439
+ params.selfCliPath,
440
+ "worker-run",
441
+ "--task",
442
+ paths.taskPath,
443
+ ],
444
+ audit_results_path: auditResultsPath,
445
+ pending_audit_tasks_path: pendingTasksPath,
446
+ timeout_ms: params.timeoutMs,
447
+ max_retries: 0,
448
+ };
449
+ const prompt = renderWorkerPrompt(task);
450
+ await writeWorkerTaskFiles(task, prompt, paths, params.artifactsDir, pendingTasks);
451
+ await writeJsonFile(pendingTasksPath, pendingTasks);
452
+ const activeReviewRun = activeReviewRunFromTask(params.artifactsDir, task);
453
+ if (!activeReviewRun) {
454
+ throw new Error("Internal error: failed to materialize active review run.");
455
+ }
456
+ const blockedBundle = {
457
+ ...params.bundle,
458
+ audit_state: blockedState,
459
+ };
460
+ await writeHandoffOnly({
461
+ root: params.root,
462
+ artifactsDir: params.artifactsDir,
463
+ bundle: blockedBundle,
464
+ audit_state: blockedState,
465
+ progress_summary: buildManualReviewBlocker(LOCAL_SUBPROCESS_PROVIDER_NAME),
466
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
467
+ activeReviewRun,
468
+ });
469
+ return { state: blockedState, bundle: blockedBundle, activeReviewRun };
470
+ }
471
+ function nextStepCommand(root, artifactsDir, extraArgs = []) {
472
+ return renderCommand([
473
+ "audit-code",
474
+ "next-step",
475
+ "--root",
476
+ root,
477
+ "--artifacts-dir",
478
+ artifactsDir,
479
+ ...extraArgs,
480
+ ]);
481
+ }
482
+ function mergeAndIngestCommand(artifactsDir, runId) {
483
+ return renderCommand([
484
+ "audit-code",
485
+ "merge-and-ingest",
486
+ "--run-id",
487
+ runId,
488
+ "--artifacts-dir",
489
+ artifactsDir,
490
+ ]);
491
+ }
492
+ function renderCapabilityCheckPrompt(params) {
493
+ const yesCommand = nextStepCommand(params.root, params.artifactsDir, [
494
+ "--host-can-dispatch-subagents",
495
+ "true",
496
+ ]);
497
+ const noCommand = nextStepCommand(params.root, params.artifactsDir, [
498
+ "--host-can-dispatch-subagents",
499
+ "false",
500
+ ]);
501
+ return [
502
+ "# audit-code capability check",
503
+ "",
504
+ "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?",
505
+ "",
506
+ "Do not run shell commands to answer this. Do not inspect packet prompts, schemas, or backend command catalogs.",
507
+ "",
508
+ "If callable subagents are available, run:",
509
+ "",
510
+ ` ${yesCommand}`,
511
+ "",
512
+ "If callable subagents are not available, run:",
513
+ "",
514
+ ` ${noCommand}`,
515
+ "",
516
+ "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.",
517
+ "",
518
+ "After the command writes the next step, read and follow only its `prompt_path`.",
519
+ "",
520
+ ].join("\n");
521
+ }
522
+ function renderDispatchReviewPrompt(params) {
523
+ const mergeCommand = mergeAndIngestCommand(params.artifactsDir, params.activeReviewRun.run_id);
524
+ const continueCommand = nextStepCommand(params.root, params.artifactsDir);
525
+ const modelLine = params.hostCanSelectSubagentModel
526
+ ? "When launching each subagent, map `entry.model_hint.tier` (`small`, `standard`, `deep`) to an available host model without asking the user for model names."
527
+ : "Ignore `entry.model_hint`; this host did not report per-subagent model selection.";
528
+ const toolsLine = params.hostCanRestrictSubagentTools
529
+ ? "Restrict review subagents to read/search plus the packet submit command named in their prompt. Do not give them source edit/write tools."
530
+ : "Do not ask the user about per-subagent tool restrictions; this host did not report a callable restriction facility.";
531
+ return [
532
+ "# audit-code dispatch review",
533
+ "",
534
+ "Dispatch is prepared. Read only this dispatch plan JSON:",
535
+ "",
536
+ ` ${params.dispatchPlanPath}`,
537
+ "",
538
+ "Launch one host subagent for each entry in the plan. Pass the packet prompt path literally; do not load packet prompt files into this orchestrator context.",
539
+ "",
540
+ "Subagent prompt shape:",
541
+ "",
542
+ ' Read and follow the audit instructions in: <entry.prompt_path>',
543
+ "",
544
+ modelLine,
545
+ toolsLine,
546
+ "",
547
+ "Wait for all review subagents to finish. Each subagent must submit its packet through the submit command printed in its packet prompt and stop after successful submission.",
548
+ "",
549
+ "Then run exactly:",
550
+ "",
551
+ ` ${mergeCommand}`,
552
+ "",
553
+ "If merge-and-ingest fails, stop and report the exact command and error output. Do not manually merge results or edit audit state.",
554
+ "",
555
+ "If merge-and-ingest succeeds, run:",
556
+ "",
557
+ ` ${continueCommand}`,
558
+ "",
559
+ "Read and follow only the new step prompt path returned by that command.",
560
+ "",
561
+ ].join("\n");
562
+ }
563
+ function renderSingleTaskFallbackStepPrompt(params) {
564
+ return [
565
+ "# audit-code single-task fallback step",
566
+ "",
567
+ "Use this step only because the host reported no callable subagent facility.",
568
+ "",
569
+ "Read and follow exactly this generated single-task prompt:",
570
+ "",
571
+ ` ${params.singleTaskPromptPath}`,
572
+ "",
573
+ "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.",
574
+ "",
575
+ "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.",
576
+ "",
577
+ "The only backend command allowed after writing the result is:",
578
+ "",
579
+ ` ${renderCommand(params.activeReviewRun.worker_command)}`,
580
+ "",
581
+ ].join("\n");
582
+ }
583
+ function renderPresentReportPrompt(finalReportPath) {
584
+ return [
585
+ "# audit-code present report",
586
+ "",
587
+ "The deterministic audit is complete.",
588
+ "",
589
+ "Read this report and present the completed audit with work blocks first:",
590
+ "",
591
+ ` ${finalReportPath}`,
592
+ "",
593
+ "Do not run the orchestrator again for this completed audit.",
594
+ "",
595
+ ].join("\n");
596
+ }
597
+ function renderBlockedStepPrompt(reason) {
598
+ return [
599
+ "# audit-code blocked",
600
+ "",
601
+ "The audit cannot continue automatically from this step.",
602
+ "",
603
+ "Report this blocker verbatim and stop:",
604
+ "",
605
+ reason,
606
+ "",
607
+ ].join("\n");
608
+ }
609
+ async function writeCurrentStep(params) {
610
+ const stepsDir = join(params.artifactsDir, "steps");
611
+ await mkdir(stepsDir, { recursive: true });
612
+ const promptPath = join(stepsDir, "current-prompt.md");
613
+ const stepPath = join(stepsDir, "current-step.json");
614
+ await writeFile(promptPath, params.prompt, "utf8");
615
+ const step = {
616
+ contract_version: STEP_CONTRACT_VERSION,
617
+ step_kind: params.stepKind,
618
+ prompt_path: promptPath,
619
+ status: params.status,
620
+ run_id: params.runId,
621
+ allowed_commands: params.allowedCommands,
622
+ stop_condition: params.stopCondition,
623
+ repo_root: params.repoRoot,
624
+ artifacts_dir: params.artifactsDir,
625
+ artifact_paths: {
626
+ current_step: stepPath,
627
+ current_prompt: promptPath,
628
+ ...params.artifactPaths,
629
+ },
630
+ };
631
+ await writeJsonFile(stepPath, step);
632
+ return step;
633
+ }
327
634
  function formatAuditResultValidationError(issues) {
328
635
  return (`audit-results validation failed with ${issues.length} error(s):\n` +
329
636
  formatAuditResultIssues(issues));
@@ -659,6 +966,253 @@ async function cmdAdvanceAudit(argv) {
659
966
  await promoteFinalAuditReport({ artifactsDir, repoRoot: root });
660
967
  }
661
968
  }
969
+ async function runDeterministicForNextStep(params) {
970
+ let lastSummary = "";
971
+ for (let index = 0; index < params.maxRuns; index++) {
972
+ const bundle = await loadArtifactBundle(params.artifactsDir);
973
+ const decision = decideNextStep(bundle);
974
+ const state = decision.state;
975
+ if (state.status === "complete") {
976
+ await writeHandoffOnly({
977
+ root: params.root,
978
+ artifactsDir: params.artifactsDir,
979
+ bundle,
980
+ audit_state: state,
981
+ progress_summary: decision.reason,
982
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
983
+ });
984
+ const promoted = await promoteFinalAuditReport({
985
+ artifactsDir: params.artifactsDir,
986
+ repoRoot: params.root,
987
+ });
988
+ return {
989
+ kind: "complete",
990
+ state,
991
+ bundle,
992
+ finalReportPath: promoted.promoted
993
+ ? join(params.root, "audit-report.md")
994
+ : join(params.artifactsDir, "audit-report.md"),
995
+ };
996
+ }
997
+ if (decision.selected_executor === "agent") {
998
+ return {
999
+ kind: "semantic_review",
1000
+ ...(await ensureSemanticReviewRun({
1001
+ root: params.root,
1002
+ artifactsDir: params.artifactsDir,
1003
+ bundle,
1004
+ state,
1005
+ obligationId: decision.selected_obligation,
1006
+ selfCliPath: params.selfCliPath,
1007
+ timeoutMs: params.timeoutMs,
1008
+ })),
1009
+ };
1010
+ }
1011
+ if (!decision.selected_executor) {
1012
+ await writeHandoffOnly({
1013
+ root: params.root,
1014
+ artifactsDir: params.artifactsDir,
1015
+ bundle,
1016
+ audit_state: state,
1017
+ progress_summary: lastSummary || decision.reason,
1018
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
1019
+ });
1020
+ return {
1021
+ kind: "blocked",
1022
+ state,
1023
+ bundle,
1024
+ reason: lastSummary || decision.reason,
1025
+ };
1026
+ }
1027
+ const result = await runAuditStep({
1028
+ root: params.root,
1029
+ artifactsDir: params.artifactsDir,
1030
+ });
1031
+ lastSummary = result.progress_summary;
1032
+ if (result.selected_executor !== "agent") {
1033
+ await clearDispatchFiles(params.artifactsDir);
1034
+ }
1035
+ if (!result.progress_made) {
1036
+ return {
1037
+ kind: "blocked",
1038
+ state: result.audit_state,
1039
+ bundle: result.updated_bundle,
1040
+ reason: result.progress_summary,
1041
+ };
1042
+ }
1043
+ }
1044
+ const bundle = await loadArtifactBundle(params.artifactsDir);
1045
+ const state = deriveAuditState(bundle);
1046
+ return {
1047
+ kind: "blocked",
1048
+ state,
1049
+ bundle,
1050
+ reason: `Reached max run limit (${params.maxRuns}) before a review, report, or blocker step was ready.`,
1051
+ };
1052
+ }
1053
+ async function cmdNextStep(argv) {
1054
+ const root = getRootDir(argv);
1055
+ const artifactsDir = getArtifactsDir(argv);
1056
+ await mkdir(artifactsDir, { recursive: true });
1057
+ await ensureSupervisorDirs(artifactsDir);
1058
+ const hostCanDispatchSubagents = getOptionalBooleanFlag(argv, "--host-can-dispatch-subagents");
1059
+ const hostCanRestrictSubagentTools = getOptionalBooleanFlag(argv, "--host-can-restrict-subagent-tools") ??
1060
+ false;
1061
+ const hostCanSelectSubagentModel = getOptionalBooleanFlag(argv, "--host-can-select-subagent-model") ?? false;
1062
+ let sessionConfig;
1063
+ try {
1064
+ sessionConfig = await loadSessionConfig(artifactsDir);
1065
+ }
1066
+ catch (error) {
1067
+ const reason = error instanceof Error ? error.message : String(error);
1068
+ await persistConfigErrorHandoff({
1069
+ root,
1070
+ artifactsDir,
1071
+ progressSummary: reason,
1072
+ });
1073
+ const step = await writeCurrentStep({
1074
+ artifactsDir,
1075
+ stepKind: "blocked",
1076
+ status: "blocked",
1077
+ runId: null,
1078
+ allowedCommands: [],
1079
+ stopCondition: "Report the configuration blocker and stop.",
1080
+ repoRoot: root,
1081
+ artifactPaths: {
1082
+ operator_handoff: join(artifactsDir, "operator-handoff.json"),
1083
+ },
1084
+ prompt: renderBlockedStepPrompt(reason),
1085
+ });
1086
+ console.log(JSON.stringify(step, null, 2));
1087
+ return;
1088
+ }
1089
+ const result = await runDeterministicForNextStep({
1090
+ root,
1091
+ artifactsDir,
1092
+ selfCliPath: resolve(argv[1] ?? process.argv[1] ?? ""),
1093
+ timeoutMs: getTimeoutMs(argv, sessionConfig),
1094
+ maxRuns: getMaxRuns(argv),
1095
+ });
1096
+ if (result.kind === "complete") {
1097
+ const step = await writeCurrentStep({
1098
+ artifactsDir,
1099
+ stepKind: "present_report",
1100
+ status: "complete",
1101
+ runId: null,
1102
+ allowedCommands: [],
1103
+ stopCondition: "Present the final report and stop.",
1104
+ repoRoot: root,
1105
+ artifactPaths: {
1106
+ final_report: result.finalReportPath,
1107
+ },
1108
+ prompt: renderPresentReportPrompt(result.finalReportPath),
1109
+ });
1110
+ console.log(JSON.stringify(step, null, 2));
1111
+ return;
1112
+ }
1113
+ if (result.kind === "blocked") {
1114
+ const step = await writeCurrentStep({
1115
+ artifactsDir,
1116
+ stepKind: "blocked",
1117
+ status: "blocked",
1118
+ runId: null,
1119
+ allowedCommands: [],
1120
+ stopCondition: "Report the blocker and stop.",
1121
+ repoRoot: root,
1122
+ artifactPaths: {
1123
+ operator_handoff: join(artifactsDir, "operator-handoff.json"),
1124
+ },
1125
+ prompt: renderBlockedStepPrompt(result.reason),
1126
+ });
1127
+ console.log(JSON.stringify(step, null, 2));
1128
+ return;
1129
+ }
1130
+ if (hostCanDispatchSubagents === undefined) {
1131
+ const yesCommand = nextStepCommand(root, artifactsDir, [
1132
+ "--host-can-dispatch-subagents",
1133
+ "true",
1134
+ ]);
1135
+ const noCommand = nextStepCommand(root, artifactsDir, [
1136
+ "--host-can-dispatch-subagents",
1137
+ "false",
1138
+ ]);
1139
+ const step = await writeCurrentStep({
1140
+ artifactsDir,
1141
+ stepKind: "capability_check",
1142
+ status: "ready",
1143
+ runId: result.activeReviewRun.run_id,
1144
+ allowedCommands: [yesCommand, noCommand],
1145
+ stopCondition: "Run exactly one next-step command with an explicit host dispatch capability.",
1146
+ repoRoot: root,
1147
+ artifactPaths: {
1148
+ active_review_task: result.activeReviewRun.task_path,
1149
+ active_review_prompt: result.activeReviewRun.prompt_path,
1150
+ pending_audit_tasks: result.activeReviewRun.pending_audit_tasks_path ?? null,
1151
+ single_task_prompt: join(artifactsDir, "dispatch", "current-single-task-prompt.md"),
1152
+ },
1153
+ prompt: renderCapabilityCheckPrompt({ root, artifactsDir }),
1154
+ });
1155
+ console.log(JSON.stringify(step, null, 2));
1156
+ return;
1157
+ }
1158
+ if (!hostCanDispatchSubagents) {
1159
+ const singleTaskPromptPath = join(artifactsDir, "dispatch", "current-single-task-prompt.md");
1160
+ const workerCommand = renderCommand(result.activeReviewRun.worker_command);
1161
+ const step = await writeCurrentStep({
1162
+ artifactsDir,
1163
+ stepKind: "single_task_fallback",
1164
+ status: "ready",
1165
+ runId: result.activeReviewRun.run_id,
1166
+ allowedCommands: [workerCommand],
1167
+ stopCondition: "Run the exact worker_command after one result, then stop without looping.",
1168
+ repoRoot: root,
1169
+ artifactPaths: {
1170
+ active_review_task: result.activeReviewRun.task_path,
1171
+ active_review_prompt: result.activeReviewRun.prompt_path,
1172
+ pending_audit_tasks: result.activeReviewRun.pending_audit_tasks_path ?? null,
1173
+ audit_results: result.activeReviewRun.audit_results_path,
1174
+ single_task_prompt: singleTaskPromptPath,
1175
+ },
1176
+ prompt: renderSingleTaskFallbackStepPrompt({
1177
+ singleTaskPromptPath,
1178
+ activeReviewRun: result.activeReviewRun,
1179
+ }),
1180
+ });
1181
+ console.log(JSON.stringify(step, null, 2));
1182
+ return;
1183
+ }
1184
+ const dispatch = await prepareDispatchArtifacts({
1185
+ runId: result.activeReviewRun.run_id,
1186
+ artifactsDir,
1187
+ root,
1188
+ });
1189
+ const mergeCommand = mergeAndIngestCommand(artifactsDir, result.activeReviewRun.run_id);
1190
+ const continueCommand = nextStepCommand(root, artifactsDir);
1191
+ const step = await writeCurrentStep({
1192
+ artifactsDir,
1193
+ stepKind: "dispatch_review",
1194
+ status: "ready",
1195
+ runId: result.activeReviewRun.run_id,
1196
+ allowedCommands: [mergeCommand, continueCommand],
1197
+ stopCondition: "Dispatch every packet, merge-and-ingest once, then run next-step again.",
1198
+ repoRoot: root,
1199
+ artifactPaths: {
1200
+ dispatch_plan: dispatch.dispatch_plan_path,
1201
+ dispatch_warnings: dispatch.dispatch_warnings_path,
1202
+ active_review_task: result.activeReviewRun.task_path,
1203
+ pending_audit_tasks: result.activeReviewRun.pending_audit_tasks_path ?? null,
1204
+ },
1205
+ prompt: renderDispatchReviewPrompt({
1206
+ root,
1207
+ artifactsDir,
1208
+ activeReviewRun: result.activeReviewRun,
1209
+ dispatchPlanPath: dispatch.dispatch_plan_path,
1210
+ hostCanRestrictSubagentTools,
1211
+ hostCanSelectSubagentModel,
1212
+ }),
1213
+ });
1214
+ console.log(JSON.stringify(step, null, 2));
1215
+ }
662
1216
  async function cmdRunToCompletion(argv) {
663
1217
  const root = getRootDir(argv);
664
1218
  const artifactsDir = getArtifactsDir(argv);
@@ -1544,17 +2098,14 @@ function renderPacketGraphContext(packet) {
1544
2098
  lines.push("");
1545
2099
  return lines;
1546
2100
  }
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);
2101
+ async function prepareDispatchArtifacts(params) {
2102
+ const runId = params.runId;
2103
+ const artifactsDir = params.artifactsDir;
1552
2104
  const runDir = join(artifactsDir, "runs", runId);
1553
2105
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1554
2106
  const taskResultsDir = join(runDir, "task-results");
1555
2107
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1556
- const explicitRoot = getFlag(argv, "--root") ? getRootDir(argv) : undefined;
1557
- let reviewRoot = explicitRoot;
2108
+ let reviewRoot = params.root;
1558
2109
  try {
1559
2110
  const workerTask = await readJsonFile(join(runDir, "task.json"));
1560
2111
  reviewRoot ??= workerTask.repo_root;
@@ -1721,6 +2272,7 @@ async function cmdPrepareDispatch(argv) {
1721
2272
  largeFileMode
1722
2273
  ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
1723
2274
  : "Use your Read tool. Paths are repo-relative from the current working directory.",
2275
+ "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
2276
  fileList,
1725
2277
  "",
1726
2278
  ...renderPacketGraphContext(packet),
@@ -1796,7 +2348,7 @@ async function cmdPrepareDispatch(argv) {
1796
2348
  if (warningsPath) {
1797
2349
  await writeJsonFile(warningsPath, warnings);
1798
2350
  }
1799
- console.log(JSON.stringify({
2351
+ return {
1800
2352
  run_id: runId,
1801
2353
  dispatch_plan_path: dispatchPlanPath,
1802
2354
  packet_count: plan.length,
@@ -1810,7 +2362,18 @@ async function cmdPrepareDispatch(argv) {
1810
2362
  : null,
1811
2363
  warning_count: warnings.length,
1812
2364
  dispatch_warnings_path: warningsPath,
1813
- }, null, 2));
2365
+ };
2366
+ }
2367
+ async function cmdPrepareDispatch(argv) {
2368
+ const runId = getFlag(argv, "--run-id");
2369
+ if (!runId)
2370
+ throw new Error("prepare-dispatch requires --run-id <run_id>");
2371
+ const result = await prepareDispatchArtifacts({
2372
+ runId,
2373
+ artifactsDir: getArtifactsDir(argv),
2374
+ root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
2375
+ });
2376
+ console.log(JSON.stringify(result, null, 2));
1814
2377
  }
1815
2378
  async function cmdSubmitPacket(argv) {
1816
2379
  const runId = resolveRunScopedArg(argv, "--run-id", "--run-id-b64");
@@ -2369,6 +2932,9 @@ async function main(argv) {
2369
2932
  case "advance-audit":
2370
2933
  await cmdAdvanceAudit(argv);
2371
2934
  return;
2935
+ case "next-step":
2936
+ await cmdNextStep(argv);
2937
+ return;
2372
2938
  case "run-to-completion":
2373
2939
  await cmdRunToCompletion(argv);
2374
2940
  return;
@@ -2425,7 +2991,7 @@ async function main(argv) {
2425
2991
  return;
2426
2992
  default:
2427
2993
  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");
2994
+ 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");
2429
2995
  process.exitCode = 1;
2430
2996
  }
2431
2997
  }
@@ -12,6 +12,7 @@ export function renderWorkerPrompt(task) {
12
12
  `Read: ${tasksPath}`,
13
13
  "Scope: review only the tasks listed in the Read file. Do not add tasks,",
14
14
  "edit source files, remediate findings, run unrelated audits, or write result_path.",
15
+ "Prefer host Read and Grep tools for source inspection. On native Windows, do not use Unix pipelines like `grep ... | head`; if shell search is unavoidable, use `Select-String` as a fallback.",
15
16
  "For each listed task: read the assigned file_paths under the specified lens,",
16
17
  "using targeted reads/searches where they give complete enough evidence without loading unrelated context,",
17
18
  "and emit exactly one AuditResult object with:",