auditor-lambda 0.1.0 → 0.2.2

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 (87) hide show
  1. package/README.md +2 -1
  2. package/audit-code-wrapper-lib.mjs +205 -187
  3. package/dist/adapters/eslint.js +4 -2
  4. package/dist/adapters/npmAudit.js +1 -1
  5. package/dist/cli.js +296 -12
  6. package/dist/coverage.d.ts +0 -1
  7. package/dist/coverage.js +3 -34
  8. package/dist/extractors/bucketing.js +14 -35
  9. package/dist/extractors/disposition.js +8 -9
  10. package/dist/extractors/flows.js +14 -23
  11. package/dist/extractors/pathPatterns.d.ts +19 -0
  12. package/dist/extractors/pathPatterns.js +87 -0
  13. package/dist/extractors/surfaces.js +2 -7
  14. package/dist/io/artifacts.d.ts +23 -1
  15. package/dist/io/artifacts.js +3 -1
  16. package/dist/io/runArtifacts.js +1 -1
  17. package/dist/orchestrator/advance.js +1 -1
  18. package/dist/orchestrator/flowPlanning.d.ts +1 -1
  19. package/dist/orchestrator/flowPlanning.js +21 -28
  20. package/dist/orchestrator/internalExecutors.js +4 -7
  21. package/dist/orchestrator/planning.js +12 -20
  22. package/dist/orchestrator/resultIngestion.js +3 -2
  23. package/dist/orchestrator/runtimeValidation.js +5 -0
  24. package/dist/orchestrator/syntaxResolutionExecutor.js +10 -2
  25. package/dist/orchestrator/taskBuilder.d.ts +7 -2
  26. package/dist/orchestrator/taskBuilder.js +47 -52
  27. package/dist/prompts/renderWorkerPrompt.js +33 -0
  28. package/dist/providers/claudeCodeProvider.js +5 -0
  29. package/dist/providers/constants.d.ts +1 -0
  30. package/dist/providers/constants.js +1 -0
  31. package/dist/providers/index.js +9 -2
  32. package/dist/providers/spawnLoggedCommand.js +4 -0
  33. package/dist/reporting/mergeFindings.js +0 -7
  34. package/dist/reporting/rootCause.d.ts +0 -1
  35. package/dist/reporting/rootCause.js +0 -6
  36. package/dist/reporting/synthesis.js +18 -0
  37. package/dist/supervisor/operatorHandoff.d.ts +2 -0
  38. package/dist/supervisor/operatorHandoff.js +21 -9
  39. package/dist/supervisor/runLedger.js +6 -3
  40. package/dist/supervisor/sessionConfig.js +1 -0
  41. package/dist/types/flowCoverage.d.ts +1 -1
  42. package/dist/types/runLedger.d.ts +1 -1
  43. package/dist/types/runtimeValidation.d.ts +2 -1
  44. package/dist/types/sessionConfig.d.ts +2 -0
  45. package/dist/types/surfaces.d.ts +2 -1
  46. package/dist/types/workerSession.d.ts +4 -0
  47. package/dist/types.d.ts +0 -2
  48. package/dist/validation/auditResults.d.ts +11 -0
  49. package/dist/validation/auditResults.js +118 -0
  50. package/docs/agent-integrations.md +61 -56
  51. package/docs/agent-roles.md +69 -69
  52. package/docs/architecture.md +90 -90
  53. package/docs/artifacts.md +69 -69
  54. package/docs/bootstrap-install.md +1 -1
  55. package/docs/model-selection.md +86 -86
  56. package/docs/next-steps.md +11 -9
  57. package/docs/packaging.md +3 -3
  58. package/docs/pipeline.md +152 -152
  59. package/docs/production-readiness.md +6 -5
  60. package/docs/repo-layout.md +18 -18
  61. package/docs/run-flow.md +5 -5
  62. package/docs/session-config.md +216 -210
  63. package/docs/supervisor.md +70 -70
  64. package/docs/windows-setup.md +139 -139
  65. package/package.json +56 -56
  66. package/schemas/audit-code-v1alpha1.schema.json +80 -76
  67. package/schemas/audit_result.schema.json +54 -48
  68. package/schemas/audit_state.schema.json +2 -2
  69. package/schemas/audit_task.schema.json +60 -49
  70. package/schemas/blind_spot_register.schema.json +13 -3
  71. package/schemas/coverage_matrix.schema.json +14 -17
  72. package/schemas/critical_flows.schema.json +6 -3
  73. package/schemas/external_analyzer_results.schema.json +10 -4
  74. package/schemas/file_disposition.schema.json +33 -33
  75. package/schemas/finding.schema.json +86 -62
  76. package/schemas/flow_coverage.schema.json +53 -44
  77. package/schemas/graph_bundle.schema.json +12 -6
  78. package/schemas/merged_findings.schema.json +7 -2
  79. package/schemas/risk_register.schema.json +5 -1
  80. package/schemas/root_cause_clusters.schema.json +2 -5
  81. package/schemas/runtime_validation_report.schema.json +34 -34
  82. package/schemas/runtime_validation_tasks.schema.json +4 -1
  83. package/schemas/surface_manifest.schema.json +4 -1
  84. package/schemas/synthesis_report.schema.json +61 -61
  85. package/schemas/unit_manifest.schema.json +10 -3
  86. package/skills/audit-code/SKILL.md +37 -37
  87. package/skills/audit-code/audit-code.prompt.md +54 -54
@@ -1,13 +1,3 @@
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;
8
- }
9
- return ranges;
10
- }
11
1
  function taskPriority(hasExternalSignal, lens) {
12
2
  if (hasExternalSignal &&
13
3
  (lens === "security" || lens === "data_integrity" || lens === "reliability")) {
@@ -47,8 +37,9 @@ function pickAnalyzerLens(category) {
47
37
  return "maintainability";
48
38
  return "correctness";
49
39
  }
40
+ const DEFAULT_FILE_SPLIT_THRESHOLD = 3000;
50
41
  export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}) {
51
- const chunkSize = options.chunk_size ?? 200;
42
+ const fileSplitThreshold = options.file_split_threshold ?? DEFAULT_FILE_SPLIT_THRESHOLD;
52
43
  const allowed = new Set(options.limit_lenses ?? []);
53
44
  const enforceLensFilter = allowed.size > 0;
54
45
  const tasks = [];
@@ -63,49 +54,50 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
63
54
  if (enforceLensFilter && !allowed.has(lens)) {
64
55
  continue;
65
56
  }
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;
57
+ const hasExternalSignal = unit.files.some((f) => externalPaths.has(f));
58
+ const priority = taskPriority(hasExternalSignal, lens);
59
+ const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
60
+ // Split files that are individually too large to group; everything else
61
+ // goes into one task so the agent can reason across file boundaries.
62
+ const oversizedFiles = fileSplitThreshold > 0
63
+ ? unit.files.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
64
+ : [];
65
+ const normalFiles = unit.files.filter((f) => !oversizedFiles.includes(f));
66
+ // One task for all normal-sized files in this unit under this lens.
67
+ if (normalFiles.length > 0) {
68
+ const id = `${unit.unit_id}:${lens}`;
69
+ if (!seen.has(id)) {
93
70
  seen.add(id);
94
71
  tasks.push({
95
72
  task_id: id,
96
73
  unit_id: unit.unit_id,
97
74
  pass_id: `pass:${lens}`,
98
75
  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." : ""}`,
76
+ file_paths: normalFiles,
77
+ rationale: `Audit ${unit.unit_id} (${normalFiles.length} file${normalFiles.length === 1 ? "" : "s"}) under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
104
78
  priority,
105
79
  tags,
106
80
  });
107
81
  }
108
82
  }
83
+ // Oversized files each get their own task so the agent isn't overwhelmed.
84
+ for (const filePath of oversizedFiles) {
85
+ const id = `${unit.unit_id}:${lens}:${filePath}`;
86
+ if (seen.has(id))
87
+ continue;
88
+ seen.add(id);
89
+ const fileHasSignal = externalPaths.has(filePath);
90
+ tasks.push({
91
+ task_id: id,
92
+ unit_id: unit.unit_id,
93
+ pass_id: `pass:${lens}`,
94
+ lens,
95
+ file_paths: [filePath],
96
+ rationale: `Audit ${filePath} (large file, split from unit) under the ${lens} lens.${fileHasSignal ? " External analyzer signals raise priority." : ""}`,
97
+ priority: taskPriority(fileHasSignal, lens),
98
+ tags: fileHasSignal ? ["external_analyzer_signal", "large_file"] : ["large_file"],
99
+ });
100
+ }
109
101
  }
110
102
  }
111
103
  return tasks.sort((a, b) => {
@@ -115,34 +107,37 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
115
107
  return a.task_id.localeCompare(b.task_id);
116
108
  });
117
109
  }
118
- export function buildExternalSignalTasks(coverageMatrix, unitLineIndex, externalAnalyzerResults) {
110
+ /** Strip control characters and newlines, then truncate to maxLen. */
111
+ function sanitizeField(value, maxLen) {
112
+ return value.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, maxLen).trimEnd();
113
+ }
114
+ export function buildExternalSignalTasks(coverageMatrix, _unitLineIndex, externalAnalyzerResults) {
119
115
  if (!externalAnalyzerResults) {
120
116
  return [];
121
117
  }
122
118
  const tasks = [];
123
119
  const seen = new Set();
124
120
  for (const result of externalAnalyzerResults.results) {
125
- const lens = pickAnalyzerLens(result.category);
121
+ const safeCategory = sanitizeField(result.category, 80);
122
+ const safePath = sanitizeField(result.path ?? "", 260);
123
+ const safeSummary = sanitizeField(result.summary ?? "", 200);
124
+ const lens = pickAnalyzerLens(safeCategory);
126
125
  const coverage = coverageMatrix.files.find((file) => file.path === result.path);
127
126
  if (!coverage || coverage.audit_status === "excluded") {
128
127
  continue;
129
128
  }
130
- const lineCount = unitLineIndex[result.path] ?? 0;
131
- const id = `analyzer:${externalAnalyzerResults.tool}:${lens}:${result.path}:${result.id}`;
129
+ const id = `analyzer:${externalAnalyzerResults.tool}:${lens}:${safePath}:${result.id}`;
132
130
  if (seen.has(id)) {
133
131
  continue;
134
132
  }
135
133
  seen.add(id);
136
134
  tasks.push({
137
135
  task_id: id,
138
- unit_id: coverage.unit_ids[0] ?? `analyzer:${result.path}`,
136
+ unit_id: coverage.unit_ids[0] ?? `analyzer:${safePath}`,
139
137
  pass_id: `analyzer:${externalAnalyzerResults.tool}:${lens}`,
140
138
  lens,
141
139
  file_paths: [result.path],
142
- line_ranges: lineCount > 0
143
- ? [{ path: result.path, start: 1, end: lineCount }]
144
- : undefined,
145
- rationale: `Analyzer follow-up for ${result.path} under the ${lens} lens because ${externalAnalyzerResults.tool} reported: ${result.summary}`,
140
+ rationale: `Analyzer follow-up for ${safePath} under the ${lens} lens because ${externalAnalyzerResults.tool} reported: ${safeSummary}`,
146
141
  priority: "high",
147
142
  tags: [
148
143
  "external_analyzer_signal",
@@ -3,6 +3,39 @@ 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 ??
8
+ `${task.artifacts_dir}/audit_tasks.json`;
9
+ const lines = [
10
+ "You are executing one bounded audit task for audit-code.",
11
+ `Run ID: ${task.run_id}`,
12
+ `Repository root: ${task.repo_root}`,
13
+ "",
14
+ `Read the task file: ${tasksPath}`,
15
+ "It contains the task(s) assigned to this run.",
16
+ "",
17
+ "For each task:",
18
+ " 1. Read every file listed in file_paths in full using your file-reading tool.",
19
+ " If line_ranges are present, they are a focus hint — still read the whole file.",
20
+ " 2. Review the content under the specified lens.",
21
+ " 3. Emit one AuditResult with:",
22
+ " task_id, unit_id, pass_id, lens",
23
+ " reviewed_ranges: [{path, start, end}] covering what you read",
24
+ " findings: array (empty if nothing found)",
25
+ " Each finding must include:",
26
+ " id, title, category, severity, confidence, lens, summary, affected_files,",
27
+ " evidence (at least one excerpt or line reference from the file you read)",
28
+ " Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
29
+ `Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
30
+ ];
31
+ if (task.skip_worker_command) {
32
+ lines.push("", "Stop after writing the results file.");
33
+ }
34
+ else {
35
+ lines.push("", "Then run this command exactly:", command, "Stop after the command completes.");
36
+ }
37
+ return lines.join("\n");
38
+ }
6
39
  return [
7
40
  "You are executing one bounded audit step for audit-code.",
8
41
  `Run ID: ${task.run_id}`,
@@ -7,6 +7,11 @@ 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 = [
@@ -0,0 +1 @@
1
+ export declare const LOCAL_SUBPROCESS_PROVIDER_NAME: "local-subprocess";
@@ -0,0 +1 @@
1
+ export const LOCAL_SUBPROCESS_PROVIDER_NAME = "local-subprocess";
@@ -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,9 @@ 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 && hasConfiguredClaudeCode(sessionConfig) && claudeAvailable) {
42
43
  return "claude-code";
43
44
  }
44
45
  if (hasConfiguredOpenCode(sessionConfig) && opencodeAvailable) {
@@ -54,6 +55,12 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
54
55
  }
55
56
  export function createFreshSessionProvider(name, sessionConfig = {}) {
56
57
  const providerName = resolveFreshSessionProviderName(name, sessionConfig);
58
+ if (providerName === "local-subprocess" &&
59
+ (name ?? sessionConfig.provider) === "auto") {
60
+ process.stderr.write("audit-code: auto provider resolved to local-subprocess — no capable agent provider detected. " +
61
+ "Agent tasks will require manual dispatch. Configure claude-code, opencode, or subprocess-template " +
62
+ "in session-config.json to automate them.\n");
63
+ }
57
64
  switch (providerName) {
58
65
  case "local-subprocess":
59
66
  return new LocalSubprocessProvider();
@@ -3,6 +3,10 @@ import { spawn } from "node:child_process";
3
3
  function tee(write, chunk) {
4
4
  write.write(chunk);
5
5
  }
6
+ // On Windows `command` must be the resolved .cmd / .exe path because `spawn`
7
+ // does not consult PATH for executables without a shell. Callers should use
8
+ // `platformCommand()` (scripts/smoke-packaged-audit-code.mjs) or similar to
9
+ // supply the correct command form for the host OS.
6
10
  export async function spawnLoggedCommand(command, args, input, env) {
7
11
  return await new Promise((resolve, reject) => {
8
12
  const stdoutLog = createWriteStream(input.stdoutPath, { flags: "a" });
@@ -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,
@@ -14,6 +14,7 @@ export interface AuditCodeHandoffArtifactPaths {
14
14
  audit_tasks: string | null;
15
15
  runtime_validation_tasks: string | null;
16
16
  }
17
+ export declare const CONFIG_ERROR_BLOCKER_PREFIX = "config-error:";
17
18
  export interface AuditCodeHandoff {
18
19
  status: AuditTopLevelStatus;
19
20
  repo_root: string;
@@ -33,5 +34,6 @@ export declare function buildAuditCodeHandoff(params: {
33
34
  bundle: ArtifactBundle;
34
35
  providerName?: string | null;
35
36
  progressSummary: string;
37
+ isConfigError?: boolean;
36
38
  }): AuditCodeHandoff;
37
39
  export declare function writeAuditCodeHandoffArtifacts(handoff: AuditCodeHandoff): Promise<void>;
@@ -1,6 +1,8 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { writeJsonFile } from "../io/json.js";
4
+ import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "../providers/constants.js";
5
+ export const CONFIG_ERROR_BLOCKER_PREFIX = "config-error:";
4
6
  function quoteShellPath(path) {
5
7
  return `"${path.replace(/"/g, '\\"')}"`;
6
8
  }
@@ -23,8 +25,8 @@ function buildSummary(status, providerName, fallbackSummary) {
23
25
  ? `Automatic work can continue under ${providerName}. Re-run the same wrapper or inspect the listed artifacts if you need operator context.`
24
26
  : "Automatic work can continue. Re-run the same wrapper or inspect the listed artifacts if you need operator context.";
25
27
  }
26
- function buildSuggestedInputs(artifactsDir, status) {
27
- if (status !== "blocked") {
28
+ function buildSuggestedInputs(artifactsDir, status, isConfigError) {
29
+ if (status !== "blocked" || isConfigError) {
28
30
  return [];
29
31
  }
30
32
  const incomingDir = join(artifactsDir, "incoming");
@@ -52,11 +54,14 @@ function buildSuggestedCommands(suggestedInputs, status) {
52
54
  }
53
55
  return suggestedInputs.map((item) => `audit-code ${item.flag} ${quoteShellPath(item.suggested_path)}`);
54
56
  }
55
- function buildInteractiveProviderHint(status, providerName, sessionConfigPath) {
57
+ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, isConfigError) {
56
58
  if (status !== "blocked") {
57
59
  return null;
58
60
  }
59
- const providerLabel = providerName ?? "local-subprocess";
61
+ if (isConfigError) {
62
+ 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.`;
63
+ }
64
+ const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
60
65
  return `Current provider is ${providerLabel}. If you want the backend to continue through an interactive provider instead of importing results manually, set "provider" in ${sessionConfigPath} to "auto", "claude-code", "opencode", "subprocess-template", or "vscode-task", then run audit-code again from the repository root.`;
61
66
  }
62
67
  function renderMarkdown(handoff) {
@@ -109,6 +114,7 @@ function renderMarkdown(handoff) {
109
114
  return lines.join("\n");
110
115
  }
111
116
  export function buildAuditCodeHandoff(params) {
117
+ const isConfigError = params.isConfigError ?? false;
112
118
  const incomingDir = join(params.artifactsDir, "incoming");
113
119
  const artifactPaths = {
114
120
  incoming_dir: incomingDir,
@@ -123,7 +129,7 @@ export function buildAuditCodeHandoff(params) {
123
129
  ? join(params.artifactsDir, "runtime_validation_tasks.json")
124
130
  : null,
125
131
  };
126
- const suggestedInputs = buildSuggestedInputs(params.artifactsDir, params.state.status);
132
+ const suggestedInputs = buildSuggestedInputs(params.artifactsDir, params.state.status, isConfigError);
127
133
  return {
128
134
  status: params.state.status,
129
135
  repo_root: params.root,
@@ -133,12 +139,18 @@ export function buildAuditCodeHandoff(params) {
133
139
  pending_obligations: buildPendingObligations(params.state),
134
140
  suggested_inputs: suggestedInputs,
135
141
  suggested_commands: buildSuggestedCommands(suggestedInputs, params.state.status),
136
- interactive_provider_hint: buildInteractiveProviderHint(params.state.status, params.providerName ?? null, artifactPaths.session_config),
142
+ interactive_provider_hint: buildInteractiveProviderHint(params.state.status, params.providerName ?? null, artifactPaths.session_config, isConfigError),
137
143
  artifact_paths: artifactPaths,
138
144
  };
139
145
  }
140
146
  export async function writeAuditCodeHandoffArtifacts(handoff) {
141
- await mkdir(handoff.artifact_paths.incoming_dir, { recursive: true });
142
- await writeJsonFile(handoff.artifact_paths.operator_handoff_json, handoff);
143
- await writeFile(handoff.artifact_paths.operator_handoff_markdown, renderMarkdown(handoff), "utf8");
147
+ try {
148
+ await mkdir(handoff.artifact_paths.incoming_dir, { recursive: true });
149
+ await writeJsonFile(handoff.artifact_paths.operator_handoff_json, handoff);
150
+ await writeFile(handoff.artifact_paths.operator_handoff_markdown, renderMarkdown(handoff), "utf8");
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ throw new Error(`Failed to write operator handoff artifacts: ${message}`);
155
+ }
144
156
  }
@@ -1,4 +1,4 @@
1
- import { readJsonFile, writeJsonFile } from "../io/json.js";
1
+ import { isFileMissingError, readJsonFile, writeJsonFile } from "../io/json.js";
2
2
  function ledgerPath(artifactsDir) {
3
3
  return `${artifactsDir}/run-ledger.json`;
4
4
  }
@@ -6,8 +6,11 @@ export async function loadRunLedger(artifactsDir) {
6
6
  try {
7
7
  return await readJsonFile(ledgerPath(artifactsDir));
8
8
  }
9
- catch {
10
- return { runs: [] };
9
+ catch (error) {
10
+ if (isFileMissingError(error)) {
11
+ return { runs: [] };
12
+ }
13
+ throw error;
11
14
  }
12
15
  }
13
16
  export async function appendRunLedgerEntry(artifactsDir, entry) {
@@ -16,6 +16,7 @@ export async function loadSessionConfig(artifactsDir) {
16
16
  const configPath = getSessionConfigPath(artifactsDir);
17
17
  const rawConfig = await readOptionalJsonFile(configPath);
18
18
  if (rawConfig === undefined) {
19
+ process.stderr.write(`[session-config] no session-config.json found at ${configPath}; using empty defaults\n`);
19
20
  return {};
20
21
  }
21
22
  const issues = validateSessionConfig(rawConfig);
@@ -3,7 +3,7 @@ export interface FlowCoverageRecord {
3
3
  paths: string[];
4
4
  required_lenses: string[];
5
5
  completed_lenses: string[];
6
- status: string;
6
+ status: "pending" | "partial" | "complete";
7
7
  notes?: string[];
8
8
  }
9
9
  export interface FlowCoverageManifest {
@@ -3,7 +3,7 @@ export interface RunLedgerEntry {
3
3
  provider: string;
4
4
  obligation_id: string | null;
5
5
  selected_executor: string | null;
6
- status: string;
6
+ status: "completed" | "blocked" | "failed" | "no_progress";
7
7
  started_at: string;
8
8
  ended_at: string;
9
9
  result_path: string;
@@ -1,6 +1,7 @@
1
+ export type RuntimeValidationKind = "unit-risk-check" | "critical-flow-check";
1
2
  export interface RuntimeValidationTask {
2
3
  id: string;
3
- kind: string;
4
+ kind: RuntimeValidationKind;
4
5
  target_paths: string[];
5
6
  reason: string;
6
7
  priority: "high" | "medium" | "low";
@@ -24,4 +24,6 @@ export interface SessionConfig {
24
24
  claude_code?: ClaudeCodeConfig;
25
25
  opencode?: OpenCodeConfig;
26
26
  vscode_task?: VSCodeTaskConfig;
27
+ agent_task_batch_size?: number;
28
+ parallel_workers?: number;
27
29
  }
@@ -1,6 +1,7 @@
1
+ export type SurfaceKind = "interface" | "background";
1
2
  export interface SurfaceRecord {
2
3
  id: string;
3
- kind: string;
4
+ kind: SurfaceKind;
4
5
  entrypoint: string;
5
6
  exposure?: string;
6
7
  methods?: string[];
@@ -8,6 +8,10 @@ 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;
15
+ timeout_ms?: number;
16
+ max_retries?: number;
13
17
  }
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[];
@@ -82,7 +81,6 @@ export interface Finding {
82
81
  likelihood?: string;
83
82
  evidence?: string[];
84
83
  reproduction?: string[];
85
- remediation?: string[];
86
84
  systemic?: boolean;
87
85
  related_findings?: string[];
88
86
  }
@@ -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;