auditor-lambda 0.2.5 → 0.2.6

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/README.md CHANGED
@@ -102,6 +102,18 @@ audit-code validate
102
102
 
103
103
  That check now covers the artifact bundle plus `session-config.json` and explicit provider readiness.
104
104
 
105
+ For native batch ingestion of multiple result files:
106
+
107
+ ```bash
108
+ audit-code --batch-results /path/to/audit-results-dir
109
+ ```
110
+
111
+ For task-to-coverage inspection without reverse-engineering multiple artifacts:
112
+
113
+ ```bash
114
+ audit-code explain-task <task_id>
115
+ ```
116
+
105
117
  The backend wrapper response schema is `schemas/audit-code-v1alpha1.schema.json`.
106
118
 
107
119
  ## Backend Provider Modes
@@ -234,13 +234,14 @@ async function ensureBuilt() {
234
234
 
235
235
  function printHelp({ usageName, preferredEntrypoint }) {
236
236
  const lines = [
237
- `Usage: node ${usageName} [--single-step] [--root PATH] [--artifacts-dir PATH] [--results FILE] [--updates FILE] [--external-analyzer-results FILE]`,
237
+ `Usage: node ${usageName} [--single-step] [--root PATH] [--artifacts-dir PATH] [--results FILE] [--batch-results DIR] [--updates FILE] [--external-analyzer-results FILE] [--timeout MS]`,
238
238
  '',
239
239
  'Helper commands:',
240
240
  '- prompt-path prints the absolute path to the canonical /audit-code prompt asset',
241
241
  '- install bootstraps /audit-code into supported repo-local host surfaces',
242
242
  '- install-host --host copilot keeps the narrower Copilot-focused install path available',
243
243
  '- validate checks the current artifact bundle plus session-config/provider readiness and exits non-zero when issues exist',
244
+ '- explain-task <task_id> prints the resolved file coverage and current status for a task id',
244
245
  '',
245
246
  'Primary usage:',
246
247
  '- from the repository root, run the wrapper with no arguments',
@@ -903,6 +904,11 @@ export async function runAuditCodeWrapper({
903
904
  return;
904
905
  }
905
906
 
907
+ if (argv[0] === 'explain-task') {
908
+ await runDistCommand('explain-task', argv.slice(1));
909
+ return;
910
+ }
911
+
906
912
  const wrapperArgs = [...argv];
907
913
  if (defaultSingleStep && !hasFlag(wrapperArgs, '--single-step')) {
908
914
  wrapperArgs.push('--single-step');
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
- import { access, mkdir } from "node:fs/promises";
1
+ import { access, mkdir, readdir, rename } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
- import { join, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { buildRepoManifest } from "./extractors/fileInventory.js";
5
5
  import { buildFileDisposition } from "./extractors/disposition.js";
6
6
  import { buildCriticalFlowManifest } from "./extractors/flows.js";
@@ -50,6 +50,10 @@ function getArtifactsDir(argv) {
50
50
  function getRootDir(argv) {
51
51
  return resolve(getFlag(argv, "--root", "."));
52
52
  }
53
+ function getBatchResultsDir(argv) {
54
+ const value = getFlag(argv, "--batch-results");
55
+ return value ? resolve(value) : undefined;
56
+ }
53
57
  function getMaxRuns(argv) {
54
58
  const raw = Number(getFlag(argv, "--max-runs", String(DEFAULT_MAX_RUNS)));
55
59
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_RUNS;
@@ -78,6 +82,15 @@ function getParallelWorkers(argv, sessionConfig) {
78
82
  }
79
83
  return 1;
80
84
  }
85
+ function getTimeoutMs(argv, sessionConfig) {
86
+ const fromArg = getFlag(argv, "--timeout");
87
+ if (fromArg !== undefined) {
88
+ const parsed = Number(fromArg);
89
+ if (Number.isFinite(parsed) && parsed > 0)
90
+ return Math.floor(parsed);
91
+ }
92
+ return sessionConfig.timeout_ms ?? DEFAULT_TIMEOUT_MS;
93
+ }
81
94
  function chunkArray(arr, size) {
82
95
  const chunks = [];
83
96
  for (let i = 0; i < arr.length; i += size) {
@@ -203,6 +216,29 @@ async function buildLineIndex(root, repoManifest) {
203
216
  }
204
217
  return Object.fromEntries(entries);
205
218
  }
219
+ async function buildLineIndexForPaths(root, paths) {
220
+ const uniquePaths = [...new Set(paths)].sort();
221
+ const entries = await Promise.all(uniquePaths.map(async (path) => {
222
+ try {
223
+ return [path, await countLines(resolve(root, path))];
224
+ }
225
+ catch {
226
+ return [path, 0];
227
+ }
228
+ }));
229
+ return Object.fromEntries(entries);
230
+ }
231
+ async function listBatchResultFiles(batchDir) {
232
+ const entries = await readdir(batchDir, { withFileTypes: true });
233
+ const files = entries
234
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
235
+ .map((entry) => join(batchDir, entry.name))
236
+ .sort((a, b) => a.localeCompare(b));
237
+ if (files.length === 0) {
238
+ throw new Error(`No JSON audit result files found in ${batchDir}.`);
239
+ }
240
+ return files;
241
+ }
206
242
  const PROJECT_SIGNALS = [
207
243
  "package.json",
208
244
  "go.mod",
@@ -229,33 +265,122 @@ async function detectProjectRoot(root) {
229
265
  }
230
266
  function buildPendingAuditTasks(bundle) {
231
267
  const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
232
- return (bundle.audit_tasks ?? []).filter((task) => !completedTaskIds.has(task.task_id));
268
+ return (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
269
+ }
270
+ function formatAuditResultValidationError(issues) {
271
+ return (`audit-results validation failed with ${issues.length} error(s):\n` +
272
+ formatAuditResultIssues(issues));
273
+ }
274
+ function buildWorkerFailureBlocker(workerResult) {
275
+ const details = workerResult.errors.filter((error) => error.trim().length > 0);
276
+ return details.length > 0
277
+ ? `${workerResult.summary} ${details.join(" ")}`
278
+ : workerResult.summary;
279
+ }
280
+ function looksLikeCliFlag(value) {
281
+ return typeof value === "string" && value.startsWith("--");
282
+ }
283
+ async function maybeArchiveLegacyPendingResults(auditResultsPath) {
284
+ if (!auditResultsPath || basename(auditResultsPath) !== "worker_results_pending.json") {
285
+ return undefined;
286
+ }
287
+ const archivedPath = join(dirname(auditResultsPath), `worker_results_submitted_${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
288
+ try {
289
+ await rename(auditResultsPath, archivedPath);
290
+ return archivedPath;
291
+ }
292
+ catch (error) {
293
+ process.stderr.write(`[audit-results cleanup] failed to archive ${auditResultsPath}: ${error instanceof Error ? error.message : String(error)}\n`);
294
+ return undefined;
295
+ }
233
296
  }
234
297
  async function runAuditStep(options) {
235
298
  const bundle = await loadArtifactBundle(options.artifactsDir);
299
+ const lineIndex = bundle.repo_manifest
300
+ ? await buildLineIndex(options.root, bundle.repo_manifest)
301
+ : undefined;
302
+ if (looksLikeCliFlag(options.auditResultsPath)) {
303
+ throw new Error(`Invalid audit results path '${options.auditResultsPath}'. This looks like a CLI flag rather than a file path.`);
304
+ }
236
305
  const auditResults = options.auditResultsPath
237
306
  ? await readJsonFile(options.auditResultsPath)
238
307
  : undefined;
308
+ if (auditResults !== undefined) {
309
+ const issues = validateAuditResults(auditResults, bundle.audit_tasks ?? [], {
310
+ lineIndex,
311
+ });
312
+ const errors = issues.filter((issue) => issue.severity === "error");
313
+ const warnings = issues.filter((issue) => issue.severity === "warning");
314
+ if (warnings.length > 0) {
315
+ process.stderr.write(`audit-results validation: ${warnings.length} warning(s):\n` +
316
+ formatAuditResultIssues(warnings) +
317
+ "\n");
318
+ }
319
+ if (errors.length > 0) {
320
+ throw new Error(formatAuditResultValidationError(errors));
321
+ }
322
+ }
239
323
  const runtimeValidationUpdates = options.runtimeUpdatesPath
240
324
  ? await readJsonFile(options.runtimeUpdatesPath)
241
325
  : undefined;
242
326
  const externalAnalyzerResults = options.externalAnalyzerPath
243
327
  ? await readJsonFile(options.externalAnalyzerPath)
244
328
  : undefined;
245
- const lineIndex = bundle.repo_manifest
246
- ? await buildLineIndex(options.root, bundle.repo_manifest)
247
- : undefined;
248
329
  const result = await advanceAudit(bundle, {
249
330
  root: options.root,
250
331
  lineIndex,
251
- auditResults,
332
+ auditResults: auditResults,
252
333
  runtimeValidationUpdates,
253
334
  externalAnalyzerResults,
254
335
  preferredExecutor: options.preferredExecutor,
255
336
  });
256
337
  await writeCoreArtifacts(options.artifactsDir, result.updated_bundle);
338
+ const archivedPendingResults = await maybeArchiveLegacyPendingResults(options.auditResultsPath);
339
+ if (archivedPendingResults) {
340
+ result.progress_summary +=
341
+ ` Archived legacy staging file to ${archivedPendingResults}.`;
342
+ }
257
343
  return result;
258
344
  }
345
+ async function ingestBatchAuditResults(options) {
346
+ const batchFiles = await listBatchResultFiles(options.batchDir);
347
+ const artifactsWritten = new Set();
348
+ const progressSummaries = [];
349
+ let lastStep = null;
350
+ let anyProgress = false;
351
+ for (const batchFile of batchFiles) {
352
+ const step = await runAuditStep({
353
+ root: options.root,
354
+ artifactsDir: options.artifactsDir,
355
+ preferredExecutor: "result_ingestion_executor",
356
+ auditResultsPath: batchFile,
357
+ });
358
+ lastStep = step;
359
+ anyProgress ||= step.progress_made;
360
+ for (const artifact of step.artifacts_written) {
361
+ artifactsWritten.add(artifact);
362
+ }
363
+ progressSummaries.push(`${basename(batchFile)}: ${step.progress_summary}`);
364
+ }
365
+ const bundle = lastStep?.updated_bundle ??
366
+ (await loadArtifactBundle(options.artifactsDir));
367
+ const state = lastStep?.audit_state ?? deriveAuditState(bundle);
368
+ const decision = decideNextStep(bundle);
369
+ return {
370
+ batchFiles,
371
+ bundle,
372
+ audit_state: state,
373
+ selected_obligation: lastStep?.selected_obligation ?? decision.selected_obligation,
374
+ selected_executor: lastStep?.selected_executor ?? "result_ingestion_executor",
375
+ progress_made: anyProgress,
376
+ artifacts_written: Array.from(artifactsWritten),
377
+ progress_summary: `Imported ${batchFiles.length} batch result file${batchFiles.length === 1 ? "" : "s"} from ${options.batchDir}.` +
378
+ (progressSummaries.length > 0
379
+ ? `\n${progressSummaries.join("\n")}`
380
+ : ""),
381
+ next_likely_step: state.status === "complete" ? null : decision.selected_obligation,
382
+ };
383
+ }
259
384
  function isWorkerResult(value) {
260
385
  return (typeof value === "object" &&
261
386
  value !== null &&
@@ -276,7 +401,9 @@ export async function runSample() {
276
401
  pass_id: "pass:security",
277
402
  lens: "security",
278
403
  agent_role: "security-auditor",
279
- reviewed_ranges: [{ path: "src/api/auth.ts", start: 1, end: 100 }],
404
+ reviewed_ranges: [
405
+ { path: "src/api/auth.ts", start: 1, end: 100, line_count: 100 },
406
+ ],
280
407
  findings: [],
281
408
  notes: ["Sample result ingestion path."],
282
409
  requires_followup: false,
@@ -322,6 +449,34 @@ async function cmdAdvanceAudit(argv) {
322
449
  const artifactsDir = getArtifactsDir(argv);
323
450
  const sessionConfig = await loadSessionConfig(artifactsDir);
324
451
  const providerName = resolveFreshSessionProviderName(getFlag(argv, "--provider"), sessionConfig);
452
+ const batchResultsDir = getBatchResultsDir(argv);
453
+ if (batchResultsDir && getFlag(argv, "--results")) {
454
+ throw new Error("Use either --results <file> or --batch-results <dir>, not both.");
455
+ }
456
+ if (batchResultsDir) {
457
+ const result = await ingestBatchAuditResults({
458
+ root,
459
+ artifactsDir,
460
+ batchDir: batchResultsDir,
461
+ });
462
+ await emitEnvelope({
463
+ root,
464
+ artifactsDir,
465
+ bundle: result.bundle,
466
+ audit_state: result.audit_state,
467
+ selected_obligation: result.selected_obligation,
468
+ selected_executor: result.selected_executor,
469
+ progress_made: result.progress_made,
470
+ artifacts_written: result.artifacts_written,
471
+ progress_summary: result.progress_summary,
472
+ next_likely_step: result.next_likely_step,
473
+ providerName,
474
+ });
475
+ if (result.audit_state.status === "complete") {
476
+ await cleanupIntermediateArtifacts(artifactsDir);
477
+ }
478
+ return;
479
+ }
325
480
  const externalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
326
481
  const result = await runAuditStep({
327
482
  root,
@@ -358,10 +513,17 @@ async function cmdRunToCompletion(argv) {
358
513
  const maxRuns = getMaxRuns(argv);
359
514
  const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
360
515
  const parallelWorkers = getParallelWorkers(argv, sessionConfig);
361
- const timeoutMs = sessionConfig.timeout_ms ?? DEFAULT_TIMEOUT_MS;
516
+ const timeoutMs = getTimeoutMs(argv, sessionConfig);
362
517
  const selfCliPath = resolve(process.argv[1] ?? "");
363
518
  await mkdir(artifactsDir, { recursive: true });
364
519
  await ensureSupervisorDirs(artifactsDir);
520
+ const batchResultsDir = getBatchResultsDir(argv);
521
+ if (batchResultsDir && getFlag(argv, "--results")) {
522
+ throw new Error("Use either --results <file> or --batch-results <dir>, not both.");
523
+ }
524
+ let pendingBatchAuditResults = batchResultsDir
525
+ ? await listBatchResultFiles(batchResultsDir)
526
+ : [];
365
527
  const earlyBundle = await loadArtifactBundle(artifactsDir);
366
528
  if (!earlyBundle.unit_manifest) {
367
529
  const foundSignal = await detectProjectRoot(root);
@@ -411,6 +573,11 @@ async function cmdRunToCompletion(argv) {
411
573
  obligationId = "external_analyzer_import";
412
574
  externalAnalyzerPath = pendingExternalAnalyzerPath;
413
575
  }
576
+ else if (pendingBatchAuditResults.length > 0 && bundle.coverage_matrix) {
577
+ preferredExecutor = "result_ingestion_executor";
578
+ obligationId = "audit_results_ingested";
579
+ auditResultsPath = pendingBatchAuditResults[0];
580
+ }
414
581
  else if (pendingAuditResultsPath && bundle.coverage_matrix) {
415
582
  preferredExecutor = "result_ingestion_executor";
416
583
  obligationId = "audit_results_ingested";
@@ -457,7 +624,7 @@ async function cmdRunToCompletion(argv) {
457
624
  pending_audit_tasks_path: blockPendingTasksPath,
458
625
  };
459
626
  const blockPrompt = renderWorkerPrompt(blockTask);
460
- await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir);
627
+ await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir, blockPendingTasks);
461
628
  await writeJsonFile(blockPendingTasksPath, blockPendingTasks);
462
629
  await emitEnvelope({
463
630
  root,
@@ -527,12 +694,12 @@ async function cmdRunToCompletion(argv) {
527
694
  skip_worker_command: true,
528
695
  };
529
696
  const slotPrompt = renderWorkerPrompt(slotTask);
530
- await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir);
697
+ await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir, group);
531
698
  await writeJsonFile(slotPendingTasksPath, group);
532
699
  workerSlots.push({ runId: slotRunId, paths: slotPaths, auditResultsPath: slotAuditResultsPath, pendingTasksPath: slotPendingTasksPath, group });
533
700
  }
534
701
  const parallelStartedAt = new Date().toISOString();
535
- await Promise.allSettled(workerSlots.map((slot) => provider.launch({
702
+ const launchResults = await Promise.allSettled(workerSlots.map((slot) => provider.launch({
536
703
  repoRoot: root,
537
704
  runId: slot.runId,
538
705
  obligationId,
@@ -544,21 +711,37 @@ async function cmdRunToCompletion(argv) {
544
711
  uiMode,
545
712
  timeoutMs,
546
713
  })));
714
+ const launchErrorsByRunId = new Map();
715
+ for (let index = 0; index < launchResults.length; index++) {
716
+ const outcome = launchResults[index];
717
+ if (outcome?.status === "rejected") {
718
+ launchErrorsByRunId.set(workerSlots[index].runId, outcome.reason instanceof Error
719
+ ? outcome.reason.message
720
+ : String(outcome.reason));
721
+ }
722
+ }
547
723
  // Result ingestion is intentionally sequential even though agent launch
548
724
  // was parallel. Writing to coverage_matrix.json is not atomic, so
549
725
  // concurrent ingest calls would race and corrupt coverage state.
550
726
  let batchProgress = false;
727
+ const batchErrors = [];
551
728
  for (const slot of workerSlots) {
552
729
  const parallelEndedAt = new Date().toISOString();
553
730
  let slotStatus = "no_progress";
554
731
  try {
732
+ const launchError = launchErrorsByRunId.get(slot.runId);
733
+ if (launchError) {
734
+ throw new Error(`Worker launch failed: ${launchError}`);
735
+ }
555
736
  const auditResults = await readJsonFile(slot.auditResultsPath);
556
737
  const pendingTaskIds = new Set(slot.group.map((t) => t.task_id));
557
738
  const matchedCount = auditResults.filter((r) => pendingTaskIds.has(r.task_id)).length;
558
739
  if (slot.group.length > 0 && matchedCount === 0) {
559
740
  throw new Error("Worker did not emit any audit results for the assigned tasks.");
560
741
  }
561
- const issues = validateAuditResults(auditResults, slot.group);
742
+ const issues = validateAuditResults(auditResults, slot.group, {
743
+ lineIndex: await buildLineIndexForPaths(root, slot.group.flatMap((task) => task.file_paths)),
744
+ });
562
745
  const errors = issues.filter((issue) => issue.severity === "error");
563
746
  const warnings = issues.filter((issue) => issue.severity === "warning");
564
747
  if (warnings.length > 0) {
@@ -582,8 +765,11 @@ async function cmdRunToCompletion(argv) {
582
765
  for (const a of stepResult.artifacts_written)
583
766
  artifactsWritten.add(a);
584
767
  }
585
- catch {
768
+ catch (error) {
586
769
  slotStatus = "failed";
770
+ const message = error instanceof Error ? error.message : String(error);
771
+ batchErrors.push(`${slot.runId}: ${message}`);
772
+ process.stderr.write(`[agent-batch] ${slot.runId} failed: ${message}\n`);
587
773
  }
588
774
  await appendRunLedgerEntry(artifactsDir, {
589
775
  run_id: slot.runId,
@@ -597,6 +783,35 @@ async function cmdRunToCompletion(argv) {
597
783
  });
598
784
  artifactsWritten.add("run-ledger.json");
599
785
  }
786
+ if (batchErrors.length > 0) {
787
+ const bundleAfter = await loadArtifactBundle(artifactsDir);
788
+ const blockedState = buildBlockedAuditState({
789
+ state: bundleAfter.audit_state ?? deriveAuditState(bundleAfter),
790
+ obligationId,
791
+ executor: "agent",
792
+ blocker: `Parallel worker batch failed for ${batchErrors.length} run(s). ` +
793
+ batchErrors.slice(0, 3).join(" | "),
794
+ });
795
+ await writeCoreArtifacts(artifactsDir, {
796
+ ...bundleAfter,
797
+ audit_state: blockedState,
798
+ });
799
+ await emitEnvelope({
800
+ root,
801
+ artifactsDir,
802
+ bundle: { ...bundleAfter, audit_state: blockedState },
803
+ audit_state: blockedState,
804
+ selected_obligation: obligationId,
805
+ selected_executor: "agent",
806
+ progress_made: anyProgress,
807
+ artifacts_written: Array.from(new Set([...artifactsWritten, "audit_state.json"])),
808
+ progress_summary: `Parallel worker batch failed for ${batchErrors.length} run(s).\n` +
809
+ batchErrors.join("\n"),
810
+ next_likely_step: null,
811
+ providerName: provider.name,
812
+ });
813
+ return;
814
+ }
600
815
  if (!batchProgress) {
601
816
  const bundleAfter = await loadArtifactBundle(artifactsDir);
602
817
  const state = bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
@@ -650,7 +865,7 @@ async function cmdRunToCompletion(argv) {
650
865
  external_analyzer_results_path: externalAnalyzerPath,
651
866
  };
652
867
  const prompt = renderWorkerPrompt(task);
653
- await writeWorkerTaskFiles(task, prompt, paths, artifactsDir);
868
+ await writeWorkerTaskFiles(task, prompt, paths, artifactsDir, pendingAuditTasks);
654
869
  if (pendingAuditTasksPath && pendingAuditTasks) {
655
870
  await writeJsonFile(pendingAuditTasksPath, pendingAuditTasks);
656
871
  }
@@ -686,6 +901,7 @@ async function cmdRunToCompletion(argv) {
686
901
  };
687
902
  }
688
903
  catch (error) {
904
+ const message = error instanceof Error ? error.message : String(error);
689
905
  workerResult = {
690
906
  contract_version: WORKER_RESULT_CONTRACT_VERSION,
691
907
  run_id: runId,
@@ -694,9 +910,9 @@ async function cmdRunToCompletion(argv) {
694
910
  progress_made: false,
695
911
  selected_executor: preferredExecutor,
696
912
  artifacts_written: [],
697
- summary: `Worker launch failed for ${preferredExecutor}.`,
913
+ summary: `Worker launch failed for ${preferredExecutor}: ${message}`,
698
914
  next_likely_step: decision.selected_obligation,
699
- errors: [error instanceof Error ? error.message : String(error)],
915
+ errors: [message],
700
916
  };
701
917
  await writeJsonFile(paths.resultPath, workerResult);
702
918
  }
@@ -720,6 +936,13 @@ async function cmdRunToCompletion(argv) {
720
936
  artifactsWritten.add("run-ledger.json");
721
937
  if (externalAnalyzerPath)
722
938
  pendingExternalAnalyzerPath = undefined;
939
+ if (auditResultsPath &&
940
+ pendingBatchAuditResults[0] === auditResultsPath &&
941
+ preferredExecutor === "result_ingestion_executor" &&
942
+ workerResult.status !== "failed" &&
943
+ workerResult.status !== "blocked") {
944
+ pendingBatchAuditResults.shift();
945
+ }
723
946
  if (providerAuditResultsPath)
724
947
  pendingAuditResultsPath = undefined;
725
948
  if (runtimeUpdatesPath)
@@ -728,18 +951,36 @@ async function cmdRunToCompletion(argv) {
728
951
  workerResult.status === "blocked" ||
729
952
  workerResult.status === "no_progress") {
730
953
  const bundleAfter = await loadArtifactBundle(artifactsDir);
731
- const state = bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
954
+ const shouldBlock = workerResult.status === "failed" || workerResult.status === "blocked";
955
+ const state = shouldBlock
956
+ ? buildBlockedAuditState({
957
+ state: bundleAfter.audit_state ?? deriveAuditState(bundleAfter),
958
+ obligationId: workerResult.obligation_id,
959
+ executor: workerResult.selected_executor,
960
+ blocker: buildWorkerFailureBlocker(workerResult),
961
+ })
962
+ : bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
963
+ if (shouldBlock) {
964
+ await writeCoreArtifacts(artifactsDir, {
965
+ ...bundleAfter,
966
+ audit_state: state,
967
+ });
968
+ }
732
969
  await emitEnvelope({
733
970
  root,
734
971
  artifactsDir,
735
- bundle: bundleAfter,
972
+ bundle: shouldBlock
973
+ ? { ...bundleAfter, audit_state: state }
974
+ : bundleAfter,
736
975
  audit_state: state,
737
976
  selected_obligation: workerResult.obligation_id,
738
977
  selected_executor: workerResult.selected_executor,
739
978
  progress_made: anyProgress,
740
- artifacts_written: Array.from(artifactsWritten),
741
- progress_summary: workerResult.summary,
742
- next_likely_step: workerResult.next_likely_step,
979
+ artifacts_written: Array.from(shouldBlock
980
+ ? new Set([...artifactsWritten, "audit_state.json"])
981
+ : artifactsWritten),
982
+ progress_summary: buildWorkerFailureBlocker(workerResult),
983
+ next_likely_step: shouldBlock ? null : workerResult.next_likely_step,
743
984
  providerName: provider.name,
744
985
  });
745
986
  return;
@@ -770,6 +1011,9 @@ async function cmdWorkerRun(argv) {
770
1011
  const task = await readJsonFile(taskPath);
771
1012
  let workerResult;
772
1013
  try {
1014
+ if (looksLikeCliFlag(task.audit_results_path)) {
1015
+ throw new Error(`task.audit_results_path resolved to '${task.audit_results_path}', which looks like a CLI flag instead of a file path.`);
1016
+ }
773
1017
  if (task.preferred_executor === "agent" && !task.audit_results_path) {
774
1018
  throw new Error("agent worker-run requires audit_results_path so provider-assisted review can be ingested.");
775
1019
  }
@@ -783,7 +1027,9 @@ async function cmdWorkerRun(argv) {
783
1027
  if (pendingTasks.length > 0 && matchedResultCount === 0) {
784
1028
  throw new Error("Provider-assisted review did not emit any audit results for the pending audit tasks.");
785
1029
  }
786
- const issues = validateAuditResults(auditResults, pendingTasks);
1030
+ const issues = validateAuditResults(auditResults, pendingTasks, {
1031
+ lineIndex: await buildLineIndexForPaths(task.repo_root, pendingTasks.flatMap((item) => item.file_paths)),
1032
+ });
787
1033
  const errors = issues.filter((issue) => issue.severity === "error");
788
1034
  const warnings = issues.filter((issue) => issue.severity === "warning");
789
1035
  if (warnings.length > 0) {
@@ -792,8 +1038,7 @@ async function cmdWorkerRun(argv) {
792
1038
  "\n");
793
1039
  }
794
1040
  if (errors.length > 0) {
795
- throw new Error(`audit-results validation failed with ${errors.length} error(s):\n` +
796
- formatAuditResultIssues(errors));
1041
+ throw new Error(formatAuditResultValidationError(errors));
797
1042
  }
798
1043
  }
799
1044
  const preferredExecutor = task.preferred_executor === "agent"
@@ -829,7 +1074,7 @@ async function cmdWorkerRun(argv) {
829
1074
  progress_made: false,
830
1075
  selected_executor: task.preferred_executor,
831
1076
  artifacts_written: [],
832
- summary: `Worker failed for executor ${task.preferred_executor}.`,
1077
+ summary: `Worker failed for executor ${task.preferred_executor}: ${error instanceof Error ? error.message : String(error)}`,
833
1078
  next_likely_step: task.obligation_id,
834
1079
  errors: [error instanceof Error ? error.message : String(error)],
835
1080
  };
@@ -882,6 +1127,24 @@ async function cmdPlan(argv) {
882
1127
  }
883
1128
  async function cmdIngestResults(argv) {
884
1129
  const artifactsDir = getArtifactsDir(argv);
1130
+ const batchResultsDir = getBatchResultsDir(argv);
1131
+ if (batchResultsDir && getFlag(argv, "--results")) {
1132
+ throw new Error("Use either --results <file> or --batch-results <dir>, not both.");
1133
+ }
1134
+ if (batchResultsDir) {
1135
+ const result = await ingestBatchAuditResults({
1136
+ root: getRootDir(argv),
1137
+ artifactsDir,
1138
+ batchDir: batchResultsDir,
1139
+ });
1140
+ console.log(JSON.stringify({
1141
+ artifacts_dir: artifactsDir,
1142
+ imported_files: result.batchFiles,
1143
+ selected_executor: result.selected_executor,
1144
+ progress_summary: result.progress_summary,
1145
+ }, null, 2));
1146
+ return;
1147
+ }
885
1148
  const result = await runAuditStep({
886
1149
  root: getRootDir(argv),
887
1150
  artifactsDir,
@@ -894,6 +1157,37 @@ async function cmdIngestResults(argv) {
894
1157
  progress_summary: result.progress_summary,
895
1158
  }, null, 2));
896
1159
  }
1160
+ async function cmdExplainTask(argv) {
1161
+ const artifactsDir = getArtifactsDir(argv);
1162
+ const taskId = getFlag(argv, "--task-id") ?? argv[3];
1163
+ if (!taskId) {
1164
+ throw new Error("explain-task requires <task_id> or --task-id <task_id>");
1165
+ }
1166
+ const bundle = await loadArtifactBundle(artifactsDir);
1167
+ const task = [...(bundle.audit_tasks ?? []), ...(bundle.requeue_tasks ?? [])].find((item) => item.task_id === taskId);
1168
+ if (!task) {
1169
+ throw new Error(`Unknown task_id '${taskId}'.`);
1170
+ }
1171
+ const coverageEntries = (bundle.coverage_matrix?.files ?? [])
1172
+ .filter((file) => task.file_paths.includes(file.path))
1173
+ .sort((a, b) => a.path.localeCompare(b.path));
1174
+ const matchingResults = (bundle.audit_results ?? []).filter((result) => result.task_id === task.task_id);
1175
+ console.log(JSON.stringify({
1176
+ artifacts_dir: artifactsDir,
1177
+ task_id: task.task_id,
1178
+ task,
1179
+ file_count: task.file_paths.length,
1180
+ coverage_entries: coverageEntries,
1181
+ pending_coverage: coverageEntries
1182
+ .map((file) => ({
1183
+ path: file.path,
1184
+ missing_lenses: file.required_lenses.filter((lens) => !file.completed_lenses.includes(lens)),
1185
+ }))
1186
+ .filter((file) => file.missing_lenses.length > 0),
1187
+ matching_result_count: matchingResults.length,
1188
+ matching_finding_ids: matchingResults.flatMap((result) => result.findings.map((finding) => finding.id)),
1189
+ }, null, 2));
1190
+ }
897
1191
  async function cmdUpdateRuntimeValidation(argv) {
898
1192
  const artifactsDir = getArtifactsDir(argv);
899
1193
  const result = await runAuditStep({
@@ -990,6 +1284,9 @@ async function main(argv) {
990
1284
  case "ingest-results":
991
1285
  await cmdIngestResults(argv);
992
1286
  return;
1287
+ case "explain-task":
1288
+ await cmdExplainTask(argv);
1289
+ return;
993
1290
  case "update-runtime-validation":
994
1291
  await cmdUpdateRuntimeValidation(argv);
995
1292
  return;
@@ -1004,7 +1301,7 @@ async function main(argv) {
1004
1301
  return;
1005
1302
  default:
1006
1303
  console.error(`Unknown command: ${command}`);
1007
- console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, update-runtime-validation, validate, requeue, synthesize");
1304
+ 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, requeue, synthesize");
1008
1305
  process.exitCode = 1;
1009
1306
  }
1010
1307
  }
@@ -1,3 +1,4 @@
1
+ import type { AuditTask } from "../types.js";
1
2
  import type { WorkerTask } from "../types/workerSession.js";
2
3
  export interface RunPaths {
3
4
  runDir: string;
@@ -11,4 +12,4 @@ export interface RunPaths {
11
12
  export declare function buildRunId(obligationId: string | null, index: number): string;
12
13
  export declare function getRunPaths(artifactsDir: string, runId: string): RunPaths;
13
14
  export declare function ensureSupervisorDirs(artifactsDir: string): Promise<void>;
14
- export declare function writeWorkerTaskFiles(task: WorkerTask, prompt: string, paths: RunPaths, artifactsDir: string): Promise<void>;
15
+ export declare function writeWorkerTaskFiles(task: WorkerTask, prompt: string, paths: RunPaths, artifactsDir: string, currentTasks?: AuditTask[]): Promise<void>;
@@ -24,7 +24,7 @@ export async function ensureSupervisorDirs(artifactsDir) {
24
24
  await mkdir(join(artifactsDir, "worker-logs"), { recursive: true });
25
25
  await mkdir(join(artifactsDir, "runs"), { recursive: true });
26
26
  }
27
- export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir) {
27
+ export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, currentTasks) {
28
28
  await mkdir(paths.runDir, { recursive: true });
29
29
  await writeJsonFile(paths.taskPath, task);
30
30
  await writeFile(paths.promptPath, prompt, "utf8");
@@ -34,4 +34,5 @@ export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir) {
34
34
  });
35
35
  await writeJsonFile(join(artifactsDir, "dispatch", "current-task.json"), task);
36
36
  await writeFile(join(artifactsDir, "dispatch", "current-prompt.md"), prompt, "utf8");
37
+ await writeJsonFile(join(artifactsDir, "dispatch", "current-tasks.json"), currentTasks ?? []);
37
38
  }
@@ -1,5 +1,5 @@
1
1
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
2
- import type { AuditTask } from "../types.js";
2
+ import type { AuditTask, CoverageMatrix } from "../types.js";
3
3
  import type { FlowCoverageManifest } from "../types/flowCoverage.js";
4
4
  import type { CriticalFlowManifest } from "../types/flows.js";
5
- export declare function buildFlowRequeueTasks(criticalFlows: CriticalFlowManifest, flowCoverage: FlowCoverageManifest, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
5
+ export declare function buildFlowRequeueTasks(criticalFlows: CriticalFlowManifest, flowCoverage: FlowCoverageManifest, coverageMatrix: CoverageMatrix, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];