auditor-lambda 0.3.4 → 0.3.5
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/audit-code-wrapper-lib.mjs +9 -2
- package/dist/cli.js +359 -53
- package/dist/orchestrator/fileAnchors.d.ts +32 -0
- package/dist/orchestrator/fileAnchors.js +217 -0
- package/dist/orchestrator/reviewPackets.js +10 -0
- package/dist/providers/claudeCodeProvider.js +3 -1
- package/dist/providers/index.js +2 -1
- package/dist/supervisor/operatorHandoff.js +22 -11
- package/dist/types/sessionConfig.d.ts +1 -0
- package/dist/validation/auditResults.js +50 -2
- package/dist/validation/sessionConfig.js +5 -0
- package/docs/agent-integrations.md +4 -1
- package/docs/contract.md +3 -0
- package/docs/dispatch-implementation-plan.md +57 -24
- package/docs/run-flow.md +5 -3
- package/docs/session-config.md +11 -3
- package/docs/supervisor.md +5 -3
- package/docs/workflow-refactor-brief.md +14 -5
- package/package.json +1 -1
- package/skills/audit-code/audit-code.prompt.md +11 -6
|
@@ -260,9 +260,11 @@ function printHelp({ usageName, preferredEntrypoint }) {
|
|
|
260
260
|
'- validate checks the current artifact bundle plus session-config/provider readiness and exits non-zero when issues exist',
|
|
261
261
|
'- validate-results --results FILE validates AuditResult payloads against the active task manifest without ingesting them',
|
|
262
262
|
'- explain-task <task_id> prints the resolved file coverage and current status for a task id',
|
|
263
|
-
'- prepare-dispatch --run-id <id> [--artifacts-dir <dir>] creates
|
|
264
|
-
'-
|
|
263
|
+
'- prepare-dispatch --run-id <id> [--artifacts-dir <dir>] creates packet prompt files and a slim dispatch-plan.json for parallel subagent dispatch',
|
|
264
|
+
'- submit-packet --run-id <id> --packet-id <id> [--artifacts-dir <dir>] validates AuditResult[] from stdin and writes only backend-assigned result files',
|
|
265
|
+
'- merge-and-ingest --run-id <id> [--root <dir>] [--artifacts-dir <dir>] merges assigned packet results and ingests them into the coverage matrix',
|
|
265
266
|
'- validate-result --run-id <id> --task-id <id> [--artifacts-dir <dir>] validates a single task result against the schema and line counts',
|
|
267
|
+
' generated packet prompts may use --run-id-b64, --task-id-b64, and --artifacts-dir-b64 to avoid shell-sensitive raw ids',
|
|
266
268
|
'',
|
|
267
269
|
'Primary usage:',
|
|
268
270
|
'- from the repository root, run the wrapper with no arguments',
|
|
@@ -2281,6 +2283,11 @@ export async function runAuditCodeWrapper({
|
|
|
2281
2283
|
return;
|
|
2282
2284
|
}
|
|
2283
2285
|
|
|
2286
|
+
if (argv[0] === 'submit-packet') {
|
|
2287
|
+
await runDistCommand('submit-packet', argv.slice(1));
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2284
2291
|
if (argv[0] === 'merge-and-ingest') {
|
|
2285
2292
|
await runDistCommand('merge-and-ingest', argv.slice(1), { ensureArtifactsDir: true });
|
|
2286
2293
|
return;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { buildRepoManifest } from "./extractors/fileInventory.js";
|
|
6
8
|
import { buildFileDisposition } from "./extractors/disposition.js";
|
|
@@ -11,7 +13,7 @@ import { buildFlowCoverage } from "./orchestrator/flowCoverage.js";
|
|
|
11
13
|
import { buildRuntimeValidationTasks, } from "./orchestrator/runtimeValidation.js";
|
|
12
14
|
import { initializeCoverageFromPlan } from "./orchestrator/planning.js";
|
|
13
15
|
import { loadArtifactBundle, writeCoreArtifacts, promoteFinalAuditReport, } from "./io/artifacts.js";
|
|
14
|
-
import { readJsonFile, writeJsonFile } from "./io/json.js";
|
|
16
|
+
import { isFileMissingError, readJsonFile, writeJsonFile } from "./io/json.js";
|
|
15
17
|
import { validateArtifactBundle } from "./validation/artifacts.js";
|
|
16
18
|
import { validateAuditResults, formatAuditResultIssues, } from "./validation/auditResults.js";
|
|
17
19
|
import { prefixValidationIssues } from "./validation/basic.js";
|
|
@@ -27,11 +29,13 @@ import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from
|
|
|
27
29
|
import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
|
|
28
30
|
import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
|
|
29
31
|
import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/reviewPackets.js";
|
|
32
|
+
import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
|
|
30
33
|
import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
|
|
31
34
|
import { runAuditCodeMcpServer } from "./mcp/server.js";
|
|
32
35
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
36
|
const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
|
|
34
37
|
const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
|
|
38
|
+
const LARGE_FILE_PACKET_TARGET_LINES = 2500;
|
|
35
39
|
const DIRECT_CLI_DEFAULTS = {
|
|
36
40
|
rootDir: ".",
|
|
37
41
|
artifactsDir: ".artifacts",
|
|
@@ -65,6 +69,45 @@ function getFlag(argv, name, fallback) {
|
|
|
65
69
|
function hasFlag(argv, name) {
|
|
66
70
|
return argv.includes(name);
|
|
67
71
|
}
|
|
72
|
+
function toBase64Url(value) {
|
|
73
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
74
|
+
}
|
|
75
|
+
function fromBase64Url(value) {
|
|
76
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
77
|
+
}
|
|
78
|
+
function digestId(value) {
|
|
79
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
80
|
+
}
|
|
81
|
+
function safeArtifactStem(value) {
|
|
82
|
+
const sanitized = value
|
|
83
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "_")
|
|
84
|
+
.replace(/^_+|_+$/g, "")
|
|
85
|
+
.slice(0, 80);
|
|
86
|
+
return sanitized.length > 0 ? sanitized : "artifact";
|
|
87
|
+
}
|
|
88
|
+
function artifactNameForId(value, extension) {
|
|
89
|
+
return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
|
|
90
|
+
}
|
|
91
|
+
function taskResultPath(taskResultsDir, taskId) {
|
|
92
|
+
return join(taskResultsDir, artifactNameForId(taskId, "json"));
|
|
93
|
+
}
|
|
94
|
+
function packetPromptPath(taskResultsDir, packetId) {
|
|
95
|
+
return join(taskResultsDir, artifactNameForId(packetId, "prompt.md"));
|
|
96
|
+
}
|
|
97
|
+
async function readStdinText() {
|
|
98
|
+
if (process.stdin.isTTY) {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
return await new Promise((resolveInput, reject) => {
|
|
102
|
+
let input = "";
|
|
103
|
+
process.stdin.setEncoding("utf8");
|
|
104
|
+
process.stdin.on("data", (chunk) => {
|
|
105
|
+
input += chunk;
|
|
106
|
+
});
|
|
107
|
+
process.stdin.on("end", () => resolveInput(input));
|
|
108
|
+
process.stdin.on("error", reject);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
68
111
|
function resolveFlagPath(argv, name, fallback) {
|
|
69
112
|
return resolve(getFlag(argv, name, fallback));
|
|
70
113
|
}
|
|
@@ -1404,6 +1447,60 @@ async function cmdWorkerRun(argv) {
|
|
|
1404
1447
|
process.exitCode = 1;
|
|
1405
1448
|
}
|
|
1406
1449
|
}
|
|
1450
|
+
const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
|
|
1451
|
+
function dispatchResultMapPath(runDir) {
|
|
1452
|
+
return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
|
|
1453
|
+
}
|
|
1454
|
+
function resolveRunScopedArg(argv, rawFlag, b64Flag) {
|
|
1455
|
+
const raw = getFlag(argv, rawFlag);
|
|
1456
|
+
const encoded = getFlag(argv, b64Flag);
|
|
1457
|
+
return raw ?? (encoded ? fromBase64Url(encoded) : undefined);
|
|
1458
|
+
}
|
|
1459
|
+
async function loadDispatchResultMap(runDir) {
|
|
1460
|
+
try {
|
|
1461
|
+
return await readJsonFile(dispatchResultMapPath(runDir));
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
if (!isFileMissingError(error)) {
|
|
1465
|
+
throw error;
|
|
1466
|
+
}
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function entriesByTaskId(entries) {
|
|
1471
|
+
return new Map(entries.map((entry) => [entry.task_id, entry]));
|
|
1472
|
+
}
|
|
1473
|
+
function isIsolatedLargeFilePacket(packet) {
|
|
1474
|
+
return (packet.file_paths.length === 1 &&
|
|
1475
|
+
packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES);
|
|
1476
|
+
}
|
|
1477
|
+
function withinRoot(root, path) {
|
|
1478
|
+
const rootPath = resolve(root);
|
|
1479
|
+
const absolutePath = resolve(rootPath, path);
|
|
1480
|
+
const relativePath = relative(rootPath, absolutePath);
|
|
1481
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
1482
|
+
throw new Error(`Path '${path}' escapes repository root '${rootPath}'.`);
|
|
1483
|
+
}
|
|
1484
|
+
return absolutePath;
|
|
1485
|
+
}
|
|
1486
|
+
function renderAnchorPreview(summary, anchorPath) {
|
|
1487
|
+
const preview = summary.anchors.slice(0, 24).map((anchor) => {
|
|
1488
|
+
const location = anchor.line ? `${summary.path}:${anchor.line}` : summary.path;
|
|
1489
|
+
const detail = anchor.detail ? ` - ${anchor.detail}` : "";
|
|
1490
|
+
return `- ${location} [${anchor.kind}] ${anchor.name}${detail}`;
|
|
1491
|
+
});
|
|
1492
|
+
return [
|
|
1493
|
+
"## Large File Review Mode",
|
|
1494
|
+
"This packet is intentionally isolated because it covers one large file.",
|
|
1495
|
+
"Use targeted reads/searches within this file, guided by the mechanical anchors.",
|
|
1496
|
+
"Do not read unrelated files unless a finding cannot be evidenced without a direct boundary check.",
|
|
1497
|
+
`Anchor file: ${anchorPath}`,
|
|
1498
|
+
`Anchor counts: symbols=${summary.counts.symbols}, routes=${summary.counts.routes}, keywords=${summary.counts.keywords}, graph_edges=${summary.counts.graph_edges}, analyzer_signals=${summary.counts.analyzer_signals}, omitted=${summary.omitted_anchor_count}`,
|
|
1499
|
+
"Anchor preview:",
|
|
1500
|
+
...(preview.length > 0 ? preview : ["- no anchors extracted beyond file boundaries"]),
|
|
1501
|
+
"",
|
|
1502
|
+
];
|
|
1503
|
+
}
|
|
1407
1504
|
async function cmdPrepareDispatch(argv) {
|
|
1408
1505
|
const runId = getFlag(argv, "--run-id");
|
|
1409
1506
|
if (!runId)
|
|
@@ -1413,6 +1510,17 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1413
1510
|
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
1414
1511
|
const taskResultsDir = join(runDir, "task-results");
|
|
1415
1512
|
const dispatchPlanPath = join(runDir, "dispatch-plan.json");
|
|
1513
|
+
const explicitRoot = getFlag(argv, "--root") ? getRootDir(argv) : undefined;
|
|
1514
|
+
let reviewRoot = explicitRoot;
|
|
1515
|
+
try {
|
|
1516
|
+
const workerTask = await readJsonFile(join(runDir, "task.json"));
|
|
1517
|
+
reviewRoot ??= workerTask.repo_root;
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
if (!isFileMissingError(error)) {
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1416
1524
|
const tasks = await readJsonFile(tasksPath);
|
|
1417
1525
|
const bundle = await loadArtifactBundle(artifactsDir);
|
|
1418
1526
|
const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
|
|
@@ -1428,18 +1536,22 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1428
1536
|
lineIndex,
|
|
1429
1537
|
});
|
|
1430
1538
|
const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
|
|
1431
|
-
const
|
|
1539
|
+
const resultPathByTaskId = new Map(orderedTasks.map((task) => [
|
|
1432
1540
|
task.task_id,
|
|
1433
|
-
|
|
1541
|
+
taskResultPath(taskResultsDir, task.task_id),
|
|
1434
1542
|
]));
|
|
1543
|
+
const resultPathSet = new Set(resultPathByTaskId.values());
|
|
1544
|
+
if (resultPathSet.size !== resultPathByTaskId.size) {
|
|
1545
|
+
throw new Error("prepare-dispatch generated duplicate result paths; task ids must be uniquely addressable.");
|
|
1546
|
+
}
|
|
1435
1547
|
const plan = [];
|
|
1548
|
+
const resultMapEntries = [];
|
|
1436
1549
|
let largestPacketId = null;
|
|
1437
1550
|
let largestLines = 0;
|
|
1438
1551
|
let largestEstimatedTokens = 0;
|
|
1439
1552
|
const warnings = [];
|
|
1440
1553
|
for (const packet of packets) {
|
|
1441
|
-
const
|
|
1442
|
-
const promptPath = join(taskResultsDir, `${sanitized}.prompt.md`);
|
|
1554
|
+
const promptPath = packetPromptPath(taskResultsDir, packet.packet_id);
|
|
1443
1555
|
const packetTasks = packet.task_ids
|
|
1444
1556
|
.map((taskId) => tasksById.get(taskId))
|
|
1445
1557
|
.filter((task) => task !== undefined);
|
|
@@ -1448,7 +1560,8 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1448
1560
|
largestEstimatedTokens = packet.estimated_tokens;
|
|
1449
1561
|
largestPacketId = packet.packet_id;
|
|
1450
1562
|
}
|
|
1451
|
-
|
|
1563
|
+
const largeFileMode = isIsolatedLargeFilePacket(packet);
|
|
1564
|
+
if (packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES && !largeFileMode) {
|
|
1452
1565
|
warnings.push({
|
|
1453
1566
|
code: "large_packet",
|
|
1454
1567
|
message: `large packet ${packet.packet_id} (~${packet.total_lines} lines) may hit quota limits`,
|
|
@@ -1466,15 +1579,57 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1466
1579
|
const lines = packet.file_line_counts[path] ?? 0;
|
|
1467
1580
|
return `- ${path} (${lines} lines)`;
|
|
1468
1581
|
}).join("\n");
|
|
1582
|
+
let anchorPath = null;
|
|
1583
|
+
let anchorSummary = null;
|
|
1584
|
+
if (largeFileMode) {
|
|
1585
|
+
const filePath = packet.file_paths[0];
|
|
1586
|
+
if (!reviewRoot) {
|
|
1587
|
+
warnings.push({
|
|
1588
|
+
code: "large_file_anchor_unavailable",
|
|
1589
|
+
message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
try {
|
|
1594
|
+
const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
|
|
1595
|
+
const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
|
|
1596
|
+
anchorSummary = buildFileAnchorSummary({
|
|
1597
|
+
path: filePath,
|
|
1598
|
+
content,
|
|
1599
|
+
totalLines,
|
|
1600
|
+
graphBundle: bundle.graph_bundle,
|
|
1601
|
+
externalAnalyzerResults: bundle.external_analyzer_results,
|
|
1602
|
+
});
|
|
1603
|
+
anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
|
|
1604
|
+
await writeJsonFile(anchorPath, anchorSummary);
|
|
1605
|
+
}
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
warnings.push({
|
|
1608
|
+
code: "large_file_anchor_failed",
|
|
1609
|
+
message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
|
|
1610
|
+
(error instanceof Error ? error.message : String(error)),
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
const largeFileSection = anchorSummary && anchorPath
|
|
1616
|
+
? renderAnchorPreview(anchorSummary, anchorPath)
|
|
1617
|
+
: largeFileMode
|
|
1618
|
+
? [
|
|
1619
|
+
"## Large File Review Mode",
|
|
1620
|
+
"This packet is intentionally isolated because it covers one large file.",
|
|
1621
|
+
"Use targeted reads/searches within this file only.",
|
|
1622
|
+
"No mechanical anchor file was available, so rely on targeted symbol and keyword searches before reading broad ranges.",
|
|
1623
|
+
"",
|
|
1624
|
+
]
|
|
1625
|
+
: [];
|
|
1469
1626
|
const taskSections = packetTasks.flatMap((task) => {
|
|
1470
1627
|
const lensDef = lensDefs[task.lens];
|
|
1471
|
-
const outputPath = outputPathByTaskId.get(task.task_id);
|
|
1472
1628
|
return [
|
|
1473
1629
|
`### ${task.task_id}`,
|
|
1474
1630
|
`unit_id: ${task.unit_id}`,
|
|
1475
1631
|
`pass_id: ${task.pass_id}`,
|
|
1476
1632
|
`lens: ${task.lens}`,
|
|
1477
|
-
`output_path: ${outputPath}`,
|
|
1478
1633
|
`rationale: ${task.rationale}`,
|
|
1479
1634
|
"",
|
|
1480
1635
|
`Lens guidance: ${lensDef?.description ?? task.lens}`,
|
|
@@ -1482,13 +1637,19 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1482
1637
|
"",
|
|
1483
1638
|
];
|
|
1484
1639
|
});
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1640
|
+
const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
|
|
1641
|
+
`--run-id-b64 ${toBase64Url(runId)} ` +
|
|
1642
|
+
`--packet-id-b64 ${toBase64Url(packet.packet_id)} ` +
|
|
1643
|
+
`--artifacts-dir-b64 ${toBase64Url(artifactsDir)}`;
|
|
1644
|
+
for (const task of packetTasks) {
|
|
1645
|
+
resultMapEntries.push({
|
|
1646
|
+
packet_id: packet.packet_id,
|
|
1647
|
+
task_id: task.task_id,
|
|
1648
|
+
result_path: resultPathByTaskId.get(task.task_id),
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1490
1651
|
const prompt = [
|
|
1491
|
-
"You are a code auditor. Review this packet once, then
|
|
1652
|
+
"You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
|
|
1492
1653
|
"",
|
|
1493
1654
|
"## Packet",
|
|
1494
1655
|
`packet_id: ${packet.packet_id}`,
|
|
@@ -1497,15 +1658,18 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1497
1658
|
`estimated_tokens: ${packet.estimated_tokens}`,
|
|
1498
1659
|
"",
|
|
1499
1660
|
"## Files to read",
|
|
1500
|
-
|
|
1661
|
+
largeFileMode
|
|
1662
|
+
? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
|
|
1663
|
+
: "Use your Read tool. Paths are repo-relative from the current working directory.",
|
|
1501
1664
|
fileList,
|
|
1502
1665
|
"",
|
|
1666
|
+
...largeFileSection,
|
|
1503
1667
|
"## Tasks",
|
|
1504
1668
|
...taskSections,
|
|
1505
1669
|
"## Output",
|
|
1506
|
-
"
|
|
1507
|
-
"Do not combine the task results into one file. Do not edit source files,",
|
|
1670
|
+
"Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
|
|
1508
1671
|
"remediate findings, create extra task results, or run unrelated audits.",
|
|
1672
|
+
"Produce one JSON array containing exactly one AuditResult object for each listed task.",
|
|
1509
1673
|
"",
|
|
1510
1674
|
"Required AuditResult fields:",
|
|
1511
1675
|
" task_id copy from the task metadata",
|
|
@@ -1532,30 +1696,30 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1532
1696
|
"3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
|
|
1533
1697
|
"4. findings: [] is correct when you find nothing genuine.",
|
|
1534
1698
|
"",
|
|
1535
|
-
"##
|
|
1536
|
-
"
|
|
1537
|
-
|
|
1699
|
+
"## Submit",
|
|
1700
|
+
"Pipe the JSON array on stdin to this command:",
|
|
1701
|
+
` ${submitCommand}`,
|
|
1538
1702
|
"",
|
|
1539
|
-
"
|
|
1703
|
+
"The command validates and writes the packet-owned result files. Exit 0 means accepted.",
|
|
1704
|
+
"Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
|
|
1540
1705
|
"",
|
|
1541
1706
|
"## Final response",
|
|
1542
|
-
`After
|
|
1707
|
+
`After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
|
|
1543
1708
|
].join("\n");
|
|
1544
1709
|
await writeFile(promptPath, prompt, "utf8");
|
|
1545
1710
|
plan.push({
|
|
1546
1711
|
packet_id: packet.packet_id,
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)`,
|
|
1550
|
-
output_paths: outputPaths,
|
|
1712
|
+
description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)` +
|
|
1713
|
+
(largeFileMode ? " [isolated large-file mode]" : ""),
|
|
1551
1714
|
prompt_path: promptPath,
|
|
1552
|
-
lenses: packet.lenses,
|
|
1553
|
-
file_paths: packet.file_paths,
|
|
1554
|
-
total_lines: packet.total_lines,
|
|
1555
|
-
estimated_tokens: packet.estimated_tokens,
|
|
1556
1715
|
});
|
|
1557
1716
|
}
|
|
1558
1717
|
await writeJsonFile(dispatchPlanPath, plan);
|
|
1718
|
+
await writeJsonFile(dispatchResultMapPath(runDir), {
|
|
1719
|
+
contract_version: "audit-code-dispatch-results/v1alpha1",
|
|
1720
|
+
run_id: runId,
|
|
1721
|
+
entries: resultMapEntries,
|
|
1722
|
+
});
|
|
1559
1723
|
const warningsPath = warnings.length > 0
|
|
1560
1724
|
? join(runDir, "dispatch-warnings.json")
|
|
1561
1725
|
: null;
|
|
@@ -1578,6 +1742,106 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1578
1742
|
dispatch_warnings_path: warningsPath,
|
|
1579
1743
|
}, null, 2));
|
|
1580
1744
|
}
|
|
1745
|
+
async function cmdSubmitPacket(argv) {
|
|
1746
|
+
const runId = resolveRunScopedArg(argv, "--run-id", "--run-id-b64");
|
|
1747
|
+
const packetId = resolveRunScopedArg(argv, "--packet-id", "--packet-id-b64");
|
|
1748
|
+
const artifactsDirB64 = getFlag(argv, "--artifacts-dir-b64");
|
|
1749
|
+
const artifactsDir = artifactsDirB64
|
|
1750
|
+
? resolve(fromBase64Url(artifactsDirB64))
|
|
1751
|
+
: getArtifactsDir(argv);
|
|
1752
|
+
if (!runId || !packetId) {
|
|
1753
|
+
throw new Error("submit-packet requires --run-id and --packet-id (or --run-id-b64/--packet-id-b64)");
|
|
1754
|
+
}
|
|
1755
|
+
const runDir = join(artifactsDir, "runs", runId);
|
|
1756
|
+
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
1757
|
+
const resultMap = await loadDispatchResultMap(runDir);
|
|
1758
|
+
if (!resultMap) {
|
|
1759
|
+
throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
|
|
1760
|
+
}
|
|
1761
|
+
const packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
|
|
1762
|
+
if (packetEntries.length === 0) {
|
|
1763
|
+
throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.`);
|
|
1764
|
+
}
|
|
1765
|
+
if (entriesByTaskId(packetEntries).size !== packetEntries.length) {
|
|
1766
|
+
throw new Error(`Dispatch result map has duplicate task entries for packet '${packetId}'.`);
|
|
1767
|
+
}
|
|
1768
|
+
const allTasks = await readJsonFile(tasksPath);
|
|
1769
|
+
const taskById = new Map(allTasks.map((task) => [task.task_id, task]));
|
|
1770
|
+
const packetTasks = packetEntries.map((entry) => taskById.get(entry.task_id));
|
|
1771
|
+
const missingTask = packetEntries.find((entry, index) => !packetTasks[index]);
|
|
1772
|
+
if (missingTask) {
|
|
1773
|
+
throw new Error(`Dispatch result map references unknown task '${missingTask.task_id}'.`);
|
|
1774
|
+
}
|
|
1775
|
+
const tasks = packetTasks;
|
|
1776
|
+
const expectedTaskIds = new Set(tasks.map((task) => task.task_id));
|
|
1777
|
+
const lineIndex = Object.fromEntries(tasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
|
|
1778
|
+
const encodedResults = getFlag(argv, "--results-b64");
|
|
1779
|
+
const raw = encodedResults ? fromBase64Url(encodedResults) : await readStdinText();
|
|
1780
|
+
if (raw.trim().length === 0) {
|
|
1781
|
+
throw new Error("submit-packet requires an AuditResult[] JSON payload on stdin or --results-b64.");
|
|
1782
|
+
}
|
|
1783
|
+
let payload;
|
|
1784
|
+
try {
|
|
1785
|
+
payload = JSON.parse(raw);
|
|
1786
|
+
}
|
|
1787
|
+
catch (error) {
|
|
1788
|
+
throw new Error(`Invalid submit-packet JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1789
|
+
}
|
|
1790
|
+
const resultErrors = [];
|
|
1791
|
+
const issues = validateAuditResults(payload, tasks, { lineIndex });
|
|
1792
|
+
const validationErrors = issues.filter((issue) => issue.severity === "error");
|
|
1793
|
+
const validationWarnings = issues.filter((issue) => issue.severity === "warning");
|
|
1794
|
+
if (validationWarnings.length > 0) {
|
|
1795
|
+
process.stderr.write(`audit-results validation: ${validationWarnings.length} warning(s):\n` +
|
|
1796
|
+
formatAuditResultIssues(validationWarnings) +
|
|
1797
|
+
"\n");
|
|
1798
|
+
}
|
|
1799
|
+
if (validationErrors.length > 0) {
|
|
1800
|
+
resultErrors.push(formatAuditResultIssues(validationErrors));
|
|
1801
|
+
}
|
|
1802
|
+
if (Array.isArray(payload)) {
|
|
1803
|
+
const seen = new Set();
|
|
1804
|
+
for (const [index, result] of payload.entries()) {
|
|
1805
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
const taskId = result.task_id;
|
|
1809
|
+
if (typeof taskId !== "string" || taskId.trim().length === 0) {
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (seen.has(taskId)) {
|
|
1813
|
+
resultErrors.push(`Duplicate audit result for assigned task '${taskId}'.`);
|
|
1814
|
+
}
|
|
1815
|
+
seen.add(taskId);
|
|
1816
|
+
if (!expectedTaskIds.has(taskId)) {
|
|
1817
|
+
resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${packetId}'.`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
for (const task of tasks) {
|
|
1821
|
+
if (!seen.has(task.task_id)) {
|
|
1822
|
+
resultErrors.push(`Missing audit result for assigned task '${task.task_id}'.`);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (resultErrors.length > 0) {
|
|
1827
|
+
throw new Error(`submit-packet rejected ${packetId}:\n${resultErrors.join("\n")}`);
|
|
1828
|
+
}
|
|
1829
|
+
const entryByTaskId = entriesByTaskId(packetEntries);
|
|
1830
|
+
for (const result of payload) {
|
|
1831
|
+
const entry = entryByTaskId.get(result.task_id);
|
|
1832
|
+
if (!entry) {
|
|
1833
|
+
throw new Error(`Internal error: no result path for accepted task '${result.task_id}'.`);
|
|
1834
|
+
}
|
|
1835
|
+
await writeJsonFile(entry.result_path, result);
|
|
1836
|
+
}
|
|
1837
|
+
const findingCount = payload.reduce((sum, result) => sum + result.findings.length, 0);
|
|
1838
|
+
console.log(JSON.stringify({
|
|
1839
|
+
run_id: runId,
|
|
1840
|
+
packet_id: packetId,
|
|
1841
|
+
accepted_count: payload.length,
|
|
1842
|
+
finding_count: findingCount,
|
|
1843
|
+
}, null, 2));
|
|
1844
|
+
}
|
|
1581
1845
|
async function cmdMergeAndIngest(argv) {
|
|
1582
1846
|
const runId = getFlag(argv, "--run-id");
|
|
1583
1847
|
if (!runId)
|
|
@@ -1589,12 +1853,20 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1589
1853
|
const taskPath = join(runDir, "task.json");
|
|
1590
1854
|
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
1591
1855
|
const workerTask = await readJsonFile(taskPath);
|
|
1856
|
+
const resultMap = await loadDispatchResultMap(runDir);
|
|
1857
|
+
if (!resultMap) {
|
|
1858
|
+
throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
|
|
1859
|
+
}
|
|
1592
1860
|
let allTasks = [];
|
|
1593
1861
|
try {
|
|
1594
1862
|
allTasks = await readJsonFile(tasksPath);
|
|
1595
1863
|
}
|
|
1596
1864
|
catch { /* may not exist */ }
|
|
1597
|
-
const
|
|
1865
|
+
const entryByTaskId = entriesByTaskId(resultMap.entries);
|
|
1866
|
+
if (entryByTaskId.size !== resultMap.entries.length) {
|
|
1867
|
+
throw new Error(`Dispatch result map for run ${runId} contains duplicate task entries.`);
|
|
1868
|
+
}
|
|
1869
|
+
const expectedPaths = new Set(resultMap.entries.map((entry) => resolve(entry.result_path)));
|
|
1598
1870
|
let files;
|
|
1599
1871
|
try {
|
|
1600
1872
|
files = (await readdir(taskResultsDir)).filter(f => f.endsWith(".json")).sort();
|
|
@@ -1606,13 +1878,38 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1606
1878
|
const failing = [];
|
|
1607
1879
|
const seenTaskIds = new Set();
|
|
1608
1880
|
for (const filename of files) {
|
|
1609
|
-
const filePath = join(taskResultsDir, filename);
|
|
1881
|
+
const filePath = resolve(join(taskResultsDir, filename));
|
|
1882
|
+
if (!expectedPaths.has(filePath)) {
|
|
1883
|
+
failing.push({
|
|
1884
|
+
task_id: filename,
|
|
1885
|
+
errors: ["Unexpected task result file; only backend-assigned result paths may be ingested."],
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
for (const task of allTasks) {
|
|
1890
|
+
const entry = entryByTaskId.get(task.task_id);
|
|
1891
|
+
if (!entry) {
|
|
1892
|
+
failing.push({
|
|
1893
|
+
task_id: task.task_id,
|
|
1894
|
+
errors: ["Missing dispatch result-map entry for assigned task."],
|
|
1895
|
+
});
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
const filePath = entry.result_path;
|
|
1610
1899
|
let obj;
|
|
1611
1900
|
try {
|
|
1612
1901
|
obj = JSON.parse(await readFile(filePath, "utf8"));
|
|
1613
1902
|
}
|
|
1614
1903
|
catch (e) {
|
|
1615
|
-
|
|
1904
|
+
if (isFileMissingError(e)) {
|
|
1905
|
+
failing.push({
|
|
1906
|
+
task_id: task.task_id,
|
|
1907
|
+
errors: ["Missing audit result for assigned task."],
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
else {
|
|
1911
|
+
failing.push({ task_id: task.task_id, errors: [`Invalid JSON: ${e.message}`] });
|
|
1912
|
+
}
|
|
1616
1913
|
continue;
|
|
1617
1914
|
}
|
|
1618
1915
|
const record = obj && typeof obj === "object" && !Array.isArray(obj)
|
|
@@ -1628,8 +1925,11 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1628
1925
|
else {
|
|
1629
1926
|
seenTaskIds.add(taskId);
|
|
1630
1927
|
}
|
|
1928
|
+
if (taskId !== task.task_id) {
|
|
1929
|
+
resultErrors.push(`Result file is assigned to '${task.task_id}' but contains task_id '${taskId}'.`);
|
|
1930
|
+
}
|
|
1631
1931
|
}
|
|
1632
|
-
const issues = validateAuditResults([obj],
|
|
1932
|
+
const issues = validateAuditResults([obj], [task], { lineIndex: task.file_line_counts ?? {} });
|
|
1633
1933
|
resultErrors.push(...issues
|
|
1634
1934
|
.filter(i => i.severity === "error")
|
|
1635
1935
|
.map(i => i.message));
|
|
@@ -1637,15 +1937,7 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1637
1937
|
passing.push(obj);
|
|
1638
1938
|
}
|
|
1639
1939
|
else {
|
|
1640
|
-
failing.push({ task_id: taskId ??
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
for (const task of allTasks) {
|
|
1644
|
-
if (!seenTaskIds.has(task.task_id)) {
|
|
1645
|
-
failing.push({
|
|
1646
|
-
task_id: task.task_id,
|
|
1647
|
-
errors: ["Missing audit result for assigned task."],
|
|
1648
|
-
});
|
|
1940
|
+
failing.push({ task_id: taskId ?? task.task_id, errors: resultErrors });
|
|
1649
1941
|
}
|
|
1650
1942
|
}
|
|
1651
1943
|
await writeJsonFile(auditResultsPath, passing);
|
|
@@ -1687,14 +1979,25 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1687
1979
|
}, null, 2));
|
|
1688
1980
|
}
|
|
1689
1981
|
async function cmdValidateResult(argv) {
|
|
1690
|
-
const
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
const
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1982
|
+
const rawRunId = getFlag(argv, "--run-id");
|
|
1983
|
+
const runIdB64 = getFlag(argv, "--run-id-b64");
|
|
1984
|
+
const rawTaskId = getFlag(argv, "--task-id");
|
|
1985
|
+
const artifactsDirB64 = getFlag(argv, "--artifacts-dir-b64");
|
|
1986
|
+
const runId = rawRunId ?? (runIdB64 ? fromBase64Url(runIdB64) : undefined);
|
|
1987
|
+
const taskIdB64 = getFlag(argv, "--task-id-b64");
|
|
1988
|
+
const taskId = rawTaskId ?? (taskIdB64 ? fromBase64Url(taskIdB64) : undefined);
|
|
1989
|
+
const artifactsDir = artifactsDirB64
|
|
1990
|
+
? resolve(fromBase64Url(artifactsDirB64))
|
|
1991
|
+
: getArtifactsDir(argv);
|
|
1992
|
+
if (!runId || !taskId) {
|
|
1993
|
+
throw new Error("validate-result requires --run-id and --task-id (or --run-id-b64/--task-id-b64)");
|
|
1994
|
+
}
|
|
1995
|
+
const runDir = join(artifactsDir, "runs", runId);
|
|
1996
|
+
const taskResultsDir = join(runDir, "task-results");
|
|
1997
|
+
const resultMap = await loadDispatchResultMap(runDir);
|
|
1998
|
+
const resultPath = resultMap?.entries.find((entry) => entry.task_id === taskId)?.result_path ??
|
|
1999
|
+
taskResultPath(taskResultsDir, taskId);
|
|
2000
|
+
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
1698
2001
|
let raw;
|
|
1699
2002
|
try {
|
|
1700
2003
|
raw = await readFile(resultPath, "utf8");
|
|
@@ -1986,12 +2289,15 @@ async function main(argv) {
|
|
|
1986
2289
|
case "merge-and-ingest":
|
|
1987
2290
|
await cmdMergeAndIngest(argv);
|
|
1988
2291
|
return;
|
|
2292
|
+
case "submit-packet":
|
|
2293
|
+
await cmdSubmitPacket(argv);
|
|
2294
|
+
return;
|
|
1989
2295
|
case "validate-result":
|
|
1990
2296
|
await cmdValidateResult(argv);
|
|
1991
2297
|
return;
|
|
1992
2298
|
default:
|
|
1993
2299
|
console.error(`Unknown command: ${command}`);
|
|
1994
|
-
console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, mcp, prepare-dispatch, merge-and-ingest, validate-result");
|
|
2300
|
+
console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result");
|
|
1995
2301
|
process.exitCode = 1;
|
|
1996
2302
|
}
|
|
1997
2303
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
2
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
3
|
+
export type FileAnchorKind = "boundary" | "import" | "export" | "symbol" | "route" | "keyword" | "graph" | "analyzer_signal";
|
|
4
|
+
export interface FileAnchor {
|
|
5
|
+
kind: FileAnchorKind;
|
|
6
|
+
name: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
detail?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FileAnchorSummary {
|
|
11
|
+
contract_version: "audit-code-file-anchors/v1alpha1";
|
|
12
|
+
path: string;
|
|
13
|
+
total_lines: number;
|
|
14
|
+
review_mode: "isolated_large_file";
|
|
15
|
+
scope_basis: string[];
|
|
16
|
+
anchors: FileAnchor[];
|
|
17
|
+
omitted_anchor_count: number;
|
|
18
|
+
counts: {
|
|
19
|
+
symbols: number;
|
|
20
|
+
routes: number;
|
|
21
|
+
keywords: number;
|
|
22
|
+
graph_edges: number;
|
|
23
|
+
analyzer_signals: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function buildFileAnchorSummary(params: {
|
|
27
|
+
path: string;
|
|
28
|
+
content: string;
|
|
29
|
+
totalLines: number;
|
|
30
|
+
graphBundle?: GraphBundle;
|
|
31
|
+
externalAnalyzerResults?: ExternalAnalyzerResults;
|
|
32
|
+
}): FileAnchorSummary;
|