auditor-lambda 0.3.2 → 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 (47) hide show
  1. package/README.md +6 -1
  2. package/audit-code-wrapper-lib.mjs +78 -5
  3. package/dist/cli.js +205 -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/io/json.js +3 -3
  11. package/dist/io/runArtifacts.js +4 -0
  12. package/dist/mcp/server.js +24 -11
  13. package/dist/orchestrator/advance.js +1 -1
  14. package/dist/orchestrator/dependencyMap.js +18 -0
  15. package/dist/orchestrator/internalExecutors.d.ts +1 -1
  16. package/dist/orchestrator/internalExecutors.js +120 -33
  17. package/dist/orchestrator/reviewPackets.d.ts +14 -0
  18. package/dist/orchestrator/reviewPackets.js +300 -0
  19. package/dist/orchestrator/selectiveDeepening.d.ts +14 -0
  20. package/dist/orchestrator/selectiveDeepening.js +392 -0
  21. package/dist/orchestrator/state.js +6 -1
  22. package/dist/orchestrator/taskBuilder.d.ts +16 -0
  23. package/dist/orchestrator/taskBuilder.js +68 -11
  24. package/dist/orchestrator.js +53 -2
  25. package/dist/prompts/renderWorkerPrompt.js +11 -4
  26. package/dist/providers/index.js +1 -1
  27. package/dist/supervisor/sessionConfig.js +1 -1
  28. package/dist/types/graph.d.ts +1 -0
  29. package/dist/types/reviewPlanning.d.ts +41 -0
  30. package/dist/types/reviewPlanning.js +1 -0
  31. package/dist/validation/artifacts.js +13 -0
  32. package/dist/validation/sessionConfig.js +1 -1
  33. package/docs/agent-integrations.md +17 -8
  34. package/docs/bootstrap-install.md +3 -0
  35. package/docs/dispatch-implementation-plan.md +179 -481
  36. package/docs/next-steps.md +13 -8
  37. package/docs/product-direction.md +5 -3
  38. package/docs/run-flow.md +23 -30
  39. package/docs/session-config.md +10 -3
  40. package/docs/supervisor.md +12 -4
  41. package/docs/workflow-refactor-brief.md +85 -147
  42. package/package.json +1 -1
  43. package/schemas/audit_results.schema.json +10 -0
  44. package/schemas/finding.schema.json +1 -15
  45. package/schemas/graph_bundle.schema.json +16 -0
  46. package/skills/audit-code/SKILL.md +12 -3
  47. package/skills/audit-code/audit-code.prompt.md +87 -57
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));
@@ -1312,6 +1318,9 @@ async function cmdRunToCompletion(argv) {
1312
1318
  next_likely_step: state.status === "complete" ? null : decision.selected_obligation,
1313
1319
  providerName: provider.name,
1314
1320
  });
1321
+ if (state.status === "complete") {
1322
+ await promoteFinalAuditReport({ artifactsDir, repoRoot: root });
1323
+ }
1315
1324
  }
1316
1325
  async function cmdWorkerRun(argv) {
1317
1326
  const taskPath = getFlag(argv, "--task");
@@ -1405,92 +1414,169 @@ async function cmdPrepareDispatch(argv) {
1405
1414
  const taskResultsDir = join(runDir, "task-results");
1406
1415
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1407
1416
  const tasks = await readJsonFile(tasksPath);
1417
+ const bundle = await loadArtifactBundle(artifactsDir);
1408
1418
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
1409
1419
  const lensDefs = await readJsonFile(lensDefsPath);
1410
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
+ ]));
1411
1435
  const plan = [];
1412
- let largestTask = null;
1436
+ let largestPacketId = null;
1413
1437
  let largestLines = 0;
1414
- for (const task of tasks) {
1415
- const sanitized = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1416
- 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, "_");
1417
1442
  const promptPath = join(taskResultsDir, `${sanitized}.prompt.md`);
1418
- const lensDef = lensDefs[task.lens];
1419
- if (!lensDef) {
1420
- 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;
1421
1450
  }
1422
- const totalLines = Object.values(task.file_line_counts ?? {}).reduce((a, b) => a + b, 0);
1423
- if (totalLines > largestLines) {
1424
- largestLines = totalLines;
1425
- 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
+ });
1426
1456
  }
1427
- if (totalLines > 1500) {
1428
- 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
+ }
1429
1464
  }
1430
- const fileList = task.file_paths.map(p => {
1431
- const lines = task.file_line_counts?.[p] ?? 0;
1432
- 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)`;
1433
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
+ ]));
1434
1490
  const prompt = [
1435
- "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.",
1436
1492
  "",
1437
- "## Task",
1438
- `task_id: ${task.task_id}`,
1439
- `unit_id: ${task.unit_id}`,
1440
- `pass_id: ${task.pass_id}`,
1441
- `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}`,
1442
1498
  "",
1443
1499
  "## Files to read",
1444
1500
  "Use your Read tool. Paths are repo-relative from the current working directory.",
1445
1501
  fileList,
1446
1502
  "",
1447
- `## Lens: ${task.lens}`,
1448
- lensDef?.description ?? task.lens,
1449
- "",
1450
- `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
1451
- "",
1503
+ "## Tasks",
1504
+ ...taskSections,
1452
1505
  "## Output",
1453
- `Write a single JSON object to: ${outputPath}`,
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,",
1508
+ "remediate findings, create extra task results, or run unrelated audits.",
1454
1509
  "",
1455
- "Required fields:",
1456
- " task_id copy from task metadata above",
1457
- " unit_id copy from task metadata above",
1458
- " pass_id copy from task metadata above",
1459
- " lens copy from task metadata above",
1460
- " file_coverage [{path, total_lines}] one entry per file; use the line counts listed above",
1461
- " 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",
1462
1517
  "",
1463
1518
  "Each finding object:",
1464
1519
  " id unique ID, e.g. \"COR-001\"",
1465
1520
  " title short title",
1466
- " 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",
1467
1522
  " severity critical|high|medium|low|info",
1468
1523
  " confidence high|medium|low",
1469
- ` lens "${task.lens}" — must match task lens exactly`,
1470
- " summary 12 sentence description",
1471
- " affected_files [{path, line_start?, line_end?, symbol?}] objects, not strings; min 1 entry",
1472
- " 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",
1473
1528
  "",
1474
1529
  "Constraints:",
1475
- "1. line_end must not exceed the file's actual line count (use counts listed above)",
1476
- "2. affected_files entries are OBJECTS with a \"path\" key NOT plain strings",
1477
- "3. Only reference files from the list above",
1478
- "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.",
1479
1534
  "",
1480
1535
  "## Validate",
1481
- "After writing your result, run:",
1482
- ` "${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.",
1483
1540
  "",
1484
- "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>`,
1485
1543
  ].join("\n");
1486
1544
  await writeFile(promptPath, prompt, "utf8");
1487
- const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalLines} lines) — ${task.lens} lens`;
1488
- 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
+ });
1489
1557
  }
1490
1558
  await writeJsonFile(dispatchPlanPath, plan);
1491
- console.log(`Wrote dispatch-plan.json ${plan.length} tasks ready for dispatch`);
1492
- if (largestTask)
1493
- 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));
1494
1580
  }
1495
1581
  async function cmdMergeAndIngest(argv) {
1496
1582
  const runId = getFlag(argv, "--run-id");
@@ -1502,12 +1588,13 @@ async function cmdMergeAndIngest(argv) {
1502
1588
  const auditResultsPath = join(runDir, "audit-results.json");
1503
1589
  const taskPath = join(runDir, "task.json");
1504
1590
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1591
+ const workerTask = await readJsonFile(taskPath);
1505
1592
  let allTasks = [];
1506
1593
  try {
1507
1594
  allTasks = await readJsonFile(tasksPath);
1508
1595
  }
1509
1596
  catch { /* may not exist */ }
1510
- 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 ?? {})));
1511
1598
  let files;
1512
1599
  try {
1513
1600
  files = (await readdir(taskResultsDir)).filter(f => f.endsWith(".json")).sort();
@@ -1517,6 +1604,7 @@ async function cmdMergeAndIngest(argv) {
1517
1604
  }
1518
1605
  const passing = [];
1519
1606
  const failing = [];
1607
+ const seenTaskIds = new Set();
1520
1608
  for (const filename of files) {
1521
1609
  const filePath = join(taskResultsDir, filename);
1522
1610
  let obj;
@@ -1527,26 +1615,76 @@ async function cmdMergeAndIngest(argv) {
1527
1615
  failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
1528
1616
  continue;
1529
1617
  }
1530
- const taskId = typeof obj.task_id === "string"
1531
- ? String(obj.task_id) : undefined;
1532
- const matchingTask = taskId ? taskMap.get(taskId) : undefined;
1533
- const issues = validateAuditResults([obj], matchingTask ? [matchingTask] : [], { lineIndex: matchingTask?.file_line_counts ?? {} });
1534
- const errors = issues.filter(i => i.severity === "error");
1535
- if (errors.length === 0) {
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 = [];
1624
+ if (taskId) {
1625
+ if (seenTaskIds.has(taskId)) {
1626
+ resultErrors.push(`Duplicate audit result for assigned task '${taskId}'.`);
1627
+ }
1628
+ else {
1629
+ seenTaskIds.add(taskId);
1630
+ }
1631
+ }
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) {
1536
1637
  passing.push(obj);
1537
1638
  }
1538
1639
  else {
1539
- failing.push({ task_id: taskId ?? filename, errors: errors.map(i => i.message) });
1640
+ failing.push({ task_id: taskId ?? filename, errors: resultErrors });
1641
+ }
1642
+ }
1643
+ for (const task of allTasks) {
1644
+ if (!seenTaskIds.has(task.task_id)) {
1645
+ failing.push({
1646
+ task_id: task.task_id,
1647
+ errors: ["Missing audit result for assigned task."],
1648
+ });
1540
1649
  }
1541
1650
  }
1542
1651
  await writeJsonFile(auditResultsPath, passing);
1543
1652
  if (failing.length > 0) {
1544
- await writeJsonFile(join(runDir, "failed-tasks.json"), failing);
1545
- process.stderr.write(`${failing.length} task(s) excluded — see ${join(runDir, "failed-tasks.json")}\n`);
1653
+ const failedTasksPath = join(runDir, "failed-tasks.json");
1654
+ await writeJsonFile(failedTasksPath, failing);
1655
+ throw new Error(`${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
1546
1656
  }
1547
- process.stderr.write(`✓ ${passing.length}/${files.length} results merged → ${auditResultsPath}\n`);
1548
- // Ingest: run worker-run logic against the merged results file
1549
- 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));
1550
1688
  }
1551
1689
  async function cmdValidateResult(argv) {
1552
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;