auditor-lambda 0.9.1 → 0.9.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.
@@ -1277,6 +1277,11 @@ async function copyClaudeDesktopBundleFiles(bundleRoot, serverRoot) {
1277
1277
  await mkdir(serverRoot, { recursive: true });
1278
1278
  await cp(distEntry.replace(/dist[\\/]index\.js$/, 'dist'), join(bundleRoot, 'dist'), { recursive: true, force: true });
1279
1279
  await cp(join(repoRoot, 'schemas'), join(bundleRoot, 'schemas'), { recursive: true, force: true });
1280
+ // dist/cli/dispatch.js reads dispatch/lens-definitions.json from the package
1281
+ // root at runtime, so the bundle must ship the top-level dispatch/ data dir
1282
+ // (lens-definitions.json + the standalone validate/merge helpers) the same way
1283
+ // the npm `files` array does. Omitting it ENOENTs the first dispatch step.
1284
+ await cp(join(repoRoot, 'dispatch'), join(bundleRoot, 'dispatch'), { recursive: true, force: true });
1280
1285
  await cp(join(repoRoot, 'skills', 'audit-code'), join(bundleRoot, 'skills', 'audit-code'), { recursive: true, force: true });
1281
1286
  await writeFile(join(bundleRoot, 'audit-code.mjs'), await readFile(join(repoRoot, 'audit-code.mjs')));
1282
1287
  await writeFile(join(bundleRoot, 'audit-code-wrapper-lib.mjs'), await readFile(join(repoRoot, 'audit-code-wrapper-lib.mjs')));
@@ -15,7 +15,7 @@ const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
15
15
  : join(process.cwd(), ".audit-artifacts");
16
16
 
17
17
  const taskResultsDir = join(artifactsDir, "runs", runId, "task-results");
18
- const auditResultsPath = join(artifactsDir, "runs", runId, "audit-results.json");
18
+ const auditResultsPath = join(artifactsDir, "runs", runId, "run-results.json");
19
19
  const failedTasksPath = join(artifactsDir, "runs", runId, "failed-tasks.json");
20
20
  const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
21
21
 
@@ -14,9 +14,28 @@ export async function cmdMergeAndIngest(argv) {
14
14
  const artifactsDir = getArtifactsDir(argv);
15
15
  const runDir = join(artifactsDir, "runs", runId);
16
16
  const taskResultsDir = join(runDir, "task-results");
17
- const auditResultsPath = join(runDir, "audit-results.json");
17
+ const auditResultsPath = join(runDir, "run-results.json");
18
18
  const taskPath = join(runDir, "task.json");
19
19
  const tasksPath = join(runDir, "pending-audit-tasks.json");
20
+ const mergeCompletePath = join(runDir, "merge-complete.json");
21
+ // Idempotency: a fully-merged run is terminal. A stray re-invocation for the
22
+ // same run-id (e.g. after the run already advanced to the next deepening
23
+ // round, which rewrites this run dir's pending-audit-tasks.json to the *next*
24
+ // round's tasks) must be a clean no-op — not a spurious "all results missing"
25
+ // hard failure that also truncates the transient results file. Replay the
26
+ // recorded summary and exit 0.
27
+ let priorSummary = null;
28
+ try {
29
+ priorSummary = await readJsonFile(mergeCompletePath);
30
+ }
31
+ catch (e) {
32
+ if (!isFileMissingError(e))
33
+ throw e;
34
+ }
35
+ if (priorSummary) {
36
+ console.log(JSON.stringify({ ...priorSummary, idempotent_replay: true }, null, 2));
37
+ return;
38
+ }
20
39
  const workerTask = await readJsonFile(taskPath);
21
40
  const resultMap = await loadDispatchResultMap(runDir);
22
41
  if (!resultMap) {
@@ -42,7 +61,7 @@ export async function cmdMergeAndIngest(argv) {
42
61
  const passing = [];
43
62
  const failing = [];
44
63
  const seenTaskIds = new Set();
45
- let spuriousFileCount = 0;
64
+ const spuriousFiles = [];
46
65
  const fallbackByTaskId = new Map();
47
66
  for (const filename of files) {
48
67
  const filePath = resolve(join(taskResultsDir, filename));
@@ -68,10 +87,16 @@ export async function cmdMergeAndIngest(argv) {
68
87
  // task-results/ dir are legitimate and must not inflate the count or bury
69
88
  // the real stray-file signal (3 -> 191 over a run before this fix).
70
89
  if (!isCanonicalResultFilename(filename)) {
71
- spuriousFileCount++;
72
- process.stderr.write(`[merge-and-ingest] Warning: unexpected file in task-results/: ${filename}\n`);
90
+ spuriousFiles.push(filename);
73
91
  }
74
92
  }
93
+ // Collapse stray-file warnings into a single stderr line so the real summary
94
+ // (emitted as the sole stdout JSON payload) is never buried under a wall of
95
+ // per-file warnings.
96
+ if (spuriousFiles.length > 0) {
97
+ process.stderr.write(`[merge-and-ingest] Warning: ${spuriousFiles.length} unexpected file(s) in ` +
98
+ `task-results/ ignored: ${spuriousFiles.join(", ")}\n`);
99
+ }
75
100
  for (const task of allTasks) {
76
101
  const entry = entryByTaskId.get(task.task_id);
77
102
  if (!entry) {
@@ -134,14 +159,18 @@ export async function cmdMergeAndIngest(argv) {
134
159
  failing.push({ task_id: taskId ?? task.task_id, errors: resultErrors });
135
160
  }
136
161
  }
137
- await writeJsonFile(auditResultsPath, passing);
138
162
  const failedTasksPath = join(runDir, "failed-tasks.json");
139
163
  if (failing.length > 0) {
140
164
  await writeJsonFile(failedTasksPath, failing);
141
165
  }
142
166
  if (passing.length === 0 && failing.length > 0) {
167
+ // Nothing merged and at least one failure: a blocked no-op. Do NOT write the
168
+ // transient results file here — truncating it to [] reads as catastrophic
169
+ // data loss on a re-run when the cumulative audit_results.jsonl store is in
170
+ // fact intact and the first merge had simply already succeeded.
143
171
  throw new Error(`All ${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
144
172
  }
173
+ await writeJsonFile(auditResultsPath, passing);
145
174
  const findingCount = passing.reduce((sum, result) => sum + result.findings.length, 0);
146
175
  let result = null;
147
176
  if (passing.length > 0) {
@@ -197,12 +226,12 @@ export async function cmdMergeAndIngest(argv) {
197
226
  errors: [],
198
227
  });
199
228
  await writeJsonFile(workerTask.result_path, workerResult);
200
- console.log(JSON.stringify({
229
+ const summaryPayload = {
201
230
  run_id: runId,
202
231
  status,
203
232
  accepted_count: passing.length,
204
233
  rejected_count: failing.length,
205
- spurious_file_count: spuriousFileCount,
234
+ spurious_file_count: spuriousFiles.length,
206
235
  finding_count: findingCount,
207
236
  audit_results_path: auditResultsPath,
208
237
  ...(retryDispatchPath ? { retry_dispatch_path: retryDispatchPath } : {}),
@@ -212,7 +241,15 @@ export async function cmdMergeAndIngest(argv) {
212
241
  progress_summary: workerResult.summary,
213
242
  next_likely_step: workerResult.next_likely_step,
214
243
  } : {}),
215
- }, null, 2));
244
+ };
245
+ // Record a completion marker for a fully-merged run so a stray re-invocation
246
+ // replays this summary (above) instead of re-processing — and possibly
247
+ // clobbering — terminal state. Only on full success: a partial merge is meant
248
+ // to be re-run after the failed packets are retried, so it stays replayable.
249
+ if (failing.length === 0) {
250
+ await writeJsonFile(mergeCompletePath, summaryPayload);
251
+ }
252
+ console.log(JSON.stringify(summaryPayload, null, 2));
216
253
  if (failing.length > 0) {
217
254
  process.exitCode = 2;
218
255
  }
@@ -35,6 +35,42 @@ async function runDeterministicForNextStep(params) {
35
35
  const FINALIZATION_CYCLE_TOLERANCE = 16;
36
36
  const seenStateSignatures = new Set();
37
37
  const obligationTrail = [];
38
+ // Build the terminal step for a deterministic loop that has stopped advancing
39
+ // (hit the run backstop or the finalization cycle guard). A rendered report is
40
+ // the deliverable: if synthesis already produced one — or the state is formally
41
+ // complete — present it instead of reporting the stopped loop as a bare
42
+ // "blocked" failure. A completed audit must never surface as blocked just
43
+ // because finalization kept churning (e.g. a runtime_validation <-> synthesis
44
+ // ping-pong, or revision churn from filesystem retries) after the report was
45
+ // written. With no report yet, the stop is a genuine block.
46
+ async function terminalStep(bundle, state, blockedReason) {
47
+ const reportRendered = state.status === "complete" || Boolean(bundle.audit_report);
48
+ await writeHandoffOnly({
49
+ root: params.root,
50
+ artifactsDir: params.artifactsDir,
51
+ bundle,
52
+ audit_state: state,
53
+ progress_summary: reportRendered && state.status !== "complete"
54
+ ? `Audit report already rendered; ending run. ${blockedReason}`
55
+ : blockedReason,
56
+ providerName: LOCAL_SUBPROCESS_PROVIDER_NAME,
57
+ });
58
+ if (!reportRendered) {
59
+ return { kind: "blocked", state, bundle, reason: blockedReason };
60
+ }
61
+ const promoted = await promoteFinalAuditReport({
62
+ artifactsDir: params.artifactsDir,
63
+ repoRoot: params.root,
64
+ });
65
+ return {
66
+ kind: "complete",
67
+ state,
68
+ bundle,
69
+ finalReportPath: promoted.promoted
70
+ ? join(params.root, AUDIT_REPORT_FILENAME)
71
+ : join(params.artifactsDir, AUDIT_REPORT_FILENAME),
72
+ };
73
+ }
38
74
  for (let index = 0; index < params.maxRuns; index++) {
39
75
  const bundle = await loadArtifactBundle(params.artifactsDir);
40
76
  const decision = decideNextStep(bundle);
@@ -318,24 +354,14 @@ async function runDeterministicForNextStep(params) {
318
354
  `progress; stopping. Cycling obligations: ${cycle.join(" -> ")}.`,
319
355
  timestamp: new Date().toISOString(),
320
356
  });
321
- return {
322
- kind: "blocked",
323
- state: result.audit_state,
324
- bundle: result.updated_bundle,
325
- reason: "Finalization is not converging: deterministic executors kept revisiting " +
326
- `prior artifact states (${cycle.join(" -> ")}). The report has been ` +
327
- "rendered; review whether these obligations are erroneously invalidating each other.",
328
- };
357
+ return await terminalStep(result.updated_bundle, result.audit_state, "Finalization is not converging: deterministic executors kept revisiting " +
358
+ `prior artifact states (${cycle.join(" -> ")}). Review whether these ` +
359
+ "obligations are erroneously invalidating each other.");
329
360
  }
330
361
  }
331
362
  const bundle = await loadArtifactBundle(params.artifactsDir);
332
363
  const state = deriveAuditState(bundle);
333
- return {
334
- kind: "blocked",
335
- state,
336
- bundle,
337
- reason: `Reached max run limit (${params.maxRuns}) before a review, report, or blocker step was ready.`,
338
- };
364
+ return await terminalStep(bundle, state, `Reached max run limit (${params.maxRuns}) before a review, report, or blocker step was ready.`);
339
365
  }
340
366
  export async function cmdNextStep(argv) {
341
367
  const root = getRootDir(argv);
@@ -90,7 +90,7 @@ export async function ensureSemanticReviewRun(params) {
90
90
  const paths = getRunPaths(params.artifactsDir, runId);
91
91
  const pendingTasks = await addFileLineCountHints(params.root, buildPendingAuditTasks(params.bundle));
92
92
  const pendingTasksPath = join(paths.runDir, "pending-audit-tasks.json");
93
- const auditResultsPath = join(paths.runDir, "audit-results.json");
93
+ const auditResultsPath = join(paths.runDir, "run-results.json");
94
94
  const taskReadPaths = new Set();
95
95
  for (const pt of pendingTasks) {
96
96
  for (const fp of pt.file_paths)
@@ -70,7 +70,7 @@ async function buildParallelWaveSlots(params) {
70
70
  runCount += 1;
71
71
  const slotRunId = buildRunId(obligationId, runCount);
72
72
  const slotPaths = getRunPaths(artifactsDir, slotRunId);
73
- const slotAuditResultsPath = join(slotPaths.runDir, "audit-results.json");
73
+ const slotAuditResultsPath = join(slotPaths.runDir, "run-results.json");
74
74
  const slotPendingTasksPath = join(slotPaths.runDir, "pending-audit-tasks.json");
75
75
  const slotReadPaths = new Set();
76
76
  for (const t of group) {
@@ -398,7 +398,7 @@ async function runSingleWorkerStep(params) {
398
398
  ? join(paths.runDir, "pending-audit-tasks.json")
399
399
  : undefined;
400
400
  const providerAuditResultsPath = preferredExecutor === "agent"
401
- ? join(paths.runDir, "audit-results.json")
401
+ ? join(paths.runDir, "run-results.json")
402
402
  : auditResultsPath;
403
403
  const providerReadPaths = new Set();
404
404
  if (pendingAuditTasks) {
@@ -694,7 +694,7 @@ export async function cmdRunToCompletion(argv) {
694
694
  const blockPaths = getRunPaths(artifactsDir, blockRunId);
695
695
  const blockPendingTasks = await addFileLineCountHints(root, buildPendingAuditTasks(bundle));
696
696
  const blockPendingTasksPath = join(blockPaths.runDir, "pending-audit-tasks.json");
697
- const blockAuditResultsPath = join(blockPaths.runDir, "audit-results.json");
697
+ const blockAuditResultsPath = join(blockPaths.runDir, "run-results.json");
698
698
  const blockReadPaths = new Set();
699
699
  for (const pt of blockPendingTasks) {
700
700
  for (const fp of pt.file_paths)
@@ -1031,23 +1031,36 @@ export async function cmdRunToCompletion(argv) {
1031
1031
  const bundle = await loadArtifactBundle(artifactsDir);
1032
1032
  const decision = decideNextStep(bundle);
1033
1033
  const state = decision.state;
1034
- if (state.status === "complete") {
1034
+ // A rendered report is the deliverable: if synthesis already produced one (or
1035
+ // the state is formally complete), finish the run on it instead of stranding
1036
+ // it in the artifacts dir behind a bare "max run limit" non-completion. This
1037
+ // mirrors next-step's terminalStep so both loops present a completed audit the
1038
+ // same way, even when finalization churned (runtime_validation <-> synthesis
1039
+ // ping-pong, or filesystem-retry revision churn) up to the backstop. With no
1040
+ // report yet, the run limit is a genuine non-terminal stop.
1041
+ const reportRendered = state.status === "complete" || Boolean(bundle.audit_report);
1042
+ if (reportRendered) {
1035
1043
  await clearDispatchFiles(artifactsDir);
1036
1044
  }
1045
+ const terminalState = reportRendered && state.status !== "complete"
1046
+ ? { ...state, status: "complete" }
1047
+ : state;
1037
1048
  await emitEnvelope({
1038
1049
  root,
1039
1050
  artifactsDir,
1040
1051
  bundle,
1041
- audit_state: state,
1052
+ audit_state: terminalState,
1042
1053
  selected_obligation: lastResult?.obligation_id ?? decision.selected_obligation,
1043
1054
  selected_executor: lastResult?.selected_executor ?? decision.selected_executor,
1044
1055
  progress_made: anyProgress,
1045
1056
  artifacts_written: Array.from(artifactsWritten),
1046
- progress_summary: `Reached max run limit (${maxRuns}) before terminal state.`,
1047
- next_likely_step: state.status === "complete" ? null : decision.selected_obligation,
1057
+ progress_summary: reportRendered && state.status !== "complete"
1058
+ ? `Audit report already rendered; completing the run after reaching the max run limit (${maxRuns}) during finalization.`
1059
+ : `Reached max run limit (${maxRuns}) before terminal state.`,
1060
+ next_likely_step: reportRendered ? null : decision.selected_obligation,
1048
1061
  providerName: provider.name,
1049
1062
  });
1050
- if (state.status === "complete") {
1063
+ if (reportRendered) {
1051
1064
  await promoteFinalAuditReport({ artifactsDir, repoRoot: root });
1052
1065
  }
1053
1066
  }
@@ -252,7 +252,12 @@ export function buildAuditCodeHandoff(params) {
252
252
  current_task: artifactPaths.current_task,
253
253
  current_prompt: artifactPaths.current_prompt,
254
254
  audit_results: params.activeReviewRun.audit_results_path,
255
- final_report: join(params.root, AUDIT_REPORT_FILENAME),
255
+ // Synthesis writes the report into the artifacts dir; it is only promoted
256
+ // to <repo-root>/audit-report.md at completion (which then removes the
257
+ // artifacts dir). A blocked-for-review handoff happens before that, so the
258
+ // advertised deliverable must point at its real mid-run location, not the
259
+ // repo-root path that does not exist yet.
260
+ final_report: join(params.artifactsDir, AUDIT_REPORT_FILENAME),
256
261
  };
257
262
  }
258
263
  return handoff;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",