auditor-lambda 0.1.0 → 0.2.1

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 (58) hide show
  1. package/README.md +2 -1
  2. package/audit-code-wrapper-lib.mjs +458 -380
  3. package/dist/cli.js +258 -11
  4. package/dist/coverage.d.ts +0 -1
  5. package/dist/coverage.js +3 -34
  6. package/dist/extractors/fileInventory.js +2 -0
  7. package/dist/io/artifacts.js +2 -1
  8. package/dist/orchestrator/advance.js +70 -52
  9. package/dist/orchestrator/flowCoverage.js +2 -1
  10. package/dist/orchestrator/flowPlanning.d.ts +1 -1
  11. package/dist/orchestrator/flowPlanning.js +21 -28
  12. package/dist/orchestrator/internalExecutors.js +0 -1
  13. package/dist/orchestrator/taskBuilder.d.ts +7 -2
  14. package/dist/orchestrator/taskBuilder.js +55 -47
  15. package/dist/prompts/renderWorkerPrompt.js +32 -0
  16. package/dist/providers/claudeCodeProvider.js +6 -0
  17. package/dist/providers/index.js +5 -2
  18. package/dist/providers/opencodeProvider.js +6 -1
  19. package/dist/providers/types.d.ts +1 -0
  20. package/dist/reporting/mergeFindings.js +0 -7
  21. package/dist/reporting/rootCause.d.ts +0 -1
  22. package/dist/reporting/rootCause.js +0 -6
  23. package/dist/reporting/synthesis.js +18 -0
  24. package/dist/supervisor/runLedger.js +6 -2
  25. package/dist/types/sessionConfig.d.ts +8 -0
  26. package/dist/types/workerSession.d.ts +2 -0
  27. package/dist/types.d.ts +1 -2
  28. package/dist/validation/auditResults.d.ts +11 -0
  29. package/dist/validation/auditResults.js +118 -0
  30. package/dist/validation/sessionConfig.js +15 -1
  31. package/docs/agent-integrations.md +61 -56
  32. package/docs/agent-roles.md +69 -69
  33. package/docs/architecture.md +90 -90
  34. package/docs/artifacts.md +69 -69
  35. package/docs/bootstrap-install.md +1 -1
  36. package/docs/model-selection.md +86 -86
  37. package/docs/next-steps.md +11 -9
  38. package/docs/packaging.md +3 -3
  39. package/docs/pipeline.md +152 -152
  40. package/docs/production-readiness.md +6 -5
  41. package/docs/repo-layout.md +18 -18
  42. package/docs/run-flow.md +5 -5
  43. package/docs/session-config.md +216 -210
  44. package/docs/supervisor.md +70 -70
  45. package/docs/windows-setup.md +139 -139
  46. package/package.json +56 -56
  47. package/schemas/audit-code-v1alpha1.schema.json +76 -76
  48. package/schemas/audit_result.schema.json +48 -48
  49. package/schemas/audit_task.schema.json +49 -49
  50. package/schemas/coverage_matrix.schema.json +0 -15
  51. package/schemas/file_disposition.schema.json +33 -33
  52. package/schemas/finding.schema.json +58 -62
  53. package/schemas/flow_coverage.schema.json +44 -44
  54. package/schemas/root_cause_clusters.schema.json +0 -4
  55. package/schemas/runtime_validation_report.schema.json +34 -34
  56. package/schemas/synthesis_report.schema.json +61 -61
  57. package/skills/audit-code/SKILL.md +37 -37
  58. package/skills/audit-code/audit-code.prompt.md +56 -54
@@ -3,39 +3,32 @@ const DEFAULT_FLOW_LENS_PRIORITY = [
3
3
  "reliability",
4
4
  "correctness",
5
5
  ];
6
- function normalizeTaskSignature(task) {
7
- const path = task.file_paths.join(",");
8
- const range = task.line_ranges?.map((r) => `${r.path}:${r.start}-${r.end}`).join(",") ??
9
- "full";
10
- return `${task.lens}|${path}|${range}`;
11
- }
12
- export function buildFlowAwareTaskAugmentations(existingTasks, criticalFlows, lineIndex) {
6
+ export function buildFlowAwareTaskAugmentations(existingTasks, criticalFlows, _lineIndex) {
13
7
  const seenTaskIds = new Set(existingTasks.map((task) => task.task_id));
14
- const existingSignatures = new Set(existingTasks.map(normalizeTaskSignature));
8
+ // Signature: lens + sorted file list, so duplicate (flow, lens) combos across
9
+ // different flow groupings are still deduplicated.
10
+ const existingSignatures = new Set(existingTasks.map((t) => `${t.lens}|${[...t.file_paths].sort().join(",")}`));
15
11
  const extraTasks = [];
16
12
  for (const flow of criticalFlows.flows) {
17
13
  const desiredLenses = flow.concerns.filter((concern) => DEFAULT_FLOW_LENS_PRIORITY.includes(concern));
18
- for (const path of flow.paths) {
19
- const totalLines = lineIndex[path] ?? 0;
20
- for (const lens of desiredLenses) {
21
- const candidate = {
22
- task_id: `flow:${flow.id}:${lens}:${path}`,
23
- unit_id: `flow:${flow.id}`,
24
- pass_id: `flow-pass:${lens}`,
25
- lens,
26
- file_paths: [path],
27
- line_ranges: totalLines > 0 ? [{ path, start: 1, end: totalLines }] : undefined,
28
- rationale: `Flow-aware audit for ${path} because it participates in critical flow ${flow.id} under the ${lens} lens.`,
29
- };
30
- const signature = normalizeTaskSignature(candidate);
31
- if (seenTaskIds.has(candidate.task_id) ||
32
- existingSignatures.has(signature)) {
33
- continue;
34
- }
35
- extraTasks.push(candidate);
36
- seenTaskIds.add(candidate.task_id);
37
- existingSignatures.add(signature);
14
+ for (const lens of desiredLenses) {
15
+ // One task per (flow, lens) with all flow paths together so the agent
16
+ // can trace data across file boundaries in a single pass.
17
+ const taskId = `flow:${flow.id}:${lens}`;
18
+ const signature = `${lens}|${[...flow.paths].sort().join(",")}`;
19
+ if (seenTaskIds.has(taskId) || existingSignatures.has(signature)) {
20
+ continue;
38
21
  }
22
+ extraTasks.push({
23
+ task_id: taskId,
24
+ unit_id: `flow:${flow.id}`,
25
+ pass_id: `flow-pass:${lens}`,
26
+ lens,
27
+ file_paths: [...flow.paths],
28
+ rationale: `Flow-aware audit for critical flow "${flow.id}" (${flow.paths.length} file${flow.paths.length === 1 ? "" : "s"}) under the ${lens} lens.`,
29
+ });
30
+ seenTaskIds.add(taskId);
31
+ existingSignatures.add(signature);
39
32
  }
40
33
  }
41
34
  return extraTasks;
@@ -87,7 +87,6 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
87
87
  const runtimeValidationTasks = buildRuntimeValidationTasks(bundle.unit_manifest, bundle.critical_flows, flowCoverage);
88
88
  const runtimeValidationReport = preserveOrPlaceholder(runtimeValidationTasks, bundle.runtime_validation_report);
89
89
  const baseTasks = buildChunkedAuditTasks(bundle.unit_manifest, lineIndex, {
90
- chunk_size: 200,
91
90
  external_analyzer_results: externalAnalyzerResults,
92
91
  });
93
92
  const analyzerTasks = buildExternalSignalTasks(coverage, lineIndex, externalAnalyzerResults);
@@ -4,9 +4,14 @@ export interface UnitLineIndex {
4
4
  [path: string]: number;
5
5
  }
6
6
  export interface BuildChunkedTaskOptions {
7
- chunk_size?: number;
7
+ /**
8
+ * Line count above which a single file gets its own task rather than being
9
+ * grouped with the rest of its unit. Default: 3000. Set to 0 to disable
10
+ * splitting entirely.
11
+ */
12
+ file_split_threshold?: number;
8
13
  limit_lenses?: Lens[];
9
14
  external_analyzer_results?: ExternalAnalyzerResults;
10
15
  }
11
16
  export declare function buildChunkedAuditTasks(unitManifest: UnitManifest, unitLineIndex: UnitLineIndex, options?: BuildChunkedTaskOptions): AuditTask[];
12
- export declare function buildExternalSignalTasks(coverageMatrix: CoverageMatrix, unitLineIndex: UnitLineIndex, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
17
+ export declare function buildExternalSignalTasks(coverageMatrix: CoverageMatrix, _unitLineIndex: UnitLineIndex, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
@@ -1,12 +1,17 @@
1
- function chunkRanges(totalLines, chunkSize) {
2
- const ranges = [];
3
- let start = 1;
4
- while (start <= totalLines) {
5
- const end = Math.min(start + chunkSize - 1, totalLines);
6
- ranges.push({ start, end });
7
- start = end + 1;
1
+ function modelHintForLens(lens) {
2
+ switch (lens) {
3
+ case "security":
4
+ case "correctness":
5
+ case "reliability":
6
+ case "data_integrity":
7
+ return "capable";
8
+ case "architecture":
9
+ case "maintainability":
10
+ case "tests":
11
+ return "balanced";
12
+ default:
13
+ return "fast";
8
14
  }
9
- return ranges;
10
15
  }
11
16
  function taskPriority(hasExternalSignal, lens) {
12
17
  if (hasExternalSignal &&
@@ -48,7 +53,7 @@ function pickAnalyzerLens(category) {
48
53
  return "correctness";
49
54
  }
50
55
  export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}) {
51
- const chunkSize = options.chunk_size ?? 200;
56
+ const fileSplitThreshold = options.file_split_threshold ?? 3000;
52
57
  const allowed = new Set(options.limit_lenses ?? []);
53
58
  const enforceLensFilter = allowed.size > 0;
54
59
  const tasks = [];
@@ -63,49 +68,54 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
63
68
  if (enforceLensFilter && !allowed.has(lens)) {
64
69
  continue;
65
70
  }
66
- for (const filePath of unit.files) {
67
- const hasExternalSignal = externalPaths.has(filePath);
68
- const priority = taskPriority(hasExternalSignal, lens);
69
- const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
70
- const lineCount = unitLineIndex[filePath] ?? 0;
71
- const ranges = chunkRanges(lineCount, chunkSize);
72
- if (ranges.length === 0) {
73
- const id = `${unit.unit_id}:${lens}:${filePath}:full`;
74
- if (seen.has(id))
75
- continue;
76
- seen.add(id);
77
- tasks.push({
78
- task_id: id,
79
- unit_id: unit.unit_id,
80
- pass_id: `pass:${lens}`,
81
- lens,
82
- file_paths: [filePath],
83
- rationale: `Audit ${filePath} under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority for this path." : ""}`,
84
- priority,
85
- tags,
86
- });
87
- continue;
88
- }
89
- for (const range of ranges) {
90
- const id = `${unit.unit_id}:${lens}:${filePath}:${range.start}-${range.end}`;
91
- if (seen.has(id))
92
- continue;
71
+ const hasExternalSignal = unit.files.some((f) => externalPaths.has(f));
72
+ const priority = taskPriority(hasExternalSignal, lens);
73
+ const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
74
+ // Split files that are individually too large to group; everything else
75
+ // goes into one task so the agent can reason across file boundaries.
76
+ const oversizedFiles = fileSplitThreshold > 0
77
+ ? unit.files.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
78
+ : [];
79
+ const normalFiles = unit.files.filter((f) => !oversizedFiles.includes(f));
80
+ // One task for all normal-sized files in this unit under this lens.
81
+ if (normalFiles.length > 0) {
82
+ const id = `${unit.unit_id}:${lens}`;
83
+ if (!seen.has(id)) {
93
84
  seen.add(id);
94
85
  tasks.push({
95
86
  task_id: id,
96
87
  unit_id: unit.unit_id,
97
88
  pass_id: `pass:${lens}`,
98
89
  lens,
99
- file_paths: [filePath],
100
- line_ranges: [
101
- { path: filePath, start: range.start, end: range.end },
102
- ],
103
- rationale: `Audit ${filePath} lines ${range.start}-${range.end} under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority for this path." : ""}`,
90
+ file_paths: normalFiles,
91
+ rationale: `Audit ${unit.unit_id} (${normalFiles.length} file${normalFiles.length === 1 ? "" : "s"}) under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
104
92
  priority,
105
93
  tags,
94
+ model_hint: modelHintForLens(lens),
106
95
  });
107
96
  }
108
97
  }
98
+ // Oversized files each get their own task so the agent isn't overwhelmed.
99
+ for (const filePath of oversizedFiles) {
100
+ const id = `${unit.unit_id}:${lens}:${filePath}`;
101
+ if (seen.has(id))
102
+ continue;
103
+ seen.add(id);
104
+ const fileHasSignal = externalPaths.has(filePath);
105
+ tasks.push({
106
+ task_id: id,
107
+ unit_id: unit.unit_id,
108
+ pass_id: `pass:${lens}`,
109
+ lens,
110
+ file_paths: [filePath],
111
+ rationale: `Audit ${filePath} (large file, split from unit) under the ${lens} lens.${fileHasSignal ? " External analyzer signals raise priority." : ""}`,
112
+ priority: taskPriority(fileHasSignal, lens),
113
+ tags: fileHasSignal
114
+ ? ["external_analyzer_signal", "large_file"]
115
+ : ["large_file"],
116
+ model_hint: modelHintForLens(lens),
117
+ });
118
+ }
109
119
  }
110
120
  }
111
121
  return tasks.sort((a, b) => {
@@ -115,19 +125,19 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
115
125
  return a.task_id.localeCompare(b.task_id);
116
126
  });
117
127
  }
118
- export function buildExternalSignalTasks(coverageMatrix, unitLineIndex, externalAnalyzerResults) {
128
+ export function buildExternalSignalTasks(coverageMatrix, _unitLineIndex, externalAnalyzerResults) {
119
129
  if (!externalAnalyzerResults) {
120
130
  return [];
121
131
  }
122
132
  const tasks = [];
123
133
  const seen = new Set();
134
+ const coverageIndex = new Map(coverageMatrix.files.map((file) => [file.path, file]));
124
135
  for (const result of externalAnalyzerResults.results) {
125
136
  const lens = pickAnalyzerLens(result.category);
126
- const coverage = coverageMatrix.files.find((file) => file.path === result.path);
137
+ const coverage = coverageIndex.get(result.path);
127
138
  if (!coverage || coverage.audit_status === "excluded") {
128
139
  continue;
129
140
  }
130
- const lineCount = unitLineIndex[result.path] ?? 0;
131
141
  const id = `analyzer:${externalAnalyzerResults.tool}:${lens}:${result.path}:${result.id}`;
132
142
  if (seen.has(id)) {
133
143
  continue;
@@ -139,15 +149,13 @@ export function buildExternalSignalTasks(coverageMatrix, unitLineIndex, external
139
149
  pass_id: `analyzer:${externalAnalyzerResults.tool}:${lens}`,
140
150
  lens,
141
151
  file_paths: [result.path],
142
- line_ranges: lineCount > 0
143
- ? [{ path: result.path, start: 1, end: lineCount }]
144
- : undefined,
145
152
  rationale: `Analyzer follow-up for ${result.path} under the ${lens} lens because ${externalAnalyzerResults.tool} reported: ${result.summary}`,
146
153
  priority: "high",
147
154
  tags: [
148
155
  "external_analyzer_signal",
149
156
  `external_tool:${externalAnalyzerResults.tool}`,
150
157
  ],
158
+ model_hint: modelHintForLens(lens),
151
159
  });
152
160
  }
153
161
  return tasks.sort((a, b) => a.task_id.localeCompare(b.task_id));
@@ -3,6 +3,38 @@ function shellQuote(arg) {
3
3
  }
4
4
  export function renderWorkerPrompt(task) {
5
5
  const command = task.worker_command.map(shellQuote).join(" ");
6
+ if (task.preferred_executor === "agent" && task.audit_results_path) {
7
+ const tasksPath = task.pending_audit_tasks_path ?? `${task.artifacts_dir}/audit_tasks.json`;
8
+ const lines = [
9
+ "You are executing one bounded audit task for audit-code.",
10
+ `Run ID: ${task.run_id}`,
11
+ `Repository root: ${task.repo_root}`,
12
+ "",
13
+ `Read the task file: ${tasksPath}`,
14
+ "It contains the task(s) assigned to this run.",
15
+ "",
16
+ "For each task:",
17
+ " 1. Read every file listed in file_paths in full using your file-reading tool.",
18
+ " If line_ranges are present, they are a focus hint — still read the whole file.",
19
+ " 2. Review the content under the specified lens.",
20
+ " 3. Emit one AuditResult with:",
21
+ " task_id, unit_id, pass_id, lens",
22
+ " reviewed_ranges: [{path, start, end}] covering what you read",
23
+ " findings: array (empty if nothing found)",
24
+ " Each finding must include:",
25
+ " id, title, category, severity, confidence, lens, summary, affected_files,",
26
+ " evidence (at least one excerpt or line reference from the file you read)",
27
+ " Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
28
+ `Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
29
+ ];
30
+ if (task.skip_worker_command) {
31
+ lines.push("", "Stop after writing the results file.");
32
+ }
33
+ else {
34
+ lines.push("", "Then run this command exactly:", command, "Stop after the command completes.");
35
+ }
36
+ return lines.join("\n");
37
+ }
6
38
  return [
7
39
  "You are executing one bounded audit step for audit-code.",
8
40
  `Run ID: ${task.run_id}`,
@@ -7,11 +7,17 @@ export class ClaudeCodeProvider {
7
7
  this.config = config;
8
8
  }
9
9
  async launch(input) {
10
+ if (process.env.CLAUDECODE) {
11
+ throw new Error("claude-code provider cannot be used inside an active Claude Code session. " +
12
+ 'Set provider to "local-subprocess" in .audit-artifacts/session-config.json, ' +
13
+ "then run /audit-code conversationally and follow the dispatch prompts manually.");
14
+ }
10
15
  const prompt = await readFile(input.promptPath, "utf8");
11
16
  const command = this.config.command ?? "claude";
12
17
  const args = [
13
18
  "-p",
14
19
  prompt,
20
+ ...(input.model ? ["--model", input.model] : []),
15
21
  ...(this.config.extra_args ?? []),
16
22
  "--dangerously-skip-permissions",
17
23
  ];
@@ -28,6 +28,7 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
28
28
  const env = options.env ?? process.env;
29
29
  const lookupCommand = options.commandExists ?? commandExists;
30
30
  const inVSCode = (env.TERM_PROGRAM ?? "").toLowerCase() === "vscode";
31
+ const insideClaudeCode = Boolean(env.CLAUDECODE);
31
32
  if (inVSCode && hasEntries(sessionConfig.vscode_task?.command_template)) {
32
33
  return "vscode-task";
33
34
  }
@@ -36,9 +37,11 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
36
37
  }
37
38
  const claudeCommand = sessionConfig.claude_code?.command ?? "claude";
38
39
  const opencodeCommand = sessionConfig.opencode?.command ?? "opencode";
39
- const claudeAvailable = lookupCommand(claudeCommand);
40
+ const claudeAvailable = !insideClaudeCode && lookupCommand(claudeCommand);
40
41
  const opencodeAvailable = lookupCommand(opencodeCommand);
41
- if (hasConfiguredClaudeCode(sessionConfig) && claudeAvailable) {
42
+ if (!insideClaudeCode &&
43
+ hasConfiguredClaudeCode(sessionConfig) &&
44
+ claudeAvailable) {
42
45
  return "claude-code";
43
46
  }
44
47
  if (hasConfiguredOpenCode(sessionConfig) && opencodeAvailable) {
@@ -9,7 +9,12 @@ export class OpenCodeProvider {
9
9
  async launch(input) {
10
10
  const prompt = await readFile(input.promptPath, "utf8");
11
11
  const command = this.config.command ?? "opencode";
12
- const args = ["run", prompt, ...(this.config.extra_args ?? [])];
12
+ const args = [
13
+ "run",
14
+ prompt,
15
+ ...(input.model ? ["--model", input.model] : []),
16
+ ...(this.config.extra_args ?? []),
17
+ ];
13
18
  return await spawnLoggedCommand(command, args, input);
14
19
  }
15
20
  }
@@ -9,6 +9,7 @@ export interface LaunchFreshSessionInput {
9
9
  stderrPath: string;
10
10
  uiMode: "visible" | "headless";
11
11
  timeoutMs: number;
12
+ model?: string;
12
13
  }
13
14
  export interface LaunchFreshSessionResult {
14
15
  accepted: boolean;
@@ -80,7 +80,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
80
80
  ...analyzerEvidence,
81
81
  ]),
82
82
  ],
83
- remediation: [...new Set(finding.remediation ?? [])],
84
83
  related_findings: [
85
84
  ...new Set([...(finding.related_findings ?? []), finding.id]),
86
85
  ],
@@ -109,12 +108,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
109
108
  ...analyzerEvidence,
110
109
  ]),
111
110
  ];
112
- existing.remediation = [
113
- ...new Set([
114
- ...(existing.remediation ?? []),
115
- ...(finding.remediation ?? []),
116
- ]),
117
- ];
118
111
  existing.related_findings = [
119
112
  ...new Set([
120
113
  ...(existing.related_findings ?? []),
@@ -6,6 +6,5 @@ export interface RootCauseCluster {
6
6
  title: string;
7
7
  summary: string;
8
8
  finding_ids: string[];
9
- recommended_actions: string[];
10
9
  }
11
10
  export declare function buildRootCauseClusters(findings: Finding[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults): RootCauseCluster[];
@@ -52,17 +52,11 @@ export function buildRootCauseClusters(findings, runtimeReport, externalAnalyzer
52
52
  .map(([key, grouped], index) => {
53
53
  const highestSeverity = grouped.reduce((max, finding) => Math.max(max, severityRank(finding.severity)), 1);
54
54
  const systemicCount = grouped.filter((finding) => finding.systemic).length;
55
- const uniqueRemediations = [
56
- ...new Set(grouped.flatMap((finding) => finding.remediation ?? [])),
57
- ].slice(0, 5);
58
55
  return {
59
56
  id: `cluster-${index + 1}`,
60
57
  title: titleForCluster(key),
61
58
  summary: `Grouped ${grouped.length} finding(s); highest severity ${highestSeverity}; systemic flags ${systemicCount}. Runtime validation status: ${runtimeSummary}. External analyzer summary: ${externalSummary}.`,
62
59
  finding_ids: grouped.map((finding) => finding.id),
63
- recommended_actions: uniqueRemediations.length > 0
64
- ? uniqueRemediations
65
- : [`Review systemic causes behind ${titleForCluster(key)}.`],
66
60
  };
67
61
  })
68
62
  .sort((a, b) => a.title.localeCompare(b.title));
@@ -14,6 +14,23 @@ function severityBreakdown(findings) {
14
14
  }
15
15
  return breakdown;
16
16
  }
17
+ function zeroFindingLensNotes(results) {
18
+ const tasksByLens = new Map();
19
+ const findingsByLens = new Map();
20
+ for (const result of results) {
21
+ tasksByLens.set(result.lens, (tasksByLens.get(result.lens) ?? 0) + 1);
22
+ findingsByLens.set(result.lens, (findingsByLens.get(result.lens) ?? 0) + result.findings.length);
23
+ }
24
+ const zeroLenses = [...tasksByLens.entries()]
25
+ .filter(([lens, count]) => count > 0 && (findingsByLens.get(lens) ?? 0) === 0)
26
+ .map(([lens]) => lens)
27
+ .sort();
28
+ if (zeroLenses.length === 0)
29
+ return [];
30
+ return [
31
+ `Zero findings across all reviewed tasks for lens(es): ${zeroLenses.join(", ")}. Verify these tasks were genuinely reviewed rather than batch-generated.`,
32
+ ];
33
+ }
17
34
  function externalSummary(results) {
18
35
  if (!results) {
19
36
  return { tool_count: 0, result_count: 0 };
@@ -47,6 +64,7 @@ export function buildSynthesisReport(results, runtimeReport, externalAnalyzerRes
47
64
  `External analyzer signals incorporated: ${extSummary.result_count} result(s) from ${externalAnalyzerResults?.tool}.`,
48
65
  ]
49
66
  : []),
67
+ ...zeroFindingLensNotes(results),
50
68
  ],
51
69
  },
52
70
  merged_findings,
@@ -1,4 +1,5 @@
1
- import { readJsonFile, writeJsonFile } from "../io/json.js";
1
+ import { rename, writeFile } from "node:fs/promises";
2
+ import { readJsonFile } from "../io/json.js";
2
3
  function ledgerPath(artifactsDir) {
3
4
  return `${artifactsDir}/run-ledger.json`;
4
5
  }
@@ -13,5 +14,8 @@ export async function loadRunLedger(artifactsDir) {
13
14
  export async function appendRunLedgerEntry(artifactsDir, entry) {
14
15
  const ledger = await loadRunLedger(artifactsDir);
15
16
  ledger.runs.push(entry);
16
- await writeJsonFile(ledgerPath(artifactsDir), ledger);
17
+ const target = ledgerPath(artifactsDir);
18
+ const tmp = `${target}.tmp`;
19
+ await writeFile(tmp, JSON.stringify(ledger, null, 2) + "\n", "utf8");
20
+ await rename(tmp, target);
17
21
  }
@@ -16,6 +16,11 @@ export interface VSCodeTaskConfig {
16
16
  command_template: string[];
17
17
  env?: Record<string, string>;
18
18
  }
19
+ export interface ModelTiersConfig {
20
+ fast?: string;
21
+ balanced?: string;
22
+ capable?: string;
23
+ }
19
24
  export interface SessionConfig {
20
25
  provider?: ProviderName;
21
26
  timeout_ms?: number;
@@ -24,4 +29,7 @@ export interface SessionConfig {
24
29
  claude_code?: ClaudeCodeConfig;
25
30
  opencode?: OpenCodeConfig;
26
31
  vscode_task?: VSCodeTaskConfig;
32
+ agent_task_batch_size?: number;
33
+ parallel_workers?: number;
34
+ model_tiers?: ModelTiersConfig;
27
35
  }
@@ -8,6 +8,8 @@ export interface WorkerTask {
8
8
  result_path: string;
9
9
  worker_command: string[];
10
10
  audit_results_path?: string;
11
+ pending_audit_tasks_path?: string;
11
12
  runtime_updates_path?: string;
12
13
  external_analyzer_results_path?: string;
14
+ skip_worker_command?: boolean;
13
15
  }
package/dist/types.d.ts CHANGED
@@ -43,7 +43,6 @@ export interface CoverageFileRecord {
43
43
  audit_status: string;
44
44
  required_lenses: Lens[];
45
45
  completed_lenses: Lens[];
46
- reviewed_line_ranges: ReviewedLineRange[];
47
46
  }
48
47
  export interface CoverageMatrix {
49
48
  files: CoverageFileRecord[];
@@ -63,6 +62,7 @@ export interface AuditTask {
63
62
  rationale: string;
64
63
  priority?: "high" | "medium" | "low";
65
64
  tags?: string[];
65
+ model_hint?: "fast" | "balanced" | "capable";
66
66
  }
67
67
  export interface Finding {
68
68
  id: string;
@@ -82,7 +82,6 @@ export interface Finding {
82
82
  likelihood?: string;
83
83
  evidence?: string[];
84
84
  reproduction?: string[];
85
- remediation?: string[];
86
85
  systemic?: boolean;
87
86
  related_findings?: string[];
88
87
  }
@@ -0,0 +1,11 @@
1
+ import type { AuditResult, AuditTask } from "../types.js";
2
+ export type IssueSeverity = "error" | "warning";
3
+ export interface AuditResultIssue {
4
+ result_index: number;
5
+ task_id: string;
6
+ severity: IssueSeverity;
7
+ field: string;
8
+ message: string;
9
+ }
10
+ export declare function validateAuditResults(results: AuditResult[], tasks: AuditTask[]): AuditResultIssue[];
11
+ export declare function formatAuditResultIssues(issues: AuditResultIssue[]): string;
@@ -0,0 +1,118 @@
1
+ const REQUIRED_FINDING_FIELDS = [
2
+ "id",
3
+ "title",
4
+ "category",
5
+ "severity",
6
+ "confidence",
7
+ "lens",
8
+ "summary",
9
+ ];
10
+ const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low", "info"]);
11
+ const VALID_CONFIDENCES = new Set(["high", "medium", "low"]);
12
+ function validateFinding(finding, label, taskId, resultIndex) {
13
+ const issues = [];
14
+ for (const field of REQUIRED_FINDING_FIELDS) {
15
+ const value = finding[field];
16
+ if (value === undefined || value === null || String(value).trim() === "") {
17
+ issues.push({
18
+ result_index: resultIndex,
19
+ task_id: taskId,
20
+ severity: "error",
21
+ field: `${label}.${field}`,
22
+ message: `Required field '${field}' is missing or empty.`,
23
+ });
24
+ }
25
+ }
26
+ if (finding.severity && !VALID_SEVERITIES.has(finding.severity)) {
27
+ issues.push({
28
+ result_index: resultIndex,
29
+ task_id: taskId,
30
+ severity: "error",
31
+ field: `${label}.severity`,
32
+ message: `Invalid severity '${finding.severity}'. Must be one of: ${[...VALID_SEVERITIES].join(", ")}.`,
33
+ });
34
+ }
35
+ if (finding.confidence && !VALID_CONFIDENCES.has(finding.confidence)) {
36
+ issues.push({
37
+ result_index: resultIndex,
38
+ task_id: taskId,
39
+ severity: "error",
40
+ field: `${label}.confidence`,
41
+ message: `Invalid confidence '${finding.confidence}'. Must be one of: ${[...VALID_CONFIDENCES].join(", ")}.`,
42
+ });
43
+ }
44
+ if (!finding.affected_files || finding.affected_files.length === 0) {
45
+ issues.push({
46
+ result_index: resultIndex,
47
+ task_id: taskId,
48
+ severity: "error",
49
+ field: `${label}.affected_files`,
50
+ message: "affected_files is empty — at least one file location is required.",
51
+ });
52
+ }
53
+ else {
54
+ for (let k = 0; k < finding.affected_files.length; k++) {
55
+ const af = finding.affected_files[k];
56
+ if (!af.path?.trim()) {
57
+ issues.push({
58
+ result_index: resultIndex,
59
+ task_id: taskId,
60
+ severity: "error",
61
+ field: `${label}.affected_files[${k}].path`,
62
+ message: "affected_files entry has an empty path.",
63
+ });
64
+ }
65
+ }
66
+ }
67
+ if (!finding.evidence || finding.evidence.length === 0) {
68
+ issues.push({
69
+ result_index: resultIndex,
70
+ task_id: taskId,
71
+ severity: "error",
72
+ field: `${label}.evidence`,
73
+ message: "evidence is empty — at least one quoted or referenced excerpt from the reviewed file is required for every finding.",
74
+ });
75
+ }
76
+ else {
77
+ const hasSubstantiveEntry = finding.evidence.some((e) => e.trim().length > 0);
78
+ if (!hasSubstantiveEntry) {
79
+ issues.push({
80
+ result_index: resultIndex,
81
+ task_id: taskId,
82
+ severity: "error",
83
+ field: `${label}.evidence`,
84
+ message: "All evidence entries are empty strings.",
85
+ });
86
+ }
87
+ }
88
+ return issues;
89
+ }
90
+ export function validateAuditResults(results, tasks) {
91
+ const issues = [];
92
+ const taskMap = new Map(tasks.map((t) => [t.task_id, t]));
93
+ for (let i = 0; i < results.length; i++) {
94
+ const result = results[i];
95
+ const taskId = result.task_id ?? `result[${i}]`;
96
+ if (!result.reviewed_ranges || result.reviewed_ranges.length === 0) {
97
+ issues.push({
98
+ result_index: i,
99
+ task_id: taskId,
100
+ severity: "error",
101
+ field: "reviewed_ranges",
102
+ message: "reviewed_ranges is empty — no proof of file reading was recorded. " +
103
+ "Each result must include the line ranges actually read.",
104
+ });
105
+ }
106
+ for (let j = 0; j < (result.findings ?? []).length; j++) {
107
+ const finding = result.findings[j];
108
+ const label = `findings[${j}]`;
109
+ issues.push(...validateFinding(finding, label, taskId, i));
110
+ }
111
+ }
112
+ return issues;
113
+ }
114
+ export function formatAuditResultIssues(issues) {
115
+ return issues
116
+ .map((issue) => ` [${issue.severity}] ${issue.task_id} / ${issue.field}: ${issue.message}`)
117
+ .join("\n");
118
+ }