auditor-lambda 0.2.17 → 0.3.0

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.
@@ -5,17 +5,21 @@ import { validateResult } from "./validate.mjs";
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
- const PROJECT_ROOT = resolve(__dirname, "..");
9
8
 
10
9
  // Parse --run-id
11
10
  const runIdIdx = process.argv.indexOf("--run-id");
12
11
  if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
13
- console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id>");
12
+ console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id> [--artifacts-dir <dir>]");
14
13
  process.exit(1);
15
14
  }
16
15
  const run_id = process.argv[runIdIdx + 1];
17
16
 
18
- const artifactsDir = join(PROJECT_ROOT, ".audit-artifacts");
17
+ // Parse --artifacts-dir (default: CWD/.audit-artifacts)
18
+ const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
19
+ const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
20
+ ? resolve(process.argv[artifactsDirIdx + 1])
21
+ : join(process.cwd(), ".audit-artifacts");
22
+
19
23
  const taskResultsDir = join(artifactsDir, "runs", run_id, "task-results");
20
24
  const auditResultsPath = join(artifactsDir, "runs", run_id, "audit-results.json");
21
25
  const failedTasksPath = join(artifactsDir, "runs", run_id, "failed-tasks.json");
@@ -69,9 +73,9 @@ writeFileSync(auditResultsPath, JSON.stringify(passing, null, 2));
69
73
 
70
74
  if (failing.length > 0) {
71
75
  writeFileSync(failedTasksPath, JSON.stringify(failing, null, 2));
72
- console.warn(`${failing.length} task(s) failed validation and were excluded:`);
76
+ process.stderr.write(`${failing.length} task(s) failed validation and were excluded:\n`);
73
77
  for (const f of failing) {
74
- console.warn(` ✗ ${f.task_id}: ${f.errors[0]}`);
78
+ process.stderr.write(` ✗ ${f.task_id}: ${f.errors[0]}\n`);
75
79
  }
76
80
  }
77
81
 
@@ -1,143 +1,131 @@
1
1
  import { dirname, resolve, join } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
3
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
4
 
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
- const PROJECT_ROOT = resolve(__dirname, "..");
7
+ const PACKAGE_ROOT = resolve(__dirname, "..");
8
8
 
9
9
  // Parse --run-id
10
10
  const runIdIdx = process.argv.indexOf("--run-id");
11
11
  if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
12
- console.error("Usage: node dispatch/prepare-dispatch.mjs --run-id <run_id>");
12
+ console.error("Usage: node dispatch/prepare-dispatch.mjs --run-id <run_id> [--artifacts-dir <dir>]");
13
13
  process.exit(1);
14
14
  }
15
15
  const run_id = process.argv[runIdIdx + 1];
16
16
 
17
- const artifactsDir = join(PROJECT_ROOT, ".audit-artifacts");
17
+ // Parse --artifacts-dir (default: CWD/.audit-artifacts)
18
+ const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
19
+ const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
20
+ ? resolve(process.argv[artifactsDirIdx + 1])
21
+ : join(process.cwd(), ".audit-artifacts");
22
+
18
23
  const runDir = join(artifactsDir, "runs", run_id);
19
24
  const tasksPath = join(runDir, "pending-audit-tasks.json");
25
+ const taskResultsDir = join(runDir, "task-results");
20
26
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
21
27
 
22
- if (!existsSync(tasksPath)) {
23
- console.error(`File not found: ${tasksPath}`);
28
+ let tasks;
29
+ try {
30
+ tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
31
+ } catch (e) {
32
+ console.error(`Cannot read ${tasksPath}: ${e.message}`);
24
33
  process.exit(1);
25
34
  }
26
35
 
27
- const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
28
36
  const lensDefinitions = JSON.parse(
29
37
  readFileSync(join(__dirname, "lens-definitions.json"), "utf8")
30
38
  );
31
- const auditResultSchema = JSON.parse(
32
- readFileSync(join(PROJECT_ROOT, "schemas", "audit_result.schema.json"), "utf8")
33
- );
34
- const findingSchema = JSON.parse(
35
- readFileSync(join(PROJECT_ROOT, "schemas", "finding.schema.json"), "utf8")
36
- );
37
39
 
38
- function buildPrompt(task, lensDef, auditResultSchema, findingSchema, outputPath, runId, artifactsDir) {
39
- const fallback = {
40
- task_id: task.task_id,
41
- unit_id: task.unit_id,
42
- pass_id: task.pass_id,
43
- lens: task.lens,
44
- file_coverage: task.file_paths.map(p => ({ path: p, total_lines: task.file_line_counts[p] })),
45
- findings: [],
46
- notes: ["Validation failed after 3 attempts — empty result written as fallback."]
47
- };
40
+ mkdirSync(taskResultsDir, { recursive: true });
48
41
 
49
- return `You are a code auditor. Perform a bounded audit of the files listed below under the specified lens.
42
+ function buildPrompt(task, lensDef, outputPath, runId, artifactsDir) {
43
+ const fileList = task.file_paths.map(p => {
44
+ const lines = task.file_line_counts?.[p] ?? 0;
45
+ return `- ${p} (${lines} lines)`;
46
+ }).join("\n");
50
47
 
51
- ## Task metadata
52
- ${JSON.stringify(task, null, 2)}
48
+ return `You are a code auditor. Review the files below under the specified lens.
53
49
 
54
- ## Files to read
55
- Read each path in task.file_paths using your Read tool. The repo root is the current working directory — paths are repo-relative (e.g. "src/foo.ts").
50
+ ## Task
51
+ task_id: ${task.task_id}
52
+ unit_id: ${task.unit_id}
53
+ pass_id: ${task.pass_id}
54
+ lens: ${task.lens}
56
55
 
57
- file_line_counts gives the expected total line count for each file. Use those exact values for file_coverage[].total_lines in your result.
56
+ ## Files to read
57
+ Use your Read tool. Paths are repo-relative from the current working directory.
58
+ ${fileList}
58
59
 
59
60
  ## Lens: ${task.lens}
60
- ${lensDef.description}
61
-
62
- Do NOT report: ${lensDef.do_not_report}
63
-
64
- ## Output format
65
- Write your result as a single JSON **object** (not an array) to this exact path:
66
- ${outputPath}
67
-
68
- The result must conform to the following schema:
69
-
70
- ### audit_result.schema.json
71
- ${JSON.stringify(auditResultSchema, null, 2)}
72
-
73
- ### finding.schema.json
74
- ${JSON.stringify(findingSchema, null, 2)}
75
-
76
- ## Hard constraints (violations will fail validation)
77
- 1. NEVER set line_end higher than the file's actual line count.
78
- Use file_line_counts as your reference. If in doubt, leave line_end omitted.
79
- 2. Every finding MUST have ALL required fields:
80
- id, title, category, severity, confidence, lens, summary, affected_files, evidence
81
- 3. lens on every finding must be exactly "${task.lens}"
82
- 4. No fields outside the schema. Forbidden: "recommendation", "tags", "description" (use "summary").
83
- 5. evidence[] must contain at least one specific file:line reference.
84
- Format: "path/to/file.ts:42 - brief description of what you see there"
85
- 6. affected_files[] entries are OBJECTS with a "path" key — NOT plain strings.
86
- Example: {"path": "src/foo.ts", "line_start": 10, "line_end": 20, "symbol": "myFunc"}
87
- 7. Only reference file paths that appear in this task's file_paths.
88
- 8. findings: [] is correct when you genuinely find nothing. Do not invent findings.
89
-
90
- ## Validation step (required)
61
+ ${lensDef?.description ?? task.lens}
62
+
63
+ Do NOT report: ${lensDef?.do_not_report ?? "N/A"}
64
+
65
+ ## Output
66
+ Write a single JSON object to: ${outputPath}
67
+
68
+ Required fields:
69
+ task_id copy from task metadata above
70
+ unit_id copy from task metadata above
71
+ pass_id copy from task metadata above
72
+ lens copy from task metadata above
73
+ file_coverage [{path, total_lines}] — one entry per file; use the line counts listed above
74
+ findings [] or array of finding objects (see below)
75
+
76
+ Each finding object (omit optional fields if not applicable):
77
+ id unique ID, e.g. "SEC-001"
78
+ title short title
79
+ category correctness|architecture|maintainability|security|reliability|performance|data_integrity|tests|operability|config_deployment
80
+ severity critical|high|medium|low|info
81
+ confidence high|medium|low
82
+ lens "${task.lens}" must match task lens exactly
83
+ summary 1–2 sentence description
84
+ affected_files [{path, line_start?, line_end?, symbol?}] objects, not strings; min 1 entry
85
+ evidence ["path/to/file.ts:42 description of what you see there"] — min 1 entry
86
+
87
+ Constraints:
88
+ 1. line_end must not exceed the file's actual line count (use the counts listed above)
89
+ 2. affected_files entries are OBJECTS with a "path" key NOT plain strings
90
+ 3. Only reference files from the list above
91
+ 4. findings: [] is correct when you find nothing genuine — do not invent findings
92
+
93
+ ## Validate
91
94
  After writing your result, run:
92
- node dispatch/validate-result.mjs ${runId} ${task.task_id}
93
-
94
- - If it exits 0: you are done. Stop.
95
- - If it exits non-zero: read the error output, fix the JSON, rewrite the file, run again.
96
- - Repeat up to 3 times.
95
+ audit-code validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir ${artifactsDir}
97
96
 
98
- If you cannot produce a valid result after 3 attempts, write this fallback (substituting real values):
99
- ${JSON.stringify(fallback, null, 2)}
100
-
101
- Then validate the fallback passes before finishing.`;
97
+ Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.`;
102
98
  }
103
99
 
104
- mkdirSync(join(runDir, "task-results"), { recursive: true });
105
-
106
100
  const plan = [];
107
101
  let largestTask = null;
108
102
  let largestLines = 0;
109
103
 
110
104
  for (const task of tasks) {
111
105
  const sanitizedId = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
112
- const outputPath = join(runDir, "task-results", sanitizedId + ".json");
106
+ const outputPath = join(taskResultsDir, sanitizedId + ".json");
107
+ const promptPath = join(taskResultsDir, sanitizedId + ".prompt.md");
113
108
  const lensDef = lensDefinitions[task.lens];
114
109
 
115
110
  if (!lensDef) {
116
- console.warn(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})`);
111
+ process.stderr.write(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})\n`);
117
112
  }
118
113
 
119
- const totalFileLines = Object.values(task.file_line_counts).reduce((a, b) => a + b, 0);
120
- const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalFileLines} lines) — ${task.lens} lens`;
121
- const prompt = buildPrompt(
122
- task,
123
- lensDef ?? { description: task.lens, do_not_report: "N/A" },
124
- auditResultSchema,
125
- findingSchema,
126
- outputPath,
127
- run_id,
128
- artifactsDir
129
- );
114
+ const totalFileLines = Object.values(task.file_line_counts ?? {}).reduce((a, b) => a + b, 0);
130
115
 
131
116
  if (totalFileLines > largestLines) {
132
117
  largestLines = totalFileLines;
133
118
  largestTask = task.task_id;
134
119
  }
135
-
136
120
  if (totalFileLines > 1500) {
137
- console.warn(`Warning: large task ${task.task_id} (~${totalFileLines} lines) may hit quota limits`);
121
+ process.stderr.write(`Warning: large task ${task.task_id} (~${totalFileLines} lines) may hit quota limits\n`);
138
122
  }
139
123
 
140
- plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt });
124
+ const prompt = buildPrompt(task, lensDef, outputPath, run_id, artifactsDir);
125
+ writeFileSync(promptPath, prompt, "utf8");
126
+
127
+ const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalFileLines} lines) — ${task.lens} lens`;
128
+ plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt_path: promptPath });
141
129
  }
142
130
 
143
131
  writeFileSync(dispatchPlanPath, JSON.stringify(plan, null, 2));
@@ -146,10 +134,3 @@ console.log(`Wrote dispatch-plan.json — ${plan.length} tasks ready for dispatc
146
134
  if (largestTask) {
147
135
  console.log(`Largest task: ${largestTask} (~${largestLines} lines)`);
148
136
  }
149
- console.log("");
150
- console.log("--- ORCHESTRATOR INSTRUCTIONS ---");
151
- console.log("Read dispatch-plan.json. For each entry, fire one Agent call with:");
152
- console.log(" description: <entry.description>");
153
- console.log(" prompt: <entry.prompt>");
154
- console.log(`Fire all ${plan.length} calls in a single message for parallel execution.`);
155
- console.log(`When all complete, run: node dispatch/merge-results.mjs --run-id ${run_id}`);
@@ -5,27 +5,31 @@ import { validateResult } from "./validate.mjs";
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
- const PROJECT_ROOT = resolve(__dirname, "..");
9
8
 
10
- const run_id = process.argv[2];
11
- const task_id = process.argv[3];
9
+ // Support both named flags and legacy positional args:
10
+ // Named: --run-id <id> --task-id <id> [--artifacts-dir <dir>]
11
+ // Positional (legacy): <run_id> <task_id>
12
+ const runIdFlagIdx = process.argv.indexOf("--run-id");
13
+ const taskIdFlagIdx = process.argv.indexOf("--task-id");
14
+ const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
15
+
16
+ const run_id = runIdFlagIdx !== -1
17
+ ? process.argv[runIdFlagIdx + 1]
18
+ : process.argv[2];
19
+
20
+ const task_id = taskIdFlagIdx !== -1
21
+ ? process.argv[taskIdFlagIdx + 1]
22
+ : process.argv[3];
12
23
 
13
24
  if (!run_id || !task_id) {
14
- console.error("Usage: node dispatch/validate-result.mjs <run_id> <task_id>");
25
+ console.error("Usage: node dispatch/validate-result.mjs --run-id <run_id> --task-id <task_id> [--artifacts-dir <dir>]");
15
26
  process.exit(1);
16
27
  }
17
28
 
18
- // Locate artifacts_dir
19
- let artifactsDir = join(PROJECT_ROOT, ".audit-artifacts");
20
- const sessionConfigPath = join(artifactsDir, "session-config.json");
21
- if (existsSync(sessionConfigPath)) {
22
- try {
23
- const cfg = JSON.parse(readFileSync(sessionConfigPath, "utf8"));
24
- if (cfg.artifacts_dir) artifactsDir = cfg.artifacts_dir;
25
- } catch {
26
- // use default
27
- }
28
- }
29
+ // Artifacts dir: explicit flag > CWD default
30
+ const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
31
+ ? resolve(process.argv[artifactsDirIdx + 1])
32
+ : join(process.cwd(), ".audit-artifacts");
29
33
 
30
34
  const sanitized = task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
31
35
  const resultPath = join(artifactsDir, "runs", run_id, "task-results", sanitized + ".json");
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readdir, rename } from "node:fs/promises";
1
+ import { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -28,6 +28,7 @@ import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writ
28
28
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
29
29
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
30
30
  import { runAuditCodeMcpServer } from "./mcp/server.js";
31
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
32
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
32
33
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
33
34
  const DIRECT_CLI_DEFAULTS = {
@@ -111,7 +112,7 @@ function getExplicitProvider(argv) {
111
112
  return getFlag(argv, "--provider");
112
113
  }
113
114
  function resolveRunProviderName(argv, sessionConfig) {
114
- return resolveFreshSessionProviderName(getExplicitProvider(argv) ?? LOCAL_SUBPROCESS_PROVIDER_NAME, sessionConfig);
115
+ return resolveFreshSessionProviderName(getExplicitProvider(argv) ?? "auto", sessionConfig);
115
116
  }
116
117
  function chunkArray(arr, size) {
117
118
  const chunkSize = normalizePositiveInteger(size);
@@ -170,8 +171,9 @@ async function emitEnvelope(params) {
170
171
  }
171
172
  function buildManualReviewBlocker(providerName) {
172
173
  return providerName === LOCAL_SUBPROCESS_PROVIDER_NAME
173
- ? "Automatic backend steps are exhausted. Remaining semantic review now belongs to the active conversation agent. Review the dispatched files, write structured audit results to the run-scoped audit_results_path, and execute the worker_command from current-task.json exactly as written. If you intentionally want a backend bridge instead, re-run audit-code with --provider auto, --provider claude-code, --provider opencode, --provider subprocess-template, or --provider vscode-task."
174
- : "Automatic work is exhausted. Remaining audit tasks require explicit audit results or an interactive provider.";
174
+ ? "Ready for LLM review. Dispatched task files are in .audit-artifacts/dispatch/. " +
175
+ "Review the code, write audit results to the specified path, then run the worker_command to continue."
176
+ : "Audit blocked: waiting for manual audit results or interactive provider configuration.";
175
177
  }
176
178
  function shouldRunInlineExecutor(selectedExecutor) {
177
179
  return selectedExecutor !== null && selectedExecutor !== "agent";
@@ -646,7 +648,7 @@ async function cmdRunToCompletion(argv) {
646
648
  throw error;
647
649
  }
648
650
  const explicitProvider = getExplicitProvider(argv);
649
- const provider = createFreshSessionProvider(explicitProvider ?? LOCAL_SUBPROCESS_PROVIDER_NAME, sessionConfig);
651
+ const provider = createFreshSessionProvider(explicitProvider ?? "auto", sessionConfig);
650
652
  const uiMode = getUiMode(argv, sessionConfig.ui_mode ?? "headless");
651
653
  const maxRuns = getMaxRuns(argv);
652
654
  const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
@@ -1393,6 +1395,284 @@ async function cmdWorkerRun(argv) {
1393
1395
  process.exitCode = 1;
1394
1396
  }
1395
1397
  }
1398
+ async function cmdPrepareDispatch(argv) {
1399
+ const runId = getFlag(argv, "--run-id");
1400
+ if (!runId)
1401
+ throw new Error("prepare-dispatch requires --run-id <run_id>");
1402
+ const artifactsDir = getArtifactsDir(argv);
1403
+ const runDir = join(artifactsDir, "runs", runId);
1404
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
1405
+ const taskResultsDir = join(runDir, "task-results");
1406
+ const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1407
+ const tasks = await readJsonFile(tasksPath);
1408
+ const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
1409
+ const lensDefs = await readJsonFile(lensDefsPath);
1410
+ await mkdir(taskResultsDir, { recursive: true });
1411
+ const plan = [];
1412
+ let largestTask = null;
1413
+ 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`);
1417
+ 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`);
1421
+ }
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;
1426
+ }
1427
+ if (totalLines > 1500) {
1428
+ process.stderr.write(`Warning: large task ${task.task_id} (~${totalLines} lines) may hit quota limits\n`);
1429
+ }
1430
+ const fileList = task.file_paths.map(p => {
1431
+ const lines = task.file_line_counts?.[p] ?? 0;
1432
+ return `- ${p} (${lines} lines)`;
1433
+ }).join("\n");
1434
+ const prompt = [
1435
+ "You are a code auditor. Review the files below under the specified lens.",
1436
+ "",
1437
+ "## Task",
1438
+ `task_id: ${task.task_id}`,
1439
+ `unit_id: ${task.unit_id}`,
1440
+ `pass_id: ${task.pass_id}`,
1441
+ `lens: ${task.lens}`,
1442
+ "",
1443
+ "## Files to read",
1444
+ "Use your Read tool. Paths are repo-relative from the current working directory.",
1445
+ fileList,
1446
+ "",
1447
+ `## Lens: ${task.lens}`,
1448
+ lensDef?.description ?? task.lens,
1449
+ "",
1450
+ `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
1451
+ "",
1452
+ "## Output",
1453
+ `Write a single JSON object to: ${outputPath}`,
1454
+ "",
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)",
1462
+ "",
1463
+ "Each finding object:",
1464
+ " id unique ID, e.g. \"COR-001\"",
1465
+ " title short title",
1466
+ " category correctness|architecture|maintainability|security|reliability|performance|data_integrity|tests|operability|config_deployment",
1467
+ " severity critical|high|medium|low|info",
1468
+ " confidence high|medium|low",
1469
+ ` lens "${task.lens}" — must match task lens exactly`,
1470
+ " summary 1–2 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",
1473
+ "",
1474
+ "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",
1479
+ "",
1480
+ "## Validate",
1481
+ "After writing your result, run:",
1482
+ ` audit-code validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir ${artifactsDir}`,
1483
+ "",
1484
+ "Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.",
1485
+ ].join("\n");
1486
+ 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 });
1489
+ }
1490
+ 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)`);
1494
+ }
1495
+ async function cmdMergeAndIngest(argv) {
1496
+ const runId = getFlag(argv, "--run-id");
1497
+ if (!runId)
1498
+ throw new Error("merge-and-ingest requires --run-id <run_id>");
1499
+ const artifactsDir = getArtifactsDir(argv);
1500
+ const runDir = join(artifactsDir, "runs", runId);
1501
+ const taskResultsDir = join(runDir, "task-results");
1502
+ const auditResultsPath = join(runDir, "audit-results.json");
1503
+ const taskPath = join(runDir, "task.json");
1504
+ // Merge: collect all per-task result files (skip .prompt.md files)
1505
+ let files;
1506
+ try {
1507
+ files = (await readdir(taskResultsDir))
1508
+ .filter(f => f.endsWith(".json"))
1509
+ .sort();
1510
+ }
1511
+ catch {
1512
+ files = [];
1513
+ }
1514
+ const passing = [];
1515
+ const failing = [];
1516
+ for (const filename of files) {
1517
+ const filePath = join(taskResultsDir, filename);
1518
+ let obj;
1519
+ try {
1520
+ obj = JSON.parse(await readFile(filePath, "utf8"));
1521
+ }
1522
+ catch (e) {
1523
+ failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
1524
+ continue;
1525
+ }
1526
+ const r = obj;
1527
+ const missing = ["task_id", "unit_id", "pass_id", "lens", "file_coverage", "findings"].filter(f => !(f in r));
1528
+ if (missing.length > 0) {
1529
+ failing.push({ task_id: String(r.task_id ?? filename), errors: [`Missing required fields: ${missing.join(", ")}`] });
1530
+ }
1531
+ else {
1532
+ passing.push(obj);
1533
+ }
1534
+ }
1535
+ await writeJsonFile(auditResultsPath, passing);
1536
+ if (failing.length > 0) {
1537
+ await writeJsonFile(join(runDir, "failed-tasks.json"), failing);
1538
+ process.stderr.write(`${failing.length} task(s) excluded — see ${join(runDir, "failed-tasks.json")}\n`);
1539
+ }
1540
+ process.stderr.write(`✓ ${passing.length}/${files.length} results merged → ${auditResultsPath}\n`);
1541
+ // Ingest: run worker-run logic against the merged results file
1542
+ await cmdWorkerRun([argv[0], argv[1], "worker-run", "--task", taskPath, "--artifacts-dir", artifactsDir]);
1543
+ }
1544
+ const VALID_LENSES_SET = new Set([
1545
+ "correctness", "architecture", "maintainability", "security", "reliability",
1546
+ "performance", "data_integrity", "tests", "operability", "config_deployment",
1547
+ ]);
1548
+ const VALID_SEVERITIES_SET = new Set(["critical", "high", "medium", "low", "info"]);
1549
+ const VALID_CONFIDENCES_SET = new Set(["high", "medium", "low"]);
1550
+ async function cmdValidateResult(argv) {
1551
+ const runId = getFlag(argv, "--run-id");
1552
+ const taskId = getFlag(argv, "--task-id");
1553
+ if (!runId || !taskId)
1554
+ throw new Error("validate-result requires --run-id and --task-id");
1555
+ const artifactsDir = getArtifactsDir(argv);
1556
+ const sanitized = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
1557
+ const resultPath = join(artifactsDir, "runs", runId, "task-results", `${sanitized}.json`);
1558
+ const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
1559
+ let raw;
1560
+ try {
1561
+ raw = await readFile(resultPath, "utf8");
1562
+ }
1563
+ catch {
1564
+ console.error(`File not found: ${resultPath}`);
1565
+ process.exitCode = 1;
1566
+ return;
1567
+ }
1568
+ let obj;
1569
+ try {
1570
+ obj = JSON.parse(raw);
1571
+ }
1572
+ catch (e) {
1573
+ console.error(`Invalid JSON: ${e.message}`);
1574
+ process.exitCode = 1;
1575
+ return;
1576
+ }
1577
+ const errors = [];
1578
+ // Required top-level fields
1579
+ for (const field of ["task_id", "unit_id", "pass_id", "lens", "file_coverage", "findings"]) {
1580
+ if (!(field in obj))
1581
+ errors.push(`Missing required field: ${field}`);
1582
+ }
1583
+ if (errors.length > 0) {
1584
+ console.error(`✗ invalid: ${taskId}`);
1585
+ for (const e of errors)
1586
+ console.error(` ${e}`);
1587
+ process.exitCode = 1;
1588
+ return;
1589
+ }
1590
+ // Lens
1591
+ if (typeof obj.lens !== "string" || !VALID_LENSES_SET.has(obj.lens)) {
1592
+ errors.push(`lens must be one of: ${[...VALID_LENSES_SET].join("|")}`);
1593
+ }
1594
+ // file_coverage
1595
+ if (!Array.isArray(obj.file_coverage) || obj.file_coverage.length === 0) {
1596
+ errors.push("file_coverage must be a non-empty array");
1597
+ }
1598
+ else {
1599
+ for (const fc of obj.file_coverage) {
1600
+ const entry = fc;
1601
+ if (typeof entry.path !== "string")
1602
+ errors.push(`file_coverage entry missing string 'path'`);
1603
+ if (typeof entry.total_lines !== "number")
1604
+ errors.push(`file_coverage entry missing numeric 'total_lines'`);
1605
+ }
1606
+ }
1607
+ // findings
1608
+ if (!Array.isArray(obj.findings)) {
1609
+ errors.push("findings must be an array");
1610
+ }
1611
+ else {
1612
+ for (const f of obj.findings) {
1613
+ const finding = f;
1614
+ for (const field of ["id", "title", "category", "severity", "confidence", "lens", "summary"]) {
1615
+ if (typeof finding[field] !== "string")
1616
+ errors.push(`finding missing string '${field}'`);
1617
+ }
1618
+ if (typeof finding.severity === "string" && !VALID_SEVERITIES_SET.has(finding.severity)) {
1619
+ errors.push(`finding '${finding.id}': invalid severity '${finding.severity}'`);
1620
+ }
1621
+ if (typeof finding.confidence === "string" && !VALID_CONFIDENCES_SET.has(finding.confidence)) {
1622
+ errors.push(`finding '${finding.id}': invalid confidence '${finding.confidence}'`);
1623
+ }
1624
+ if (!Array.isArray(finding.affected_files) || finding.affected_files.length === 0) {
1625
+ errors.push(`finding '${finding.id}': affected_files must be a non-empty array`);
1626
+ }
1627
+ else {
1628
+ for (const af of finding.affected_files) {
1629
+ if (typeof af.path !== "string") {
1630
+ errors.push(`finding '${finding.id}': affected_files entries must be objects with a 'path' key`);
1631
+ }
1632
+ }
1633
+ }
1634
+ if (!Array.isArray(finding.evidence) || finding.evidence.length === 0) {
1635
+ errors.push(`finding '${finding.id}': evidence must be a non-empty array`);
1636
+ }
1637
+ if (typeof finding.lens === "string" && finding.lens !== obj.lens) {
1638
+ errors.push(`finding '${finding.id}': lens '${finding.lens}' does not match task lens '${obj.lens}'`);
1639
+ }
1640
+ }
1641
+ }
1642
+ // Line range bounds (load from pending-audit-tasks.json if available)
1643
+ let fileLineCounts = {};
1644
+ try {
1645
+ const tasks = await readJsonFile(tasksPath);
1646
+ const task = tasks.find(t => t.task_id === taskId);
1647
+ fileLineCounts = task?.file_line_counts ?? {};
1648
+ }
1649
+ catch { /* ignore */ }
1650
+ if (Array.isArray(obj.file_coverage) && Array.isArray(obj.findings)) {
1651
+ const coverageMap = new Map(obj.file_coverage.map(fc => [fc.path, fc.total_lines]));
1652
+ const allowedPaths = new Set(coverageMap.keys());
1653
+ for (const f of obj.findings) {
1654
+ for (const af of (f.affected_files ?? [])) {
1655
+ const p = af.path;
1656
+ if (!allowedPaths.has(p))
1657
+ errors.push(`finding '${f.id}': path '${p}' not in file_coverage`);
1658
+ if (typeof af.line_end === "number") {
1659
+ const max = coverageMap.get(p) ?? fileLineCounts[p] ?? Infinity;
1660
+ if (af.line_end > max)
1661
+ errors.push(`finding '${f.id}': line_end ${af.line_end} exceeds total_lines ${max} for ${p}`);
1662
+ }
1663
+ }
1664
+ }
1665
+ }
1666
+ if (errors.length === 0) {
1667
+ console.log(`✓ valid: ${taskId}`);
1668
+ }
1669
+ else {
1670
+ console.error(`✗ invalid: ${taskId}`);
1671
+ for (const e of errors)
1672
+ console.error(` ${e}`);
1673
+ process.exitCode = 1;
1674
+ }
1675
+ }
1396
1676
  async function cmdImportExternalAnalyzer(argv) {
1397
1677
  const artifactsDir = getArtifactsDir(argv);
1398
1678
  const sourcePath = getFlag(argv, "--external-analyzer-results", `${artifactsDir}/external_analyzer_results.json`);
@@ -1528,7 +1808,7 @@ async function cmdValidate(argv) {
1528
1808
  ...providerIssues,
1529
1809
  ];
1530
1810
  const resolvedProvider = rawSessionConfig === undefined
1531
- ? "local-subprocess"
1811
+ ? "auto"
1532
1812
  : sessionConfigIssues.length > 0
1533
1813
  ? null
1534
1814
  : resolveFreshSessionProviderName(undefined, rawSessionConfig);
@@ -1641,9 +1921,18 @@ async function main(argv) {
1641
1921
  case "mcp":
1642
1922
  await cmdMcp(argv);
1643
1923
  return;
1924
+ case "prepare-dispatch":
1925
+ await cmdPrepareDispatch(argv);
1926
+ return;
1927
+ case "merge-and-ingest":
1928
+ await cmdMergeAndIngest(argv);
1929
+ return;
1930
+ case "validate-result":
1931
+ await cmdValidateResult(argv);
1932
+ return;
1644
1933
  default:
1645
1934
  console.error(`Unknown command: ${command}`);
1646
- 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, mcp");
1935
+ 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, mcp, prepare-dispatch, merge-and-ingest, validate-result");
1647
1936
  process.exitCode = 1;
1648
1937
  }
1649
1938
  }
@@ -8,60 +8,28 @@ export function renderWorkerPrompt(task) {
8
8
  const tasksPath = task.pending_audit_tasks_path ??
9
9
  `${task.artifacts_dir}/audit_tasks.json`;
10
10
  const lines = [
11
- "You are executing one bounded audit run for audit-code.",
12
- `Run ID: ${task.run_id}`,
13
- `Repository root: ${task.repo_root}`,
14
- "",
15
- `Read the task file: ${tasksPath}`,
16
- "It contains the task(s) assigned to this run.",
17
- "",
18
- "For each task:",
19
- " 1. Read every file listed in file_paths in full using your file-reading tool.",
20
- " If line_ranges are present, they are a focus hint — still read the whole file.",
21
- " 2. Review the content under the specified lens.",
22
- " 3. Emit one AuditResult with:",
23
- " task_id, unit_id, pass_id, lens",
24
- " file_coverage: [{path, total_lines}] for every assigned file you reviewed",
25
- " findings: array (empty if nothing found)",
26
- " If the task includes file_line_counts, use those values for file_coverage.total_lines.",
27
- " total_lines must match the file's current total line count.",
28
- " Each finding must include:",
29
- " id, title, category, severity, confidence, lens, summary",
30
- " affected_files: [{path, line_start?, line_end?, symbol?}] — path is repo-relative, NOT a plain string",
31
- " evidence: array of plain strings only, at least one excerpt or line reference from the file you read",
32
- " Example evidence entry: src/foo.ts:42 - variable overwritten before use",
33
- " Example affected_files entry: {\"path\": \"src/foo.ts\", \"line_start\": 42, \"line_end\": 55, \"symbol\": \"myFunction\"}",
34
- " Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
35
- " Low-priority tasks still require a real review. Use findings: [] only when you genuinely found nothing notable.",
36
- task.timeout_ms
37
- ? ` Time budget for this task: ${task.timeout_ms} ms.`
38
- : " Keep the task bounded to the assigned files only.",
39
- `Reference schemas: ${task.artifacts_dir}/dispatch/audit-result.schema.json and ${task.artifacts_dir}/dispatch/finding.schema.json`,
40
- `Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
11
+ `Audit run: ${task.run_id}`,
12
+ `Read: ${tasksPath}`,
13
+ "For each task: read all file_paths in full, review under the specified lens,",
14
+ "and emit one AuditResult with: task_id, unit_id, pass_id, lens, file_coverage,",
15
+ "findings. Each finding: id, title, category, severity, confidence, lens, summary,",
16
+ "affected_files (path, line_start, line_end, symbol), evidence (plain strings).",
17
+ `Write to: ${task.audit_results_path}`,
41
18
  ];
42
19
  if (usesDeferredWorkerCommand(task)) {
43
- lines.push("", "This run is using deferred worker-command ingestion.", "Do not execute worker_command in this session.", "Stop after writing the results file.");
20
+ lines.push("Deferred mode: write results, do not execute worker_command.");
44
21
  }
45
22
  else {
46
- lines.push("", "Then execute the worker_command array from task.json exactly as written.", "Preserve argv boundaries instead of reconstructing shell quoting.", `worker_command argv JSON: ${commandArgv}`, "Stop after the command completes.");
23
+ lines.push("Then execute worker_command from task.json exactly.", `Command: ${commandArgv}`);
47
24
  }
48
25
  return lines.join("\n");
49
26
  }
50
27
  return [
51
- "You are executing one bounded audit step for audit-code.",
52
- `Run ID: ${task.run_id}`,
53
- `Repository root: ${task.repo_root}`,
54
- `Obligation: ${task.obligation_id ?? "unknown"}`,
28
+ `Task: ${task.run_id}`,
55
29
  `Executor: ${task.preferred_executor}`,
56
- "Execute the worker_command array from task.json exactly as written.",
57
- "Preserve argv boundaries instead of reconstructing shell quoting.",
58
- `worker_command argv JSON: ${commandArgv}`,
59
- "Do not continue the audit recursively.",
60
- "Do not choose another task.",
61
- task.timeout_ms
62
- ? `The worker command is budgeted for ${task.timeout_ms} ms.`
63
- : "If the command hangs or fails, stop and let the supervisor handle it.",
64
- `The command must write the worker result JSON to: ${task.result_path}`,
65
- "After the command completes, stop.",
30
+ "Execute worker_command from task.json exactly.",
31
+ `Command: ${commandArgv}`,
32
+ "Write result to: " + task.result_path,
33
+ "Stop after completion.",
66
34
  ].join("\n");
67
35
  }
@@ -21,7 +21,7 @@ function commandExists(command) {
21
21
  return result.status === 0;
22
22
  }
23
23
  export function resolveFreshSessionProviderName(name, sessionConfig = {}, options = {}) {
24
- const requestedProvider = name ?? sessionConfig.provider ?? "local-subprocess";
24
+ const requestedProvider = name ?? sessionConfig.provider ?? "auto";
25
25
  if (requestedProvider !== "auto") {
26
26
  return requestedProvider;
27
27
  }
@@ -29,6 +29,14 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
29
29
  const lookupCommand = options.commandExists ?? commandExists;
30
30
  const inVSCode = (env.TERM_PROGRAM ?? "").toLowerCase() === "vscode";
31
31
  const insideClaudeCode = Boolean(env.CLAUDECODE);
32
+ const insideOpenCode = Boolean(env.OPENCODE);
33
+ // If we're inside a specific IDE/conversation, use that as the provider
34
+ if (insideOpenCode) {
35
+ return "opencode";
36
+ }
37
+ if (insideClaudeCode) {
38
+ return "claude-code";
39
+ }
32
40
  if (inVSCode && hasEntries(sessionConfig.vscode_task?.command_template)) {
33
41
  return "vscode-task";
34
42
  }
@@ -38,6 +38,8 @@ export interface AuditCodeHandoff {
38
38
  interactive_provider_hint: string | null;
39
39
  artifact_paths: AuditCodeHandoffArtifactPaths;
40
40
  active_review_run?: ActiveReviewRun;
41
+ quick_start?: string;
42
+ file_map?: Record<string, string>;
41
43
  }
42
44
  export declare function buildAuditCodeHandoff(params: {
43
45
  root: string;
@@ -115,10 +115,10 @@ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, i
115
115
  return null;
116
116
  }
117
117
  if (isConfigError) {
118
- return `A project configuration issue is blocking the audit. Verify that --root points to the repository root containing a project file (package.json, go.mod, etc.), then run audit-code again.`;
118
+ return `Configuration error: Verify --root points to a repository root (with package.json, go.mod, etc.).`;
119
119
  }
120
120
  const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
121
- return `Current backend worker provider is ${providerLabel}. Remaining semantic review belongs to the active conversation agent by default. If you intentionally want the backend to continue through a compatibility bridge instead, configure ${sessionConfigPath} for ${formatQuotedList(INTERACTIVE_PROVIDER_OPTIONS)} and re-run audit-code with an explicit --provider value from the repository root.`;
121
+ return `Provider: ${providerLabel}. For automatic LLM review, configure an interactive provider in ${sessionConfigPath}.`;
122
122
  }
123
123
  function renderMarkdown(handoff) {
124
124
  const lines = [
@@ -208,7 +208,7 @@ export function buildAuditCodeHandoff(params) {
208
208
  : null,
209
209
  };
210
210
  const suggestedInputs = buildSuggestedInputs(params.artifactsDir, params.state.status, isConfigError, params.activeReviewRun);
211
- return {
211
+ const handoff = {
212
212
  status: params.state.status,
213
213
  repo_root: params.root,
214
214
  artifacts_dir: params.artifactsDir,
@@ -221,6 +221,17 @@ export function buildAuditCodeHandoff(params) {
221
221
  artifact_paths: artifactPaths,
222
222
  active_review_run: params.activeReviewRun,
223
223
  };
224
+ // Add quick_start command and file map when blocked for review
225
+ if (params.state.status === BLOCKED_STATUS && params.activeReviewRun) {
226
+ handoff.quick_start = `audit-code worker-run --task ${params.activeReviewRun.task_path}`;
227
+ handoff.file_map = {
228
+ current_task: artifactPaths.current_task,
229
+ current_prompt: artifactPaths.current_prompt,
230
+ audit_results: params.activeReviewRun.audit_results_path,
231
+ final_report: join(params.root, "audit-report.md"),
232
+ };
233
+ }
234
+ return handoff;
224
235
  }
225
236
  export async function writeAuditCodeHandoffArtifacts(handoff) {
226
237
  try {
@@ -4,7 +4,7 @@ import { formatValidationIssues, } from "../validation/basic.js";
4
4
  import { validateSessionConfig } from "../validation/sessionConfig.js";
5
5
  import { writeJsonFile } from "../io/json.js";
6
6
  const SESSION_CONFIG_FILENAME = "session-config.json";
7
- const DEFAULT_SESSION_CONFIG = { provider: "local-subprocess" };
7
+ const DEFAULT_SESSION_CONFIG = { provider: "auto" };
8
8
  export function getSessionConfigPath(artifactsDir) {
9
9
  return join(artifactsDir, SESSION_CONFIG_FILENAME);
10
10
  }
@@ -156,7 +156,7 @@ export function validateConfiguredProviderEnvironment(sessionConfig, options = {
156
156
  const issues = [];
157
157
  const lookupCommand = options.commandExists ?? commandExists;
158
158
  const lookupPath = options.pathExists ?? configuredPathExists;
159
- const provider = sessionConfig.provider ?? "local-subprocess";
159
+ const provider = sessionConfig.provider ?? "auto";
160
160
  if (provider === "claude-code") {
161
161
  const command = sessionConfig.claude_code?.command ?? "claude";
162
162
  if (isBareExecutableName(command) && !lookupCommand(command)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.2.17",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -236,6 +236,15 @@
236
236
  "type": ["string", "null"]
237
237
  }
238
238
  }
239
+ },
240
+ "quick_start": {
241
+ "type": "string"
242
+ },
243
+ "file_map": {
244
+ "type": "object",
245
+ "additionalProperties": {
246
+ "type": "string"
247
+ }
239
248
  }
240
249
  }
241
250
  }
@@ -21,10 +21,10 @@ Repeat Steps 1–5 until the audit status is `"complete"`.
21
21
  Run:
22
22
 
23
23
  ```bash
24
- node audit-code.mjs
24
+ audit-code
25
25
  ```
26
26
 
27
- _(Outside the `auditor-lambda` repo itself, use `audit-code` or `npx audit-code` instead.)_
27
+ _(Inside the `auditor-lambda` repo itself, use `node audit-code.mjs` instead.)_
28
28
 
29
29
  Parse the JSON output. Check `audit_state.status`:
30
30
 
@@ -38,13 +38,13 @@ Parse the JSON output. Check `audit_state.status`:
38
38
 
39
39
  ### Step 2 — Read the Task
40
40
 
41
- Read `.audit-artifacts/dispatch/current-task.json`.
41
+ Read the file at `.audit-artifacts/dispatch/current-task.json`.
42
42
 
43
43
  Note these fields:
44
44
  - `run_id` — identifies this batch of audit work
45
45
  - `artifacts_dir` — base artifacts directory
46
- - `pending_audit_tasks_path` — path to the pending task list
47
- - `worker_command` JSON array; run this after the audit work is complete
46
+
47
+ _(If `audit_state.blockers` contains a message that requires operator input rather than code review, stop and report the blocker verbatim to the user.)_
48
48
 
49
49
  ---
50
50
 
@@ -53,16 +53,14 @@ Note these fields:
53
53
  Run:
54
54
 
55
55
  ```bash
56
- node dispatch/prepare-dispatch.mjs --run-id <run_id>
56
+ audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
57
57
  ```
58
58
 
59
- This reads every pending audit task, pre-computes a complete subagent prompt for each, and writes `dispatch-plan.json` to the same directory as `pending_audit_tasks_path`. It prints the task count and warns about any tasks exceeding 1500 lines.
60
-
61
- Read `dispatch-plan.json`. It is a JSON array where each entry has:
59
+ Read `<artifacts_dir>/runs/<run_id>/dispatch-plan.json`. It is a JSON array where each entry has:
62
60
  - `task_id` — task identifier
63
61
  - `description` — short label for the Agent call
64
62
  - `output_path` — where the subagent writes its result
65
- - `prompt` — the complete, ready-to-use subagent prompt (do not modify it)
63
+ - `prompt_path` — path to the complete subagent instructions file
66
64
 
67
65
  ---
68
66
 
@@ -71,25 +69,23 @@ Read `dispatch-plan.json`. It is a JSON array where each entry has:
71
69
  **In a single message**, fire one `Agent` call per entry in `dispatch-plan.json`:
72
70
 
73
71
  ```
74
- Agent({ description: entry.description, prompt: entry.prompt })
72
+ Agent({ description: entry.description, prompt: "Read and follow the audit instructions in: " + entry.prompt_path })
75
73
  ```
76
74
 
77
75
  All calls must be sent simultaneously — never await one before firing the next. This is the critical performance constraint. Wait for all to complete before proceeding.
78
76
 
79
- Each subagent reads its assigned files, writes a validated JSON result to `output_path`, and self-validates via `node dispatch/validate-result.mjs`. You do not need to check individual subagent output.
77
+ Each subagent reads its instruction file, reviews the assigned code, writes a validated JSON result to `output_path`, and self-validates. You do not need to inspect individual subagent output.
80
78
 
81
79
  ---
82
80
 
83
81
  ### Step 5 — Merge and Ingest
84
82
 
85
- Run in sequence:
83
+ Run:
86
84
 
87
85
  ```bash
88
- node dispatch/merge-results.mjs --run-id <run_id>
86
+ audit-code merge-and-ingest --run-id <run_id> --artifacts-dir <artifacts_dir>
89
87
  ```
90
88
 
91
- Then execute the `worker_command` from `current-task.json`. It is a JSON array — join the elements into a shell command and run it.
92
-
93
89
  Loop back to **Step 1**.
94
90
 
95
91
  ---
@@ -98,14 +94,12 @@ Loop back to **Step 1**.
98
94
 
99
95
  When `audit_state.status` is `"complete"`, stop the loop. Do **not** run the orchestrator again.
100
96
 
101
- Read `audit-report.md` and present the completed audit to the user. Lead with the work blocks — they are the primary remediation handoff. Wait for the user to ask you to begin resolving one or more work blocks.
97
+ Read `audit-report.md` and present the completed audit to the user. Lead with the work blocks — they are the primary remediation handoff.
102
98
 
103
99
  ---
104
100
 
105
101
  ## Edge Cases
106
102
 
107
- **Non-agent blocker:** If `audit_state.blockers` contains a message that requires operator input (not code review), stop and report the blocker verbatim to the user.
108
-
109
- **Large task warnings:** `prepare-dispatch.mjs` warns about tasks exceeding ~1500 lines. If a subagent hits a quota limit and fails to produce output, `merge-results.mjs` excludes it silently — those tasks remain pending and are picked up in the next loop iteration. No manual intervention needed.
103
+ **Large task warnings:** `prepare-dispatch` warns about tasks exceeding ~1500 lines. If a subagent hits a quota limit and fails to produce output, `merge-and-ingest` excludes it silently — those tasks remain pending and are picked up in the next loop iteration. No manual intervention needed.
110
104
 
111
- **Failed validation:** Subagents self-validate and retry up to 3 times before writing a fallback empty result. `merge-results.mjs` writes `failed-tasks.json` listing any tasks that still failed. Those tasks are requeued automatically in the next cycle.
105
+ **Failed validation:** Subagents self-validate and retry up to 3 times before finishing. `merge-and-ingest` excludes any results that still lack required fields and writes `failed-tasks.json`. Those tasks are requeued automatically in the next cycle.