auditor-lambda 0.3.3 → 0.3.4

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.
Files changed (35) hide show
  1. package/README.md +6 -1
  2. package/audit-code-wrapper-lib.mjs +78 -5
  3. package/dist/cli.js +187 -67
  4. package/dist/extractors/graph.d.ts +5 -1
  5. package/dist/extractors/graph.js +223 -3
  6. package/dist/extractors/pathPatterns.d.ts +3 -2
  7. package/dist/extractors/pathPatterns.js +97 -24
  8. package/dist/io/artifacts.d.ts +5 -0
  9. package/dist/io/artifacts.js +2 -0
  10. package/dist/orchestrator/advance.js +1 -1
  11. package/dist/orchestrator/dependencyMap.js +18 -0
  12. package/dist/orchestrator/internalExecutors.d.ts +1 -1
  13. package/dist/orchestrator/internalExecutors.js +120 -33
  14. package/dist/orchestrator/reviewPackets.d.ts +14 -0
  15. package/dist/orchestrator/reviewPackets.js +300 -0
  16. package/dist/orchestrator/selectiveDeepening.d.ts +14 -0
  17. package/dist/orchestrator/selectiveDeepening.js +392 -0
  18. package/dist/orchestrator/state.js +6 -1
  19. package/dist/orchestrator/taskBuilder.d.ts +16 -0
  20. package/dist/orchestrator/taskBuilder.js +68 -11
  21. package/dist/prompts/renderWorkerPrompt.js +2 -1
  22. package/dist/types/graph.d.ts +1 -0
  23. package/dist/types/reviewPlanning.d.ts +41 -0
  24. package/dist/types/reviewPlanning.js +1 -0
  25. package/dist/validation/artifacts.js +13 -0
  26. package/docs/bootstrap-install.md +3 -0
  27. package/docs/dispatch-implementation-plan.md +179 -481
  28. package/docs/next-steps.md +13 -8
  29. package/docs/product-direction.md +5 -3
  30. package/docs/run-flow.md +23 -30
  31. package/docs/session-config.md +4 -1
  32. package/docs/workflow-refactor-brief.md +83 -154
  33. package/package.json +1 -1
  34. package/schemas/finding.schema.json +1 -15
  35. package/schemas/graph_bundle.schema.json +16 -0
package/README.md CHANGED
@@ -36,6 +36,10 @@ That bootstraps repo-local `/audit-code` surfaces for the hosts we can automate
36
36
  - VS Code prompt, custom agent, Copilot instructions, and `.vscode/mcp.json`
37
37
  - Antigravity planning-mode guidance plus the shared repo-local MCP launcher
38
38
 
39
+ Re-run the same `audit-code install` command whenever the packaged prompt or
40
+ skill changes. It is the single supported refresh path for the shared
41
+ `.audit-code/install/*` assets and every generated host surface.
42
+
39
43
  After bootstrap, you can smoke-test the generated host assets and launcher from the repository root:
40
44
 
41
45
  ```bash
@@ -172,7 +176,8 @@ The next implementation work is tracked in:
172
176
 
173
177
  The short version is:
174
178
 
175
- - realign review dispatch around the conversation-owned, non-overlapping lens-block workflow
179
+ - keep the packet dispatch workflow verified in real host environments
180
+ - benchmark `/audit-code` packet counts and warning counts against nontrivial external repositories
176
181
  - prove the generated Codex, Claude Desktop, OpenCode, VS Code, and Antigravity guidance in real host flows
177
182
  - tighten the repo-local MCP-first bootstrap where host smoke tests expose friction
178
183
  - polish provider-assisted continuation and failure guidance
@@ -1526,16 +1526,30 @@ async function verifyInstalledBootstrap(argv) {
1526
1526
 
1527
1527
  await collectVerifyCheck(generalChecks, 'installed_prompt', async () => {
1528
1528
  await ensureFile(assetPaths.installedPromptPath, 'Installed prompt asset');
1529
+ const installedPrompt = await readFile(assetPaths.installedPromptPath, 'utf8');
1530
+ const sourcePrompt = await readFile(promptAssetPath, 'utf8');
1531
+ if (installedPrompt !== sourcePrompt) {
1532
+ throw new Error(
1533
+ `Installed prompt is out of sync with the source prompt. Run "audit-code install" from ${root}.`,
1534
+ );
1535
+ }
1529
1536
  return {
1530
- summary: 'Installed prompt asset is present.',
1537
+ summary: 'Installed prompt asset is present and matches the source prompt.',
1531
1538
  path: assetPaths.installedPromptPath,
1532
1539
  };
1533
1540
  });
1534
1541
 
1535
1542
  await collectVerifyCheck(generalChecks, 'installed_skill', async () => {
1536
1543
  await ensureFile(assetPaths.installedSkillPath, 'Installed skill asset');
1544
+ const installedSkill = (await readFile(assetPaths.installedSkillPath, 'utf8')).replace(/\r\n/g, '\n');
1545
+ const sourceSkill = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
1546
+ if (installedSkill !== sourceSkill) {
1547
+ throw new Error(
1548
+ `Installed skill is out of sync with the source skill. Run "audit-code install" from ${root}.`,
1549
+ );
1550
+ }
1537
1551
  return {
1538
- summary: 'Installed skill asset is present.',
1552
+ summary: 'Installed skill asset is present and matches the source skill.',
1539
1553
  path: assetPaths.installedSkillPath,
1540
1554
  };
1541
1555
  });
@@ -1599,11 +1613,30 @@ async function verifyInstalledBootstrap(argv) {
1599
1613
  if (!content.includes('# audit-code skill')) {
1600
1614
  throw new Error(`Codex skill file is missing the expected heading: ${assetPaths.codexSkillPath}`);
1601
1615
  }
1616
+ const sourceSkill = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
1617
+ if (content.replace(/\r\n/g, '\n') !== sourceSkill) {
1618
+ throw new Error(
1619
+ `Codex skill is out of sync with the source skill. Run "audit-code install --host codex" or "audit-code install".`,
1620
+ );
1621
+ }
1602
1622
  return {
1603
- summary: 'Codex skill bundle is present.',
1623
+ summary: 'Codex skill bundle is present and matches the source skill.',
1604
1624
  path: assetPaths.codexSkillPath,
1605
1625
  };
1606
1626
  });
1627
+ await collectVerifyCheck(checks, 'codex_prompt', async () => {
1628
+ const content = await readFile(assetPaths.codexPromptPath, 'utf8');
1629
+ const sourcePrompt = await readFile(promptAssetPath, 'utf8');
1630
+ if (content !== sourcePrompt) {
1631
+ throw new Error(
1632
+ `Codex prompt is out of sync with the source prompt. Run "audit-code install --host codex" or "audit-code install".`,
1633
+ );
1634
+ }
1635
+ return {
1636
+ summary: 'Codex prompt bundle is present and matches the source prompt.',
1637
+ path: assetPaths.codexPromptPath,
1638
+ };
1639
+ });
1607
1640
  await collectVerifyCheck(checks, 'codex_mcp_setup', async () => {
1608
1641
  const content = await readFile(assetPaths.codexMcpSetupPath, 'utf8');
1609
1642
  if (!content.includes(MCP_LAUNCHER_FILENAME)) {
@@ -1701,11 +1734,44 @@ async function verifyInstalledBootstrap(argv) {
1701
1734
  if (!content.includes('agent: auditor')) {
1702
1735
  throw new Error(`OpenCode command file is missing the auditor agent frontmatter: ${assetPaths.opencodeCommandPath}`);
1703
1736
  }
1737
+ const { body: commandBody } = splitFrontmatter(content);
1738
+ const { body: sourceBody } = splitFrontmatter(await readFile(promptAssetPath, 'utf8'));
1739
+ if (commandBody !== sourceBody.trimStart()) {
1740
+ throw new Error(
1741
+ `OpenCode command prompt body is out of sync with the source prompt. Run "audit-code install --host opencode" or "audit-code install".`,
1742
+ );
1743
+ }
1704
1744
  return {
1705
- summary: 'OpenCode command file is present.',
1745
+ summary: 'OpenCode command file is present and uses the source prompt body.',
1706
1746
  path: assetPaths.opencodeCommandPath,
1707
1747
  };
1708
1748
  });
1749
+ await collectVerifyCheck(checks, 'opencode_skill', async () => {
1750
+ const content = (await readFile(assetPaths.opencodeSkillPath, 'utf8')).replace(/\r\n/g, '\n');
1751
+ const sourceSkill = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
1752
+ if (content !== sourceSkill) {
1753
+ throw new Error(
1754
+ `OpenCode skill is out of sync with the source skill. Run "audit-code install --host opencode" or "audit-code install".`,
1755
+ );
1756
+ }
1757
+ return {
1758
+ summary: 'OpenCode skill is present and matches the source skill.',
1759
+ path: assetPaths.opencodeSkillPath,
1760
+ };
1761
+ });
1762
+ await collectVerifyCheck(checks, 'opencode_prompt', async () => {
1763
+ const content = await readFile(assetPaths.opencodePromptPath, 'utf8');
1764
+ const sourcePrompt = await readFile(promptAssetPath, 'utf8');
1765
+ if (content !== sourcePrompt) {
1766
+ throw new Error(
1767
+ `OpenCode prompt is out of sync with the source prompt. Run "audit-code install --host opencode" or "audit-code install".`,
1768
+ );
1769
+ }
1770
+ return {
1771
+ summary: 'OpenCode prompt is present and matches the source prompt.',
1772
+ path: assetPaths.opencodePromptPath,
1773
+ };
1774
+ });
1709
1775
  await collectVerifyCheck(checks, 'opencode_config', async () => {
1710
1776
  const config = await readJson(assetPaths.opencodeConfigPath, 'OpenCode project config');
1711
1777
  const command = config?.mcp?.auditor?.command;
@@ -1730,8 +1796,15 @@ async function verifyInstalledBootstrap(argv) {
1730
1796
  if (!content.includes('name: audit-code')) {
1731
1797
  throw new Error(`VS Code prompt file is missing the expected frontmatter name: ${assetPaths.vscodePromptPath}`);
1732
1798
  }
1799
+ const { body: promptBody } = splitFrontmatter(content);
1800
+ const { body: sourceBody } = splitFrontmatter(await readFile(promptAssetPath, 'utf8'));
1801
+ if (promptBody !== sourceBody.trimStart()) {
1802
+ throw new Error(
1803
+ `VS Code prompt body is out of sync with the source prompt. Run "audit-code install --host vscode" or "audit-code install".`,
1804
+ );
1805
+ }
1733
1806
  return {
1734
- summary: 'VS Code prompt file is present.',
1807
+ summary: 'VS Code prompt file is present and uses the source prompt body.',
1735
1808
  path: assetPaths.vscodePromptPath,
1736
1809
  };
1737
1810
  });
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./superv
26
26
  import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
27
27
  import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
28
28
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
29
+ import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/reviewPackets.js";
29
30
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
30
31
  import { runAuditCodeMcpServer } from "./mcp/server.js";
31
32
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -35,7 +36,7 @@ const DIRECT_CLI_DEFAULTS = {
35
36
  rootDir: ".",
36
37
  artifactsDir: ".artifacts",
37
38
  maxRuns: 1000,
38
- agentBatchSize: 1,
39
+ agentBatchSize: 6,
39
40
  parallelWorkers: 1,
40
41
  timeoutMs: 30 * 60 * 1000, // 30 minutes
41
42
  uiMode: "headless",
@@ -287,7 +288,12 @@ async function detectProjectRoot(root) {
287
288
  }
288
289
  function buildPendingAuditTasks(bundle) {
289
290
  const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
290
- return (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
291
+ const pendingTasks = (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
292
+ const lineIndex = Object.fromEntries(pendingTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
293
+ return orderTasksForPacketReview(pendingTasks, {
294
+ graphBundle: bundle.graph_bundle,
295
+ lineIndex,
296
+ });
291
297
  }
292
298
  async function addFileLineCountHints(root, tasks) {
293
299
  const lineIndex = await buildLineIndexForPaths(root, tasks.flatMap((task) => task.file_paths));
@@ -1408,94 +1414,169 @@ async function cmdPrepareDispatch(argv) {
1408
1414
  const taskResultsDir = join(runDir, "task-results");
1409
1415
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1410
1416
  const tasks = await readJsonFile(tasksPath);
1417
+ const bundle = await loadArtifactBundle(artifactsDir);
1411
1418
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
1412
1419
  const lensDefs = await readJsonFile(lensDefsPath);
1413
1420
  await mkdir(taskResultsDir, { recursive: true });
1421
+ const lineIndex = Object.fromEntries(tasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
1422
+ const orderedTasks = orderTasksForPacketReview(tasks, {
1423
+ graphBundle: bundle.graph_bundle,
1424
+ lineIndex,
1425
+ });
1426
+ const packets = buildReviewPackets(orderedTasks, {
1427
+ graphBundle: bundle.graph_bundle,
1428
+ lineIndex,
1429
+ });
1430
+ const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
1431
+ const outputPathByTaskId = new Map(orderedTasks.map((task) => [
1432
+ task.task_id,
1433
+ join(taskResultsDir, `${task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`),
1434
+ ]));
1414
1435
  const plan = [];
1415
- let largestTask = null;
1436
+ let largestPacketId = null;
1416
1437
  let largestLines = 0;
1417
- for (const task of tasks) {
1418
- const sanitized = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1419
- const outputPath = join(taskResultsDir, `${sanitized}.json`);
1438
+ let largestEstimatedTokens = 0;
1439
+ const warnings = [];
1440
+ for (const packet of packets) {
1441
+ const sanitized = packet.packet_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1420
1442
  const promptPath = join(taskResultsDir, `${sanitized}.prompt.md`);
1421
- const lensDef = lensDefs[task.lens];
1422
- if (!lensDef) {
1423
- process.stderr.write(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})\n`);
1443
+ const packetTasks = packet.task_ids
1444
+ .map((taskId) => tasksById.get(taskId))
1445
+ .filter((task) => task !== undefined);
1446
+ if (packet.total_lines > largestLines) {
1447
+ largestLines = packet.total_lines;
1448
+ largestEstimatedTokens = packet.estimated_tokens;
1449
+ largestPacketId = packet.packet_id;
1424
1450
  }
1425
- const totalLines = Object.values(task.file_line_counts ?? {}).reduce((a, b) => a + b, 0);
1426
- if (totalLines > largestLines) {
1427
- largestLines = totalLines;
1428
- largestTask = task.task_id;
1451
+ if (packet.total_lines > 2500) {
1452
+ warnings.push({
1453
+ code: "large_packet",
1454
+ message: `large packet ${packet.packet_id} (~${packet.total_lines} lines) may hit quota limits`,
1455
+ });
1429
1456
  }
1430
- if (totalLines > 1500) {
1431
- process.stderr.write(`Warning: large task ${task.task_id} (~${totalLines} lines) may hit quota limits\n`);
1457
+ for (const task of packetTasks) {
1458
+ if (!lensDefs[task.lens]) {
1459
+ warnings.push({
1460
+ code: "missing_lens_definition",
1461
+ message: `no lens definition for '${task.lens}' (task ${task.task_id})`,
1462
+ });
1463
+ }
1432
1464
  }
1433
- const fileList = task.file_paths.map(p => {
1434
- const lines = task.file_line_counts?.[p] ?? 0;
1435
- return `- ${p} (${lines} lines)`;
1465
+ const fileList = packet.file_paths.map((path) => {
1466
+ const lines = packet.file_line_counts[path] ?? 0;
1467
+ return `- ${path} (${lines} lines)`;
1436
1468
  }).join("\n");
1469
+ const taskSections = packetTasks.flatMap((task) => {
1470
+ const lensDef = lensDefs[task.lens];
1471
+ const outputPath = outputPathByTaskId.get(task.task_id);
1472
+ return [
1473
+ `### ${task.task_id}`,
1474
+ `unit_id: ${task.unit_id}`,
1475
+ `pass_id: ${task.pass_id}`,
1476
+ `lens: ${task.lens}`,
1477
+ `output_path: ${outputPath}`,
1478
+ `rationale: ${task.rationale}`,
1479
+ "",
1480
+ `Lens guidance: ${lensDef?.description ?? task.lens}`,
1481
+ `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
1482
+ "",
1483
+ ];
1484
+ });
1485
+ const validationCommands = packetTasks.map((task) => ` "${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir "${artifactsDir}"`);
1486
+ const outputPaths = Object.fromEntries(packetTasks.map((task) => [
1487
+ task.task_id,
1488
+ outputPathByTaskId.get(task.task_id) ?? "",
1489
+ ]));
1437
1490
  const prompt = [
1438
- "You are a code auditor. Review the files below under the specified lens.",
1491
+ "You are a code auditor. Review this packet once, then produce one result file per listed task.",
1439
1492
  "",
1440
- "## Task",
1441
- `task_id: ${task.task_id}`,
1442
- `unit_id: ${task.unit_id}`,
1443
- `pass_id: ${task.pass_id}`,
1444
- `lens: ${task.lens}`,
1493
+ "## Packet",
1494
+ `packet_id: ${packet.packet_id}`,
1495
+ `task_count: ${packet.task_ids.length}`,
1496
+ `lenses: ${packet.lenses.join(", ")}`,
1497
+ `estimated_tokens: ${packet.estimated_tokens}`,
1445
1498
  "",
1446
1499
  "## Files to read",
1447
1500
  "Use your Read tool. Paths are repo-relative from the current working directory.",
1448
1501
  fileList,
1449
1502
  "",
1450
- `## Lens: ${task.lens}`,
1451
- lensDef?.description ?? task.lens,
1452
- "",
1453
- `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
1454
- "",
1503
+ "## Tasks",
1504
+ ...taskSections,
1455
1505
  "## Output",
1456
- `Write a single JSON object to: ${outputPath}`,
1457
- "Write only this assigned task's AuditResult object. Do not edit source files,",
1506
+ "Write exactly one JSON object for each task to that task's output_path.",
1507
+ "Do not combine the task results into one file. Do not edit source files,",
1458
1508
  "remediate findings, create extra task results, or run unrelated audits.",
1459
1509
  "",
1460
- "Required fields:",
1461
- " task_id copy from task metadata above",
1462
- " unit_id copy from task metadata above",
1463
- " pass_id copy from task metadata above",
1464
- " lens copy from task metadata above",
1465
- " file_coverage [{path, total_lines}] one entry per file; use the line counts listed above",
1466
- " findings [] or array of finding objects (see below)",
1510
+ "Required AuditResult fields:",
1511
+ " task_id copy from the task metadata",
1512
+ " unit_id copy from the task metadata",
1513
+ " pass_id copy from the task metadata",
1514
+ " lens copy from the task metadata",
1515
+ " file_coverage [{path, total_lines}] - one entry per assigned file; use the line counts listed above",
1516
+ " findings [] or array of finding objects",
1467
1517
  "",
1468
1518
  "Each finding object:",
1469
1519
  " id unique ID, e.g. \"COR-001\"",
1470
1520
  " title short title",
1471
- " category correctness|architecture|maintainability|security|reliability|performance|data_integrity|tests|operability|config_deployment",
1521
+ " category specific finding category, such as missing-validation or command-execution",
1472
1522
  " severity critical|high|medium|low|info",
1473
1523
  " confidence high|medium|low",
1474
- ` lens "${task.lens}" — must match task lens exactly`,
1475
- " summary 12 sentence description",
1476
- " affected_files [{path, line_start?, line_end?, symbol?}] objects, not strings; min 1 entry",
1477
- " evidence [\"path/to/file.ts:42 description of what you see there\"] min 1 entry",
1524
+ " lens must match the task lens exactly",
1525
+ " summary 1-2 sentence description",
1526
+ " affected_files [{path, line_start?, line_end?, symbol?}] - objects, not strings; min 1 entry",
1527
+ " evidence [\"path/to/file.ts:42 - description of what you see there\"] - min 1 entry",
1478
1528
  "",
1479
1529
  "Constraints:",
1480
- "1. line_end must not exceed the file's actual line count (use counts listed above)",
1481
- "2. affected_files entries are OBJECTS with a \"path\" key NOT plain strings",
1482
- "3. Only reference files from the list above",
1483
- "4. findings: [] is correct when you find nothing genuine",
1530
+ "1. line_end must not exceed the file's actual line count.",
1531
+ "2. affected_files entries are objects with a path key, not plain strings.",
1532
+ "3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
1533
+ "4. findings: [] is correct when you find nothing genuine.",
1484
1534
  "",
1485
1535
  "## Validate",
1486
- "After writing your result, run:",
1487
- ` "${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir "${artifactsDir}"`,
1536
+ "After writing every result, run:",
1537
+ ...validationCommands,
1538
+ "",
1539
+ "Exit 0 means valid. Non-zero: read the errors, fix the JSON, rewrite the file, run again. Retry up to 3 times.",
1488
1540
  "",
1489
- "Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.",
1541
+ "## Final response",
1542
+ `After every validation command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
1490
1543
  ].join("\n");
1491
1544
  await writeFile(promptPath, prompt, "utf8");
1492
- const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalLines} lines) — ${task.lens} lens`;
1493
- plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt_path: promptPath });
1545
+ plan.push({
1546
+ packet_id: packet.packet_id,
1547
+ task_id: packet.task_ids.length === 1 ? packet.task_ids[0] : packet.packet_id,
1548
+ task_ids: packet.task_ids,
1549
+ description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)`,
1550
+ output_paths: outputPaths,
1551
+ prompt_path: promptPath,
1552
+ lenses: packet.lenses,
1553
+ file_paths: packet.file_paths,
1554
+ total_lines: packet.total_lines,
1555
+ estimated_tokens: packet.estimated_tokens,
1556
+ });
1494
1557
  }
1495
1558
  await writeJsonFile(dispatchPlanPath, plan);
1496
- console.log(`Wrote dispatch-plan.json ${plan.length} tasks ready for dispatch`);
1497
- if (largestTask)
1498
- console.log(`Largest task: ${largestTask} (~${largestLines} lines)`);
1559
+ const warningsPath = warnings.length > 0
1560
+ ? join(runDir, "dispatch-warnings.json")
1561
+ : null;
1562
+ if (warningsPath) {
1563
+ await writeJsonFile(warningsPath, warnings);
1564
+ }
1565
+ console.log(JSON.stringify({
1566
+ run_id: runId,
1567
+ dispatch_plan_path: dispatchPlanPath,
1568
+ packet_count: plan.length,
1569
+ task_count: orderedTasks.length,
1570
+ largest_packet: largestPacketId
1571
+ ? {
1572
+ packet_id: largestPacketId,
1573
+ total_lines: largestLines,
1574
+ estimated_tokens: largestEstimatedTokens,
1575
+ }
1576
+ : null,
1577
+ warning_count: warnings.length,
1578
+ dispatch_warnings_path: warningsPath,
1579
+ }, null, 2));
1499
1580
  }
1500
1581
  async function cmdMergeAndIngest(argv) {
1501
1582
  const runId = getFlag(argv, "--run-id");
@@ -1507,12 +1588,13 @@ async function cmdMergeAndIngest(argv) {
1507
1588
  const auditResultsPath = join(runDir, "audit-results.json");
1508
1589
  const taskPath = join(runDir, "task.json");
1509
1590
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1591
+ const workerTask = await readJsonFile(taskPath);
1510
1592
  let allTasks = [];
1511
1593
  try {
1512
1594
  allTasks = await readJsonFile(tasksPath);
1513
1595
  }
1514
1596
  catch { /* may not exist */ }
1515
- const taskMap = new Map(allTasks.map(t => [t.task_id, t]));
1597
+ const lineIndex = Object.fromEntries(allTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
1516
1598
  let files;
1517
1599
  try {
1518
1600
  files = (await readdir(taskResultsDir)).filter(f => f.endsWith(".json")).sort();
@@ -1533,19 +1615,29 @@ async function cmdMergeAndIngest(argv) {
1533
1615
  failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
1534
1616
  continue;
1535
1617
  }
1536
- const taskId = typeof obj.task_id === "string"
1537
- ? String(obj.task_id) : undefined;
1618
+ const record = obj && typeof obj === "object" && !Array.isArray(obj)
1619
+ ? obj
1620
+ : undefined;
1621
+ const taskId = typeof record?.task_id === "string"
1622
+ ? String(record.task_id) : undefined;
1623
+ const resultErrors = [];
1538
1624
  if (taskId) {
1539
- seenTaskIds.add(taskId);
1625
+ if (seenTaskIds.has(taskId)) {
1626
+ resultErrors.push(`Duplicate audit result for assigned task '${taskId}'.`);
1627
+ }
1628
+ else {
1629
+ seenTaskIds.add(taskId);
1630
+ }
1540
1631
  }
1541
- const matchingTask = taskId ? taskMap.get(taskId) : undefined;
1542
- const issues = validateAuditResults([obj], matchingTask ? [matchingTask] : [], { lineIndex: matchingTask?.file_line_counts ?? {} });
1543
- const errors = issues.filter(i => i.severity === "error");
1544
- if (errors.length === 0) {
1632
+ const issues = validateAuditResults([obj], allTasks, { lineIndex });
1633
+ resultErrors.push(...issues
1634
+ .filter(i => i.severity === "error")
1635
+ .map(i => i.message));
1636
+ if (resultErrors.length === 0) {
1545
1637
  passing.push(obj);
1546
1638
  }
1547
1639
  else {
1548
- failing.push({ task_id: taskId ?? filename, errors: errors.map(i => i.message) });
1640
+ failing.push({ task_id: taskId ?? filename, errors: resultErrors });
1549
1641
  }
1550
1642
  }
1551
1643
  for (const task of allTasks) {
@@ -1562,9 +1654,37 @@ async function cmdMergeAndIngest(argv) {
1562
1654
  await writeJsonFile(failedTasksPath, failing);
1563
1655
  throw new Error(`${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
1564
1656
  }
1565
- process.stderr.write(`✓ ${passing.length}/${files.length} results merged → ${auditResultsPath}\n`);
1566
- // Ingest: run worker-run logic against the merged results file
1567
- await cmdWorkerRun([argv[0], argv[1], "worker-run", "--task", taskPath, "--artifacts-dir", artifactsDir]);
1657
+ const findingCount = passing.reduce((sum, result) => sum + result.findings.length, 0);
1658
+ const result = await runAuditStep({
1659
+ root: workerTask.repo_root,
1660
+ artifactsDir,
1661
+ preferredExecutor: "result_ingestion_executor",
1662
+ auditResultsPath,
1663
+ });
1664
+ const workerResult = buildWorkerResult({
1665
+ runId,
1666
+ obligationId: workerTask.obligation_id,
1667
+ status: result.progress_made ? "completed" : "no_progress",
1668
+ progressMade: result.progress_made,
1669
+ selectedExecutor: result.selected_executor,
1670
+ artifactsWritten: result.artifacts_written,
1671
+ summary: result.progress_summary,
1672
+ nextLikelyStep: result.next_likely_step,
1673
+ errors: [],
1674
+ });
1675
+ await writeJsonFile(workerTask.result_path, workerResult);
1676
+ console.log(JSON.stringify({
1677
+ run_id: runId,
1678
+ status: workerResult.status,
1679
+ accepted_count: passing.length,
1680
+ rejected_count: 0,
1681
+ finding_count: findingCount,
1682
+ audit_results_path: auditResultsPath,
1683
+ selected_executor: workerResult.selected_executor,
1684
+ progress_made: workerResult.progress_made,
1685
+ progress_summary: workerResult.summary,
1686
+ next_likely_step: workerResult.next_likely_step,
1687
+ }, null, 2));
1568
1688
  }
1569
1689
  async function cmdValidateResult(argv) {
1570
1690
  const runId = getFlag(argv, "--run-id");
@@ -1,4 +1,8 @@
1
1
  import type { RepoManifest } from "../types.js";
2
2
  import type { FileDisposition } from "../types/disposition.js";
3
3
  import type { GraphBundle } from "../types/graph.js";
4
- export declare function buildGraphBundle(repoManifest: RepoManifest, disposition?: FileDisposition): GraphBundle;
4
+ export interface BuildGraphBundleOptions {
5
+ fileContents?: Record<string, string>;
6
+ }
7
+ export declare function buildGraphBundleFromFs(repoManifest: RepoManifest, root: string, disposition?: FileDisposition): Promise<GraphBundle>;
8
+ export declare function buildGraphBundle(repoManifest: RepoManifest, disposition?: FileDisposition, options?: BuildGraphBundleOptions): GraphBundle;