auditor-lambda 0.2.4 → 0.2.6
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.
- package/README.md +12 -0
- package/audit-code-wrapper-lib.mjs +7 -1
- package/dist/cli.js +324 -27
- package/dist/io/runArtifacts.d.ts +2 -1
- package/dist/io/runArtifacts.js +2 -1
- package/dist/orchestrator/flowRequeue.d.ts +2 -2
- package/dist/orchestrator/flowRequeue.js +15 -2
- package/dist/orchestrator/internalExecutors.js +34 -10
- package/dist/orchestrator/requeue.js +1 -0
- package/dist/orchestrator/requeueCommand.js +15 -2
- package/dist/orchestrator/resultIngestion.d.ts +2 -1
- package/dist/orchestrator/resultIngestion.js +21 -0
- package/dist/orchestrator/state.js +10 -1
- package/dist/orchestrator/taskBuilder.js +4 -2
- package/dist/orchestrator/trivialAudit.d.ts +4 -0
- package/dist/orchestrator/trivialAudit.js +46 -0
- package/dist/prompts/renderWorkerPrompt.js +5 -2
- package/dist/providers/spawnLoggedCommand.js +17 -0
- package/dist/reporting/mergeFindings.js +14 -11
- package/dist/reporting/rootCause.js +92 -9
- package/dist/supervisor/sessionConfig.js +4 -2
- package/dist/types.d.ts +5 -0
- package/dist/validation/auditResults.d.ts +5 -2
- package/dist/validation/auditResults.js +369 -42
- package/docs/artifacts.md +8 -1
- package/docs/contract.md +118 -27
- package/docs/field-trial-bug-report.md +237 -0
- package/docs/session-config.md +20 -1
- package/docs/usage.md +22 -0
- package/package.json +3 -1
- package/schemas/audit_result.schema.json +3 -2
- package/schemas/audit_task.schema.json +10 -0
- package/scripts/postinstall.mjs +19 -0
- package/skills/audit-code/audit-code.prompt.md +15 -11
package/README.md
CHANGED
|
@@ -102,6 +102,18 @@ audit-code validate
|
|
|
102
102
|
|
|
103
103
|
That check now covers the artifact bundle plus `session-config.json` and explicit provider readiness.
|
|
104
104
|
|
|
105
|
+
For native batch ingestion of multiple result files:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
audit-code --batch-results /path/to/audit-results-dir
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For task-to-coverage inspection without reverse-engineering multiple artifacts:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
audit-code explain-task <task_id>
|
|
115
|
+
```
|
|
116
|
+
|
|
105
117
|
The backend wrapper response schema is `schemas/audit-code-v1alpha1.schema.json`.
|
|
106
118
|
|
|
107
119
|
## Backend Provider Modes
|
|
@@ -234,13 +234,14 @@ async function ensureBuilt() {
|
|
|
234
234
|
|
|
235
235
|
function printHelp({ usageName, preferredEntrypoint }) {
|
|
236
236
|
const lines = [
|
|
237
|
-
`Usage: node ${usageName} [--single-step] [--root PATH] [--artifacts-dir PATH] [--results FILE] [--updates FILE] [--external-analyzer-results FILE]`,
|
|
237
|
+
`Usage: node ${usageName} [--single-step] [--root PATH] [--artifacts-dir PATH] [--results FILE] [--batch-results DIR] [--updates FILE] [--external-analyzer-results FILE] [--timeout MS]`,
|
|
238
238
|
'',
|
|
239
239
|
'Helper commands:',
|
|
240
240
|
'- prompt-path prints the absolute path to the canonical /audit-code prompt asset',
|
|
241
241
|
'- install bootstraps /audit-code into supported repo-local host surfaces',
|
|
242
242
|
'- install-host --host copilot keeps the narrower Copilot-focused install path available',
|
|
243
243
|
'- validate checks the current artifact bundle plus session-config/provider readiness and exits non-zero when issues exist',
|
|
244
|
+
'- explain-task <task_id> prints the resolved file coverage and current status for a task id',
|
|
244
245
|
'',
|
|
245
246
|
'Primary usage:',
|
|
246
247
|
'- from the repository root, run the wrapper with no arguments',
|
|
@@ -903,6 +904,11 @@ export async function runAuditCodeWrapper({
|
|
|
903
904
|
return;
|
|
904
905
|
}
|
|
905
906
|
|
|
907
|
+
if (argv[0] === 'explain-task') {
|
|
908
|
+
await runDistCommand('explain-task', argv.slice(1));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
906
912
|
const wrapperArgs = [...argv];
|
|
907
913
|
if (defaultSingleStep && !hasFlag(wrapperArgs, '--single-step')) {
|
|
908
914
|
wrapperArgs.push('--single-step');
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { access, mkdir } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readdir, rename } from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { buildRepoManifest } from "./extractors/fileInventory.js";
|
|
5
5
|
import { buildFileDisposition } from "./extractors/disposition.js";
|
|
6
6
|
import { buildCriticalFlowManifest } from "./extractors/flows.js";
|
|
@@ -50,6 +50,10 @@ function getArtifactsDir(argv) {
|
|
|
50
50
|
function getRootDir(argv) {
|
|
51
51
|
return resolve(getFlag(argv, "--root", "."));
|
|
52
52
|
}
|
|
53
|
+
function getBatchResultsDir(argv) {
|
|
54
|
+
const value = getFlag(argv, "--batch-results");
|
|
55
|
+
return value ? resolve(value) : undefined;
|
|
56
|
+
}
|
|
53
57
|
function getMaxRuns(argv) {
|
|
54
58
|
const raw = Number(getFlag(argv, "--max-runs", String(DEFAULT_MAX_RUNS)));
|
|
55
59
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_RUNS;
|
|
@@ -78,6 +82,15 @@ function getParallelWorkers(argv, sessionConfig) {
|
|
|
78
82
|
}
|
|
79
83
|
return 1;
|
|
80
84
|
}
|
|
85
|
+
function getTimeoutMs(argv, sessionConfig) {
|
|
86
|
+
const fromArg = getFlag(argv, "--timeout");
|
|
87
|
+
if (fromArg !== undefined) {
|
|
88
|
+
const parsed = Number(fromArg);
|
|
89
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
90
|
+
return Math.floor(parsed);
|
|
91
|
+
}
|
|
92
|
+
return sessionConfig.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
93
|
+
}
|
|
81
94
|
function chunkArray(arr, size) {
|
|
82
95
|
const chunks = [];
|
|
83
96
|
for (let i = 0; i < arr.length; i += size) {
|
|
@@ -203,6 +216,29 @@ async function buildLineIndex(root, repoManifest) {
|
|
|
203
216
|
}
|
|
204
217
|
return Object.fromEntries(entries);
|
|
205
218
|
}
|
|
219
|
+
async function buildLineIndexForPaths(root, paths) {
|
|
220
|
+
const uniquePaths = [...new Set(paths)].sort();
|
|
221
|
+
const entries = await Promise.all(uniquePaths.map(async (path) => {
|
|
222
|
+
try {
|
|
223
|
+
return [path, await countLines(resolve(root, path))];
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [path, 0];
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
return Object.fromEntries(entries);
|
|
230
|
+
}
|
|
231
|
+
async function listBatchResultFiles(batchDir) {
|
|
232
|
+
const entries = await readdir(batchDir, { withFileTypes: true });
|
|
233
|
+
const files = entries
|
|
234
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
|
|
235
|
+
.map((entry) => join(batchDir, entry.name))
|
|
236
|
+
.sort((a, b) => a.localeCompare(b));
|
|
237
|
+
if (files.length === 0) {
|
|
238
|
+
throw new Error(`No JSON audit result files found in ${batchDir}.`);
|
|
239
|
+
}
|
|
240
|
+
return files;
|
|
241
|
+
}
|
|
206
242
|
const PROJECT_SIGNALS = [
|
|
207
243
|
"package.json",
|
|
208
244
|
"go.mod",
|
|
@@ -229,33 +265,122 @@ async function detectProjectRoot(root) {
|
|
|
229
265
|
}
|
|
230
266
|
function buildPendingAuditTasks(bundle) {
|
|
231
267
|
const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
|
|
232
|
-
return (bundle.audit_tasks ?? []).filter((task) => !completedTaskIds.has(task.task_id));
|
|
268
|
+
return (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
|
|
269
|
+
}
|
|
270
|
+
function formatAuditResultValidationError(issues) {
|
|
271
|
+
return (`audit-results validation failed with ${issues.length} error(s):\n` +
|
|
272
|
+
formatAuditResultIssues(issues));
|
|
273
|
+
}
|
|
274
|
+
function buildWorkerFailureBlocker(workerResult) {
|
|
275
|
+
const details = workerResult.errors.filter((error) => error.trim().length > 0);
|
|
276
|
+
return details.length > 0
|
|
277
|
+
? `${workerResult.summary} ${details.join(" ")}`
|
|
278
|
+
: workerResult.summary;
|
|
279
|
+
}
|
|
280
|
+
function looksLikeCliFlag(value) {
|
|
281
|
+
return typeof value === "string" && value.startsWith("--");
|
|
282
|
+
}
|
|
283
|
+
async function maybeArchiveLegacyPendingResults(auditResultsPath) {
|
|
284
|
+
if (!auditResultsPath || basename(auditResultsPath) !== "worker_results_pending.json") {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const archivedPath = join(dirname(auditResultsPath), `worker_results_submitted_${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
|
|
288
|
+
try {
|
|
289
|
+
await rename(auditResultsPath, archivedPath);
|
|
290
|
+
return archivedPath;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
process.stderr.write(`[audit-results cleanup] failed to archive ${auditResultsPath}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
233
296
|
}
|
|
234
297
|
async function runAuditStep(options) {
|
|
235
298
|
const bundle = await loadArtifactBundle(options.artifactsDir);
|
|
299
|
+
const lineIndex = bundle.repo_manifest
|
|
300
|
+
? await buildLineIndex(options.root, bundle.repo_manifest)
|
|
301
|
+
: undefined;
|
|
302
|
+
if (looksLikeCliFlag(options.auditResultsPath)) {
|
|
303
|
+
throw new Error(`Invalid audit results path '${options.auditResultsPath}'. This looks like a CLI flag rather than a file path.`);
|
|
304
|
+
}
|
|
236
305
|
const auditResults = options.auditResultsPath
|
|
237
306
|
? await readJsonFile(options.auditResultsPath)
|
|
238
307
|
: undefined;
|
|
308
|
+
if (auditResults !== undefined) {
|
|
309
|
+
const issues = validateAuditResults(auditResults, bundle.audit_tasks ?? [], {
|
|
310
|
+
lineIndex,
|
|
311
|
+
});
|
|
312
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
313
|
+
const warnings = issues.filter((issue) => issue.severity === "warning");
|
|
314
|
+
if (warnings.length > 0) {
|
|
315
|
+
process.stderr.write(`audit-results validation: ${warnings.length} warning(s):\n` +
|
|
316
|
+
formatAuditResultIssues(warnings) +
|
|
317
|
+
"\n");
|
|
318
|
+
}
|
|
319
|
+
if (errors.length > 0) {
|
|
320
|
+
throw new Error(formatAuditResultValidationError(errors));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
239
323
|
const runtimeValidationUpdates = options.runtimeUpdatesPath
|
|
240
324
|
? await readJsonFile(options.runtimeUpdatesPath)
|
|
241
325
|
: undefined;
|
|
242
326
|
const externalAnalyzerResults = options.externalAnalyzerPath
|
|
243
327
|
? await readJsonFile(options.externalAnalyzerPath)
|
|
244
328
|
: undefined;
|
|
245
|
-
const lineIndex = bundle.repo_manifest
|
|
246
|
-
? await buildLineIndex(options.root, bundle.repo_manifest)
|
|
247
|
-
: undefined;
|
|
248
329
|
const result = await advanceAudit(bundle, {
|
|
249
330
|
root: options.root,
|
|
250
331
|
lineIndex,
|
|
251
|
-
auditResults,
|
|
332
|
+
auditResults: auditResults,
|
|
252
333
|
runtimeValidationUpdates,
|
|
253
334
|
externalAnalyzerResults,
|
|
254
335
|
preferredExecutor: options.preferredExecutor,
|
|
255
336
|
});
|
|
256
337
|
await writeCoreArtifacts(options.artifactsDir, result.updated_bundle);
|
|
338
|
+
const archivedPendingResults = await maybeArchiveLegacyPendingResults(options.auditResultsPath);
|
|
339
|
+
if (archivedPendingResults) {
|
|
340
|
+
result.progress_summary +=
|
|
341
|
+
` Archived legacy staging file to ${archivedPendingResults}.`;
|
|
342
|
+
}
|
|
257
343
|
return result;
|
|
258
344
|
}
|
|
345
|
+
async function ingestBatchAuditResults(options) {
|
|
346
|
+
const batchFiles = await listBatchResultFiles(options.batchDir);
|
|
347
|
+
const artifactsWritten = new Set();
|
|
348
|
+
const progressSummaries = [];
|
|
349
|
+
let lastStep = null;
|
|
350
|
+
let anyProgress = false;
|
|
351
|
+
for (const batchFile of batchFiles) {
|
|
352
|
+
const step = await runAuditStep({
|
|
353
|
+
root: options.root,
|
|
354
|
+
artifactsDir: options.artifactsDir,
|
|
355
|
+
preferredExecutor: "result_ingestion_executor",
|
|
356
|
+
auditResultsPath: batchFile,
|
|
357
|
+
});
|
|
358
|
+
lastStep = step;
|
|
359
|
+
anyProgress ||= step.progress_made;
|
|
360
|
+
for (const artifact of step.artifacts_written) {
|
|
361
|
+
artifactsWritten.add(artifact);
|
|
362
|
+
}
|
|
363
|
+
progressSummaries.push(`${basename(batchFile)}: ${step.progress_summary}`);
|
|
364
|
+
}
|
|
365
|
+
const bundle = lastStep?.updated_bundle ??
|
|
366
|
+
(await loadArtifactBundle(options.artifactsDir));
|
|
367
|
+
const state = lastStep?.audit_state ?? deriveAuditState(bundle);
|
|
368
|
+
const decision = decideNextStep(bundle);
|
|
369
|
+
return {
|
|
370
|
+
batchFiles,
|
|
371
|
+
bundle,
|
|
372
|
+
audit_state: state,
|
|
373
|
+
selected_obligation: lastStep?.selected_obligation ?? decision.selected_obligation,
|
|
374
|
+
selected_executor: lastStep?.selected_executor ?? "result_ingestion_executor",
|
|
375
|
+
progress_made: anyProgress,
|
|
376
|
+
artifacts_written: Array.from(artifactsWritten),
|
|
377
|
+
progress_summary: `Imported ${batchFiles.length} batch result file${batchFiles.length === 1 ? "" : "s"} from ${options.batchDir}.` +
|
|
378
|
+
(progressSummaries.length > 0
|
|
379
|
+
? `\n${progressSummaries.join("\n")}`
|
|
380
|
+
: ""),
|
|
381
|
+
next_likely_step: state.status === "complete" ? null : decision.selected_obligation,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
259
384
|
function isWorkerResult(value) {
|
|
260
385
|
return (typeof value === "object" &&
|
|
261
386
|
value !== null &&
|
|
@@ -276,7 +401,9 @@ export async function runSample() {
|
|
|
276
401
|
pass_id: "pass:security",
|
|
277
402
|
lens: "security",
|
|
278
403
|
agent_role: "security-auditor",
|
|
279
|
-
reviewed_ranges: [
|
|
404
|
+
reviewed_ranges: [
|
|
405
|
+
{ path: "src/api/auth.ts", start: 1, end: 100, line_count: 100 },
|
|
406
|
+
],
|
|
280
407
|
findings: [],
|
|
281
408
|
notes: ["Sample result ingestion path."],
|
|
282
409
|
requires_followup: false,
|
|
@@ -322,6 +449,34 @@ async function cmdAdvanceAudit(argv) {
|
|
|
322
449
|
const artifactsDir = getArtifactsDir(argv);
|
|
323
450
|
const sessionConfig = await loadSessionConfig(artifactsDir);
|
|
324
451
|
const providerName = resolveFreshSessionProviderName(getFlag(argv, "--provider"), sessionConfig);
|
|
452
|
+
const batchResultsDir = getBatchResultsDir(argv);
|
|
453
|
+
if (batchResultsDir && getFlag(argv, "--results")) {
|
|
454
|
+
throw new Error("Use either --results <file> or --batch-results <dir>, not both.");
|
|
455
|
+
}
|
|
456
|
+
if (batchResultsDir) {
|
|
457
|
+
const result = await ingestBatchAuditResults({
|
|
458
|
+
root,
|
|
459
|
+
artifactsDir,
|
|
460
|
+
batchDir: batchResultsDir,
|
|
461
|
+
});
|
|
462
|
+
await emitEnvelope({
|
|
463
|
+
root,
|
|
464
|
+
artifactsDir,
|
|
465
|
+
bundle: result.bundle,
|
|
466
|
+
audit_state: result.audit_state,
|
|
467
|
+
selected_obligation: result.selected_obligation,
|
|
468
|
+
selected_executor: result.selected_executor,
|
|
469
|
+
progress_made: result.progress_made,
|
|
470
|
+
artifacts_written: result.artifacts_written,
|
|
471
|
+
progress_summary: result.progress_summary,
|
|
472
|
+
next_likely_step: result.next_likely_step,
|
|
473
|
+
providerName,
|
|
474
|
+
});
|
|
475
|
+
if (result.audit_state.status === "complete") {
|
|
476
|
+
await cleanupIntermediateArtifacts(artifactsDir);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
325
480
|
const externalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
|
|
326
481
|
const result = await runAuditStep({
|
|
327
482
|
root,
|
|
@@ -358,10 +513,17 @@ async function cmdRunToCompletion(argv) {
|
|
|
358
513
|
const maxRuns = getMaxRuns(argv);
|
|
359
514
|
const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
|
|
360
515
|
const parallelWorkers = getParallelWorkers(argv, sessionConfig);
|
|
361
|
-
const timeoutMs = sessionConfig
|
|
516
|
+
const timeoutMs = getTimeoutMs(argv, sessionConfig);
|
|
362
517
|
const selfCliPath = resolve(process.argv[1] ?? "");
|
|
363
518
|
await mkdir(artifactsDir, { recursive: true });
|
|
364
519
|
await ensureSupervisorDirs(artifactsDir);
|
|
520
|
+
const batchResultsDir = getBatchResultsDir(argv);
|
|
521
|
+
if (batchResultsDir && getFlag(argv, "--results")) {
|
|
522
|
+
throw new Error("Use either --results <file> or --batch-results <dir>, not both.");
|
|
523
|
+
}
|
|
524
|
+
let pendingBatchAuditResults = batchResultsDir
|
|
525
|
+
? await listBatchResultFiles(batchResultsDir)
|
|
526
|
+
: [];
|
|
365
527
|
const earlyBundle = await loadArtifactBundle(artifactsDir);
|
|
366
528
|
if (!earlyBundle.unit_manifest) {
|
|
367
529
|
const foundSignal = await detectProjectRoot(root);
|
|
@@ -411,6 +573,11 @@ async function cmdRunToCompletion(argv) {
|
|
|
411
573
|
obligationId = "external_analyzer_import";
|
|
412
574
|
externalAnalyzerPath = pendingExternalAnalyzerPath;
|
|
413
575
|
}
|
|
576
|
+
else if (pendingBatchAuditResults.length > 0 && bundle.coverage_matrix) {
|
|
577
|
+
preferredExecutor = "result_ingestion_executor";
|
|
578
|
+
obligationId = "audit_results_ingested";
|
|
579
|
+
auditResultsPath = pendingBatchAuditResults[0];
|
|
580
|
+
}
|
|
414
581
|
else if (pendingAuditResultsPath && bundle.coverage_matrix) {
|
|
415
582
|
preferredExecutor = "result_ingestion_executor";
|
|
416
583
|
obligationId = "audit_results_ingested";
|
|
@@ -457,7 +624,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
457
624
|
pending_audit_tasks_path: blockPendingTasksPath,
|
|
458
625
|
};
|
|
459
626
|
const blockPrompt = renderWorkerPrompt(blockTask);
|
|
460
|
-
await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir);
|
|
627
|
+
await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir, blockPendingTasks);
|
|
461
628
|
await writeJsonFile(blockPendingTasksPath, blockPendingTasks);
|
|
462
629
|
await emitEnvelope({
|
|
463
630
|
root,
|
|
@@ -527,12 +694,12 @@ async function cmdRunToCompletion(argv) {
|
|
|
527
694
|
skip_worker_command: true,
|
|
528
695
|
};
|
|
529
696
|
const slotPrompt = renderWorkerPrompt(slotTask);
|
|
530
|
-
await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir);
|
|
697
|
+
await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir, group);
|
|
531
698
|
await writeJsonFile(slotPendingTasksPath, group);
|
|
532
699
|
workerSlots.push({ runId: slotRunId, paths: slotPaths, auditResultsPath: slotAuditResultsPath, pendingTasksPath: slotPendingTasksPath, group });
|
|
533
700
|
}
|
|
534
701
|
const parallelStartedAt = new Date().toISOString();
|
|
535
|
-
await Promise.allSettled(workerSlots.map((slot) => provider.launch({
|
|
702
|
+
const launchResults = await Promise.allSettled(workerSlots.map((slot) => provider.launch({
|
|
536
703
|
repoRoot: root,
|
|
537
704
|
runId: slot.runId,
|
|
538
705
|
obligationId,
|
|
@@ -544,21 +711,37 @@ async function cmdRunToCompletion(argv) {
|
|
|
544
711
|
uiMode,
|
|
545
712
|
timeoutMs,
|
|
546
713
|
})));
|
|
714
|
+
const launchErrorsByRunId = new Map();
|
|
715
|
+
for (let index = 0; index < launchResults.length; index++) {
|
|
716
|
+
const outcome = launchResults[index];
|
|
717
|
+
if (outcome?.status === "rejected") {
|
|
718
|
+
launchErrorsByRunId.set(workerSlots[index].runId, outcome.reason instanceof Error
|
|
719
|
+
? outcome.reason.message
|
|
720
|
+
: String(outcome.reason));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
547
723
|
// Result ingestion is intentionally sequential even though agent launch
|
|
548
724
|
// was parallel. Writing to coverage_matrix.json is not atomic, so
|
|
549
725
|
// concurrent ingest calls would race and corrupt coverage state.
|
|
550
726
|
let batchProgress = false;
|
|
727
|
+
const batchErrors = [];
|
|
551
728
|
for (const slot of workerSlots) {
|
|
552
729
|
const parallelEndedAt = new Date().toISOString();
|
|
553
730
|
let slotStatus = "no_progress";
|
|
554
731
|
try {
|
|
732
|
+
const launchError = launchErrorsByRunId.get(slot.runId);
|
|
733
|
+
if (launchError) {
|
|
734
|
+
throw new Error(`Worker launch failed: ${launchError}`);
|
|
735
|
+
}
|
|
555
736
|
const auditResults = await readJsonFile(slot.auditResultsPath);
|
|
556
737
|
const pendingTaskIds = new Set(slot.group.map((t) => t.task_id));
|
|
557
738
|
const matchedCount = auditResults.filter((r) => pendingTaskIds.has(r.task_id)).length;
|
|
558
739
|
if (slot.group.length > 0 && matchedCount === 0) {
|
|
559
740
|
throw new Error("Worker did not emit any audit results for the assigned tasks.");
|
|
560
741
|
}
|
|
561
|
-
const issues = validateAuditResults(auditResults, slot.group
|
|
742
|
+
const issues = validateAuditResults(auditResults, slot.group, {
|
|
743
|
+
lineIndex: await buildLineIndexForPaths(root, slot.group.flatMap((task) => task.file_paths)),
|
|
744
|
+
});
|
|
562
745
|
const errors = issues.filter((issue) => issue.severity === "error");
|
|
563
746
|
const warnings = issues.filter((issue) => issue.severity === "warning");
|
|
564
747
|
if (warnings.length > 0) {
|
|
@@ -582,8 +765,11 @@ async function cmdRunToCompletion(argv) {
|
|
|
582
765
|
for (const a of stepResult.artifacts_written)
|
|
583
766
|
artifactsWritten.add(a);
|
|
584
767
|
}
|
|
585
|
-
catch {
|
|
768
|
+
catch (error) {
|
|
586
769
|
slotStatus = "failed";
|
|
770
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
771
|
+
batchErrors.push(`${slot.runId}: ${message}`);
|
|
772
|
+
process.stderr.write(`[agent-batch] ${slot.runId} failed: ${message}\n`);
|
|
587
773
|
}
|
|
588
774
|
await appendRunLedgerEntry(artifactsDir, {
|
|
589
775
|
run_id: slot.runId,
|
|
@@ -597,6 +783,35 @@ async function cmdRunToCompletion(argv) {
|
|
|
597
783
|
});
|
|
598
784
|
artifactsWritten.add("run-ledger.json");
|
|
599
785
|
}
|
|
786
|
+
if (batchErrors.length > 0) {
|
|
787
|
+
const bundleAfter = await loadArtifactBundle(artifactsDir);
|
|
788
|
+
const blockedState = buildBlockedAuditState({
|
|
789
|
+
state: bundleAfter.audit_state ?? deriveAuditState(bundleAfter),
|
|
790
|
+
obligationId,
|
|
791
|
+
executor: "agent",
|
|
792
|
+
blocker: `Parallel worker batch failed for ${batchErrors.length} run(s). ` +
|
|
793
|
+
batchErrors.slice(0, 3).join(" | "),
|
|
794
|
+
});
|
|
795
|
+
await writeCoreArtifacts(artifactsDir, {
|
|
796
|
+
...bundleAfter,
|
|
797
|
+
audit_state: blockedState,
|
|
798
|
+
});
|
|
799
|
+
await emitEnvelope({
|
|
800
|
+
root,
|
|
801
|
+
artifactsDir,
|
|
802
|
+
bundle: { ...bundleAfter, audit_state: blockedState },
|
|
803
|
+
audit_state: blockedState,
|
|
804
|
+
selected_obligation: obligationId,
|
|
805
|
+
selected_executor: "agent",
|
|
806
|
+
progress_made: anyProgress,
|
|
807
|
+
artifacts_written: Array.from(new Set([...artifactsWritten, "audit_state.json"])),
|
|
808
|
+
progress_summary: `Parallel worker batch failed for ${batchErrors.length} run(s).\n` +
|
|
809
|
+
batchErrors.join("\n"),
|
|
810
|
+
next_likely_step: null,
|
|
811
|
+
providerName: provider.name,
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
600
815
|
if (!batchProgress) {
|
|
601
816
|
const bundleAfter = await loadArtifactBundle(artifactsDir);
|
|
602
817
|
const state = bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
|
|
@@ -650,7 +865,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
650
865
|
external_analyzer_results_path: externalAnalyzerPath,
|
|
651
866
|
};
|
|
652
867
|
const prompt = renderWorkerPrompt(task);
|
|
653
|
-
await writeWorkerTaskFiles(task, prompt, paths, artifactsDir);
|
|
868
|
+
await writeWorkerTaskFiles(task, prompt, paths, artifactsDir, pendingAuditTasks);
|
|
654
869
|
if (pendingAuditTasksPath && pendingAuditTasks) {
|
|
655
870
|
await writeJsonFile(pendingAuditTasksPath, pendingAuditTasks);
|
|
656
871
|
}
|
|
@@ -686,6 +901,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
686
901
|
};
|
|
687
902
|
}
|
|
688
903
|
catch (error) {
|
|
904
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
689
905
|
workerResult = {
|
|
690
906
|
contract_version: WORKER_RESULT_CONTRACT_VERSION,
|
|
691
907
|
run_id: runId,
|
|
@@ -694,9 +910,9 @@ async function cmdRunToCompletion(argv) {
|
|
|
694
910
|
progress_made: false,
|
|
695
911
|
selected_executor: preferredExecutor,
|
|
696
912
|
artifacts_written: [],
|
|
697
|
-
summary: `Worker launch failed for ${preferredExecutor}
|
|
913
|
+
summary: `Worker launch failed for ${preferredExecutor}: ${message}`,
|
|
698
914
|
next_likely_step: decision.selected_obligation,
|
|
699
|
-
errors: [
|
|
915
|
+
errors: [message],
|
|
700
916
|
};
|
|
701
917
|
await writeJsonFile(paths.resultPath, workerResult);
|
|
702
918
|
}
|
|
@@ -720,6 +936,13 @@ async function cmdRunToCompletion(argv) {
|
|
|
720
936
|
artifactsWritten.add("run-ledger.json");
|
|
721
937
|
if (externalAnalyzerPath)
|
|
722
938
|
pendingExternalAnalyzerPath = undefined;
|
|
939
|
+
if (auditResultsPath &&
|
|
940
|
+
pendingBatchAuditResults[0] === auditResultsPath &&
|
|
941
|
+
preferredExecutor === "result_ingestion_executor" &&
|
|
942
|
+
workerResult.status !== "failed" &&
|
|
943
|
+
workerResult.status !== "blocked") {
|
|
944
|
+
pendingBatchAuditResults.shift();
|
|
945
|
+
}
|
|
723
946
|
if (providerAuditResultsPath)
|
|
724
947
|
pendingAuditResultsPath = undefined;
|
|
725
948
|
if (runtimeUpdatesPath)
|
|
@@ -728,18 +951,36 @@ async function cmdRunToCompletion(argv) {
|
|
|
728
951
|
workerResult.status === "blocked" ||
|
|
729
952
|
workerResult.status === "no_progress") {
|
|
730
953
|
const bundleAfter = await loadArtifactBundle(artifactsDir);
|
|
731
|
-
const
|
|
954
|
+
const shouldBlock = workerResult.status === "failed" || workerResult.status === "blocked";
|
|
955
|
+
const state = shouldBlock
|
|
956
|
+
? buildBlockedAuditState({
|
|
957
|
+
state: bundleAfter.audit_state ?? deriveAuditState(bundleAfter),
|
|
958
|
+
obligationId: workerResult.obligation_id,
|
|
959
|
+
executor: workerResult.selected_executor,
|
|
960
|
+
blocker: buildWorkerFailureBlocker(workerResult),
|
|
961
|
+
})
|
|
962
|
+
: bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
|
|
963
|
+
if (shouldBlock) {
|
|
964
|
+
await writeCoreArtifacts(artifactsDir, {
|
|
965
|
+
...bundleAfter,
|
|
966
|
+
audit_state: state,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
732
969
|
await emitEnvelope({
|
|
733
970
|
root,
|
|
734
971
|
artifactsDir,
|
|
735
|
-
bundle:
|
|
972
|
+
bundle: shouldBlock
|
|
973
|
+
? { ...bundleAfter, audit_state: state }
|
|
974
|
+
: bundleAfter,
|
|
736
975
|
audit_state: state,
|
|
737
976
|
selected_obligation: workerResult.obligation_id,
|
|
738
977
|
selected_executor: workerResult.selected_executor,
|
|
739
978
|
progress_made: anyProgress,
|
|
740
|
-
artifacts_written: Array.from(
|
|
741
|
-
|
|
742
|
-
|
|
979
|
+
artifacts_written: Array.from(shouldBlock
|
|
980
|
+
? new Set([...artifactsWritten, "audit_state.json"])
|
|
981
|
+
: artifactsWritten),
|
|
982
|
+
progress_summary: buildWorkerFailureBlocker(workerResult),
|
|
983
|
+
next_likely_step: shouldBlock ? null : workerResult.next_likely_step,
|
|
743
984
|
providerName: provider.name,
|
|
744
985
|
});
|
|
745
986
|
return;
|
|
@@ -770,6 +1011,9 @@ async function cmdWorkerRun(argv) {
|
|
|
770
1011
|
const task = await readJsonFile(taskPath);
|
|
771
1012
|
let workerResult;
|
|
772
1013
|
try {
|
|
1014
|
+
if (looksLikeCliFlag(task.audit_results_path)) {
|
|
1015
|
+
throw new Error(`task.audit_results_path resolved to '${task.audit_results_path}', which looks like a CLI flag instead of a file path.`);
|
|
1016
|
+
}
|
|
773
1017
|
if (task.preferred_executor === "agent" && !task.audit_results_path) {
|
|
774
1018
|
throw new Error("agent worker-run requires audit_results_path so provider-assisted review can be ingested.");
|
|
775
1019
|
}
|
|
@@ -783,7 +1027,9 @@ async function cmdWorkerRun(argv) {
|
|
|
783
1027
|
if (pendingTasks.length > 0 && matchedResultCount === 0) {
|
|
784
1028
|
throw new Error("Provider-assisted review did not emit any audit results for the pending audit tasks.");
|
|
785
1029
|
}
|
|
786
|
-
const issues = validateAuditResults(auditResults, pendingTasks
|
|
1030
|
+
const issues = validateAuditResults(auditResults, pendingTasks, {
|
|
1031
|
+
lineIndex: await buildLineIndexForPaths(task.repo_root, pendingTasks.flatMap((item) => item.file_paths)),
|
|
1032
|
+
});
|
|
787
1033
|
const errors = issues.filter((issue) => issue.severity === "error");
|
|
788
1034
|
const warnings = issues.filter((issue) => issue.severity === "warning");
|
|
789
1035
|
if (warnings.length > 0) {
|
|
@@ -792,8 +1038,7 @@ async function cmdWorkerRun(argv) {
|
|
|
792
1038
|
"\n");
|
|
793
1039
|
}
|
|
794
1040
|
if (errors.length > 0) {
|
|
795
|
-
throw new Error(
|
|
796
|
-
formatAuditResultIssues(errors));
|
|
1041
|
+
throw new Error(formatAuditResultValidationError(errors));
|
|
797
1042
|
}
|
|
798
1043
|
}
|
|
799
1044
|
const preferredExecutor = task.preferred_executor === "agent"
|
|
@@ -829,7 +1074,7 @@ async function cmdWorkerRun(argv) {
|
|
|
829
1074
|
progress_made: false,
|
|
830
1075
|
selected_executor: task.preferred_executor,
|
|
831
1076
|
artifacts_written: [],
|
|
832
|
-
summary: `Worker failed for executor ${task.preferred_executor}
|
|
1077
|
+
summary: `Worker failed for executor ${task.preferred_executor}: ${error instanceof Error ? error.message : String(error)}`,
|
|
833
1078
|
next_likely_step: task.obligation_id,
|
|
834
1079
|
errors: [error instanceof Error ? error.message : String(error)],
|
|
835
1080
|
};
|
|
@@ -882,6 +1127,24 @@ async function cmdPlan(argv) {
|
|
|
882
1127
|
}
|
|
883
1128
|
async function cmdIngestResults(argv) {
|
|
884
1129
|
const artifactsDir = getArtifactsDir(argv);
|
|
1130
|
+
const batchResultsDir = getBatchResultsDir(argv);
|
|
1131
|
+
if (batchResultsDir && getFlag(argv, "--results")) {
|
|
1132
|
+
throw new Error("Use either --results <file> or --batch-results <dir>, not both.");
|
|
1133
|
+
}
|
|
1134
|
+
if (batchResultsDir) {
|
|
1135
|
+
const result = await ingestBatchAuditResults({
|
|
1136
|
+
root: getRootDir(argv),
|
|
1137
|
+
artifactsDir,
|
|
1138
|
+
batchDir: batchResultsDir,
|
|
1139
|
+
});
|
|
1140
|
+
console.log(JSON.stringify({
|
|
1141
|
+
artifacts_dir: artifactsDir,
|
|
1142
|
+
imported_files: result.batchFiles,
|
|
1143
|
+
selected_executor: result.selected_executor,
|
|
1144
|
+
progress_summary: result.progress_summary,
|
|
1145
|
+
}, null, 2));
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
885
1148
|
const result = await runAuditStep({
|
|
886
1149
|
root: getRootDir(argv),
|
|
887
1150
|
artifactsDir,
|
|
@@ -894,6 +1157,37 @@ async function cmdIngestResults(argv) {
|
|
|
894
1157
|
progress_summary: result.progress_summary,
|
|
895
1158
|
}, null, 2));
|
|
896
1159
|
}
|
|
1160
|
+
async function cmdExplainTask(argv) {
|
|
1161
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
1162
|
+
const taskId = getFlag(argv, "--task-id") ?? argv[3];
|
|
1163
|
+
if (!taskId) {
|
|
1164
|
+
throw new Error("explain-task requires <task_id> or --task-id <task_id>");
|
|
1165
|
+
}
|
|
1166
|
+
const bundle = await loadArtifactBundle(artifactsDir);
|
|
1167
|
+
const task = [...(bundle.audit_tasks ?? []), ...(bundle.requeue_tasks ?? [])].find((item) => item.task_id === taskId);
|
|
1168
|
+
if (!task) {
|
|
1169
|
+
throw new Error(`Unknown task_id '${taskId}'.`);
|
|
1170
|
+
}
|
|
1171
|
+
const coverageEntries = (bundle.coverage_matrix?.files ?? [])
|
|
1172
|
+
.filter((file) => task.file_paths.includes(file.path))
|
|
1173
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
1174
|
+
const matchingResults = (bundle.audit_results ?? []).filter((result) => result.task_id === task.task_id);
|
|
1175
|
+
console.log(JSON.stringify({
|
|
1176
|
+
artifacts_dir: artifactsDir,
|
|
1177
|
+
task_id: task.task_id,
|
|
1178
|
+
task,
|
|
1179
|
+
file_count: task.file_paths.length,
|
|
1180
|
+
coverage_entries: coverageEntries,
|
|
1181
|
+
pending_coverage: coverageEntries
|
|
1182
|
+
.map((file) => ({
|
|
1183
|
+
path: file.path,
|
|
1184
|
+
missing_lenses: file.required_lenses.filter((lens) => !file.completed_lenses.includes(lens)),
|
|
1185
|
+
}))
|
|
1186
|
+
.filter((file) => file.missing_lenses.length > 0),
|
|
1187
|
+
matching_result_count: matchingResults.length,
|
|
1188
|
+
matching_finding_ids: matchingResults.flatMap((result) => result.findings.map((finding) => finding.id)),
|
|
1189
|
+
}, null, 2));
|
|
1190
|
+
}
|
|
897
1191
|
async function cmdUpdateRuntimeValidation(argv) {
|
|
898
1192
|
const artifactsDir = getArtifactsDir(argv);
|
|
899
1193
|
const result = await runAuditStep({
|
|
@@ -990,6 +1284,9 @@ async function main(argv) {
|
|
|
990
1284
|
case "ingest-results":
|
|
991
1285
|
await cmdIngestResults(argv);
|
|
992
1286
|
return;
|
|
1287
|
+
case "explain-task":
|
|
1288
|
+
await cmdExplainTask(argv);
|
|
1289
|
+
return;
|
|
993
1290
|
case "update-runtime-validation":
|
|
994
1291
|
await cmdUpdateRuntimeValidation(argv);
|
|
995
1292
|
return;
|
|
@@ -1004,7 +1301,7 @@ async function main(argv) {
|
|
|
1004
1301
|
return;
|
|
1005
1302
|
default:
|
|
1006
1303
|
console.error(`Unknown command: ${command}`);
|
|
1007
|
-
console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, update-runtime-validation, validate, requeue, synthesize");
|
|
1304
|
+
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, requeue, synthesize");
|
|
1008
1305
|
process.exitCode = 1;
|
|
1009
1306
|
}
|
|
1010
1307
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuditTask } from "../types.js";
|
|
1
2
|
import type { WorkerTask } from "../types/workerSession.js";
|
|
2
3
|
export interface RunPaths {
|
|
3
4
|
runDir: string;
|
|
@@ -11,4 +12,4 @@ export interface RunPaths {
|
|
|
11
12
|
export declare function buildRunId(obligationId: string | null, index: number): string;
|
|
12
13
|
export declare function getRunPaths(artifactsDir: string, runId: string): RunPaths;
|
|
13
14
|
export declare function ensureSupervisorDirs(artifactsDir: string): Promise<void>;
|
|
14
|
-
export declare function writeWorkerTaskFiles(task: WorkerTask, prompt: string, paths: RunPaths, artifactsDir: string): Promise<void>;
|
|
15
|
+
export declare function writeWorkerTaskFiles(task: WorkerTask, prompt: string, paths: RunPaths, artifactsDir: string, currentTasks?: AuditTask[]): Promise<void>;
|
package/dist/io/runArtifacts.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function ensureSupervisorDirs(artifactsDir) {
|
|
|
24
24
|
await mkdir(join(artifactsDir, "worker-logs"), { recursive: true });
|
|
25
25
|
await mkdir(join(artifactsDir, "runs"), { recursive: true });
|
|
26
26
|
}
|
|
27
|
-
export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir) {
|
|
27
|
+
export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, currentTasks) {
|
|
28
28
|
await mkdir(paths.runDir, { recursive: true });
|
|
29
29
|
await writeJsonFile(paths.taskPath, task);
|
|
30
30
|
await writeFile(paths.promptPath, prompt, "utf8");
|
|
@@ -34,4 +34,5 @@ export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir) {
|
|
|
34
34
|
});
|
|
35
35
|
await writeJsonFile(join(artifactsDir, "dispatch", "current-task.json"), task);
|
|
36
36
|
await writeFile(join(artifactsDir, "dispatch", "current-prompt.md"), prompt, "utf8");
|
|
37
|
+
await writeJsonFile(join(artifactsDir, "dispatch", "current-tasks.json"), currentTasks ?? []);
|
|
37
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
2
|
-
import type { AuditTask } from "../types.js";
|
|
2
|
+
import type { AuditTask, CoverageMatrix } from "../types.js";
|
|
3
3
|
import type { FlowCoverageManifest } from "../types/flowCoverage.js";
|
|
4
4
|
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
5
|
-
export declare function buildFlowRequeueTasks(criticalFlows: CriticalFlowManifest, flowCoverage: FlowCoverageManifest, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
|
|
5
|
+
export declare function buildFlowRequeueTasks(criticalFlows: CriticalFlowManifest, flowCoverage: FlowCoverageManifest, coverageMatrix: CoverageMatrix, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
|