auditor-lambda 0.2.18 → 0.3.0
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/dispatch/merge-results.mjs +9 -5
- package/dispatch/prepare-dispatch.mjs +75 -94
- package/dispatch/validate-result.mjs +19 -15
- package/dist/cli.js +293 -4
- package/dist/prompts/renderWorkerPrompt.js +14 -46
- package/dist/providers/index.js +8 -0
- package/dist/supervisor/operatorHandoff.d.ts +2 -0
- package/dist/supervisor/operatorHandoff.js +14 -3
- package/package.json +1 -1
- package/schemas/audit-code-v1alpha1.schema.json +9 -0
- package/skills/audit-code/audit-code.prompt.md +15 -21
|
@@ -5,17 +5,21 @@ import { validateResult } from "./validate.mjs";
|
|
|
5
5
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
-
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
9
8
|
|
|
10
9
|
// Parse --run-id
|
|
11
10
|
const runIdIdx = process.argv.indexOf("--run-id");
|
|
12
11
|
if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
|
|
13
|
-
console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id>");
|
|
12
|
+
console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id> [--artifacts-dir <dir>]");
|
|
14
13
|
process.exit(1);
|
|
15
14
|
}
|
|
16
15
|
const run_id = process.argv[runIdIdx + 1];
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
// Parse --artifacts-dir (default: CWD/.audit-artifacts)
|
|
18
|
+
const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
|
|
19
|
+
const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
|
|
20
|
+
? resolve(process.argv[artifactsDirIdx + 1])
|
|
21
|
+
: join(process.cwd(), ".audit-artifacts");
|
|
22
|
+
|
|
19
23
|
const taskResultsDir = join(artifactsDir, "runs", run_id, "task-results");
|
|
20
24
|
const auditResultsPath = join(artifactsDir, "runs", run_id, "audit-results.json");
|
|
21
25
|
const failedTasksPath = join(artifactsDir, "runs", run_id, "failed-tasks.json");
|
|
@@ -69,9 +73,9 @@ writeFileSync(auditResultsPath, JSON.stringify(passing, null, 2));
|
|
|
69
73
|
|
|
70
74
|
if (failing.length > 0) {
|
|
71
75
|
writeFileSync(failedTasksPath, JSON.stringify(failing, null, 2));
|
|
72
|
-
|
|
76
|
+
process.stderr.write(`${failing.length} task(s) failed validation and were excluded:\n`);
|
|
73
77
|
for (const f of failing) {
|
|
74
|
-
|
|
78
|
+
process.stderr.write(` ✗ ${f.task_id}: ${f.errors[0]}\n`);
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -1,143 +1,131 @@
|
|
|
1
1
|
import { dirname, resolve, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
|
-
const
|
|
7
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
8
8
|
|
|
9
9
|
// Parse --run-id
|
|
10
10
|
const runIdIdx = process.argv.indexOf("--run-id");
|
|
11
11
|
if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
|
|
12
|
-
console.error("Usage: node dispatch/prepare-dispatch.mjs --run-id <run_id>");
|
|
12
|
+
console.error("Usage: node dispatch/prepare-dispatch.mjs --run-id <run_id> [--artifacts-dir <dir>]");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
const run_id = process.argv[runIdIdx + 1];
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Parse --artifacts-dir (default: CWD/.audit-artifacts)
|
|
18
|
+
const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
|
|
19
|
+
const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
|
|
20
|
+
? resolve(process.argv[artifactsDirIdx + 1])
|
|
21
|
+
: join(process.cwd(), ".audit-artifacts");
|
|
22
|
+
|
|
18
23
|
const runDir = join(artifactsDir, "runs", run_id);
|
|
19
24
|
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
25
|
+
const taskResultsDir = join(runDir, "task-results");
|
|
20
26
|
const dispatchPlanPath = join(runDir, "dispatch-plan.json");
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
let tasks;
|
|
29
|
+
try {
|
|
30
|
+
tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(`Cannot read ${tasksPath}: ${e.message}`);
|
|
24
33
|
process.exit(1);
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
|
|
28
36
|
const lensDefinitions = JSON.parse(
|
|
29
37
|
readFileSync(join(__dirname, "lens-definitions.json"), "utf8")
|
|
30
38
|
);
|
|
31
|
-
const auditResultSchema = JSON.parse(
|
|
32
|
-
readFileSync(join(PROJECT_ROOT, "schemas", "audit_result.schema.json"), "utf8")
|
|
33
|
-
);
|
|
34
|
-
const findingSchema = JSON.parse(
|
|
35
|
-
readFileSync(join(PROJECT_ROOT, "schemas", "finding.schema.json"), "utf8")
|
|
36
|
-
);
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
const fallback = {
|
|
40
|
-
task_id: task.task_id,
|
|
41
|
-
unit_id: task.unit_id,
|
|
42
|
-
pass_id: task.pass_id,
|
|
43
|
-
lens: task.lens,
|
|
44
|
-
file_coverage: task.file_paths.map(p => ({ path: p, total_lines: task.file_line_counts[p] })),
|
|
45
|
-
findings: [],
|
|
46
|
-
notes: ["Validation failed after 3 attempts — empty result written as fallback."]
|
|
47
|
-
};
|
|
40
|
+
mkdirSync(taskResultsDir, { recursive: true });
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
function buildPrompt(task, lensDef, outputPath, runId, artifactsDir) {
|
|
43
|
+
const fileList = task.file_paths.map(p => {
|
|
44
|
+
const lines = task.file_line_counts?.[p] ?? 0;
|
|
45
|
+
return `- ${p} (${lines} lines)`;
|
|
46
|
+
}).join("\n");
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
${JSON.stringify(task, null, 2)}
|
|
48
|
+
return `You are a code auditor. Review the files below under the specified lens.
|
|
53
49
|
|
|
54
|
-
##
|
|
55
|
-
|
|
50
|
+
## Task
|
|
51
|
+
task_id: ${task.task_id}
|
|
52
|
+
unit_id: ${task.unit_id}
|
|
53
|
+
pass_id: ${task.pass_id}
|
|
54
|
+
lens: ${task.lens}
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
## Files to read
|
|
57
|
+
Use your Read tool. Paths are repo-relative from the current working directory.
|
|
58
|
+
${fileList}
|
|
58
59
|
|
|
59
60
|
## Lens: ${task.lens}
|
|
60
|
-
${lensDef.
|
|
61
|
-
|
|
62
|
-
Do NOT report: ${lensDef
|
|
63
|
-
|
|
64
|
-
## Output
|
|
65
|
-
Write
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
61
|
+
${lensDef?.description ?? task.lens}
|
|
62
|
+
|
|
63
|
+
Do NOT report: ${lensDef?.do_not_report ?? "N/A"}
|
|
64
|
+
|
|
65
|
+
## Output
|
|
66
|
+
Write a single JSON object to: ${outputPath}
|
|
67
|
+
|
|
68
|
+
Required fields:
|
|
69
|
+
task_id copy from task metadata above
|
|
70
|
+
unit_id copy from task metadata above
|
|
71
|
+
pass_id copy from task metadata above
|
|
72
|
+
lens copy from task metadata above
|
|
73
|
+
file_coverage [{path, total_lines}] — one entry per file; use the line counts listed above
|
|
74
|
+
findings [] or array of finding objects (see below)
|
|
75
|
+
|
|
76
|
+
Each finding object (omit optional fields if not applicable):
|
|
77
|
+
id unique ID, e.g. "SEC-001"
|
|
78
|
+
title short title
|
|
79
|
+
category correctness|architecture|maintainability|security|reliability|performance|data_integrity|tests|operability|config_deployment
|
|
80
|
+
severity critical|high|medium|low|info
|
|
81
|
+
confidence high|medium|low
|
|
82
|
+
lens "${task.lens}" — must match task lens exactly
|
|
83
|
+
summary 1–2 sentence description
|
|
84
|
+
affected_files [{path, line_start?, line_end?, symbol?}] — objects, not strings; min 1 entry
|
|
85
|
+
evidence ["path/to/file.ts:42 — description of what you see there"] — min 1 entry
|
|
86
|
+
|
|
87
|
+
Constraints:
|
|
88
|
+
1. line_end must not exceed the file's actual line count (use the counts listed above)
|
|
89
|
+
2. affected_files entries are OBJECTS with a "path" key — NOT plain strings
|
|
90
|
+
3. Only reference files from the list above
|
|
91
|
+
4. findings: [] is correct when you find nothing genuine — do not invent findings
|
|
92
|
+
|
|
93
|
+
## Validate
|
|
91
94
|
After writing your result, run:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- If it exits 0: you are done. Stop.
|
|
95
|
-
- If it exits non-zero: read the error output, fix the JSON, rewrite the file, run again.
|
|
96
|
-
- Repeat up to 3 times.
|
|
95
|
+
audit-code validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir ${artifactsDir}
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
${JSON.stringify(fallback, null, 2)}
|
|
100
|
-
|
|
101
|
-
Then validate the fallback passes before finishing.`;
|
|
97
|
+
Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.`;
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
mkdirSync(join(runDir, "task-results"), { recursive: true });
|
|
105
|
-
|
|
106
100
|
const plan = [];
|
|
107
101
|
let largestTask = null;
|
|
108
102
|
let largestLines = 0;
|
|
109
103
|
|
|
110
104
|
for (const task of tasks) {
|
|
111
105
|
const sanitizedId = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
112
|
-
const outputPath = join(
|
|
106
|
+
const outputPath = join(taskResultsDir, sanitizedId + ".json");
|
|
107
|
+
const promptPath = join(taskResultsDir, sanitizedId + ".prompt.md");
|
|
113
108
|
const lensDef = lensDefinitions[task.lens];
|
|
114
109
|
|
|
115
110
|
if (!lensDef) {
|
|
116
|
-
|
|
111
|
+
process.stderr.write(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})\n`);
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
const totalFileLines = Object.values(task.file_line_counts).reduce((a, b) => a + b, 0);
|
|
120
|
-
const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalFileLines} lines) — ${task.lens} lens`;
|
|
121
|
-
const prompt = buildPrompt(
|
|
122
|
-
task,
|
|
123
|
-
lensDef ?? { description: task.lens, do_not_report: "N/A" },
|
|
124
|
-
auditResultSchema,
|
|
125
|
-
findingSchema,
|
|
126
|
-
outputPath,
|
|
127
|
-
run_id,
|
|
128
|
-
artifactsDir
|
|
129
|
-
);
|
|
114
|
+
const totalFileLines = Object.values(task.file_line_counts ?? {}).reduce((a, b) => a + b, 0);
|
|
130
115
|
|
|
131
116
|
if (totalFileLines > largestLines) {
|
|
132
117
|
largestLines = totalFileLines;
|
|
133
118
|
largestTask = task.task_id;
|
|
134
119
|
}
|
|
135
|
-
|
|
136
120
|
if (totalFileLines > 1500) {
|
|
137
|
-
|
|
121
|
+
process.stderr.write(`Warning: large task ${task.task_id} (~${totalFileLines} lines) may hit quota limits\n`);
|
|
138
122
|
}
|
|
139
123
|
|
|
140
|
-
|
|
124
|
+
const prompt = buildPrompt(task, lensDef, outputPath, run_id, artifactsDir);
|
|
125
|
+
writeFileSync(promptPath, prompt, "utf8");
|
|
126
|
+
|
|
127
|
+
const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalFileLines} lines) — ${task.lens} lens`;
|
|
128
|
+
plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt_path: promptPath });
|
|
141
129
|
}
|
|
142
130
|
|
|
143
131
|
writeFileSync(dispatchPlanPath, JSON.stringify(plan, null, 2));
|
|
@@ -146,10 +134,3 @@ console.log(`Wrote dispatch-plan.json — ${plan.length} tasks ready for dispatc
|
|
|
146
134
|
if (largestTask) {
|
|
147
135
|
console.log(`Largest task: ${largestTask} (~${largestLines} lines)`);
|
|
148
136
|
}
|
|
149
|
-
console.log("");
|
|
150
|
-
console.log("--- ORCHESTRATOR INSTRUCTIONS ---");
|
|
151
|
-
console.log("Read dispatch-plan.json. For each entry, fire one Agent call with:");
|
|
152
|
-
console.log(" description: <entry.description>");
|
|
153
|
-
console.log(" prompt: <entry.prompt>");
|
|
154
|
-
console.log(`Fire all ${plan.length} calls in a single message for parallel execution.`);
|
|
155
|
-
console.log(`When all complete, run: node dispatch/merge-results.mjs --run-id ${run_id}`);
|
|
@@ -5,27 +5,31 @@ import { validateResult } from "./validate.mjs";
|
|
|
5
5
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
-
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Support both named flags and legacy positional args:
|
|
10
|
+
// Named: --run-id <id> --task-id <id> [--artifacts-dir <dir>]
|
|
11
|
+
// Positional (legacy): <run_id> <task_id>
|
|
12
|
+
const runIdFlagIdx = process.argv.indexOf("--run-id");
|
|
13
|
+
const taskIdFlagIdx = process.argv.indexOf("--task-id");
|
|
14
|
+
const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
|
|
15
|
+
|
|
16
|
+
const run_id = runIdFlagIdx !== -1
|
|
17
|
+
? process.argv[runIdFlagIdx + 1]
|
|
18
|
+
: process.argv[2];
|
|
19
|
+
|
|
20
|
+
const task_id = taskIdFlagIdx !== -1
|
|
21
|
+
? process.argv[taskIdFlagIdx + 1]
|
|
22
|
+
: process.argv[3];
|
|
12
23
|
|
|
13
24
|
if (!run_id || !task_id) {
|
|
14
|
-
console.error("Usage: node dispatch/validate-result.mjs <run_id> <task_id>");
|
|
25
|
+
console.error("Usage: node dispatch/validate-result.mjs --run-id <run_id> --task-id <task_id> [--artifacts-dir <dir>]");
|
|
15
26
|
process.exit(1);
|
|
16
27
|
}
|
|
17
28
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const cfg = JSON.parse(readFileSync(sessionConfigPath, "utf8"));
|
|
24
|
-
if (cfg.artifacts_dir) artifactsDir = cfg.artifacts_dir;
|
|
25
|
-
} catch {
|
|
26
|
-
// use default
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
+
// Artifacts dir: explicit flag > CWD default
|
|
30
|
+
const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
|
|
31
|
+
? resolve(process.argv[artifactsDirIdx + 1])
|
|
32
|
+
: join(process.cwd(), ".audit-artifacts");
|
|
29
33
|
|
|
30
34
|
const sanitized = task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
31
35
|
const resultPath = join(artifactsDir, "runs", run_id, "task-results", sanitized + ".json");
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, readdir, rename } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,6 +28,7 @@ import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writ
|
|
|
28
28
|
import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
|
|
29
29
|
import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
|
|
30
30
|
import { runAuditCodeMcpServer } from "./mcp/server.js";
|
|
31
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
31
32
|
const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
|
|
32
33
|
const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
|
|
33
34
|
const DIRECT_CLI_DEFAULTS = {
|
|
@@ -170,8 +171,9 @@ async function emitEnvelope(params) {
|
|
|
170
171
|
}
|
|
171
172
|
function buildManualReviewBlocker(providerName) {
|
|
172
173
|
return providerName === LOCAL_SUBPROCESS_PROVIDER_NAME
|
|
173
|
-
? "
|
|
174
|
-
|
|
174
|
+
? "Ready for LLM review. Dispatched task files are in .audit-artifacts/dispatch/. " +
|
|
175
|
+
"Review the code, write audit results to the specified path, then run the worker_command to continue."
|
|
176
|
+
: "Audit blocked: waiting for manual audit results or interactive provider configuration.";
|
|
175
177
|
}
|
|
176
178
|
function shouldRunInlineExecutor(selectedExecutor) {
|
|
177
179
|
return selectedExecutor !== null && selectedExecutor !== "agent";
|
|
@@ -1393,6 +1395,284 @@ async function cmdWorkerRun(argv) {
|
|
|
1393
1395
|
process.exitCode = 1;
|
|
1394
1396
|
}
|
|
1395
1397
|
}
|
|
1398
|
+
async function cmdPrepareDispatch(argv) {
|
|
1399
|
+
const runId = getFlag(argv, "--run-id");
|
|
1400
|
+
if (!runId)
|
|
1401
|
+
throw new Error("prepare-dispatch requires --run-id <run_id>");
|
|
1402
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
1403
|
+
const runDir = join(artifactsDir, "runs", runId);
|
|
1404
|
+
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
1405
|
+
const taskResultsDir = join(runDir, "task-results");
|
|
1406
|
+
const dispatchPlanPath = join(runDir, "dispatch-plan.json");
|
|
1407
|
+
const tasks = await readJsonFile(tasksPath);
|
|
1408
|
+
const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
|
|
1409
|
+
const lensDefs = await readJsonFile(lensDefsPath);
|
|
1410
|
+
await mkdir(taskResultsDir, { recursive: true });
|
|
1411
|
+
const plan = [];
|
|
1412
|
+
let largestTask = null;
|
|
1413
|
+
let largestLines = 0;
|
|
1414
|
+
for (const task of tasks) {
|
|
1415
|
+
const sanitized = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1416
|
+
const outputPath = join(taskResultsDir, `${sanitized}.json`);
|
|
1417
|
+
const promptPath = join(taskResultsDir, `${sanitized}.prompt.md`);
|
|
1418
|
+
const lensDef = lensDefs[task.lens];
|
|
1419
|
+
if (!lensDef) {
|
|
1420
|
+
process.stderr.write(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})\n`);
|
|
1421
|
+
}
|
|
1422
|
+
const totalLines = Object.values(task.file_line_counts ?? {}).reduce((a, b) => a + b, 0);
|
|
1423
|
+
if (totalLines > largestLines) {
|
|
1424
|
+
largestLines = totalLines;
|
|
1425
|
+
largestTask = task.task_id;
|
|
1426
|
+
}
|
|
1427
|
+
if (totalLines > 1500) {
|
|
1428
|
+
process.stderr.write(`Warning: large task ${task.task_id} (~${totalLines} lines) may hit quota limits\n`);
|
|
1429
|
+
}
|
|
1430
|
+
const fileList = task.file_paths.map(p => {
|
|
1431
|
+
const lines = task.file_line_counts?.[p] ?? 0;
|
|
1432
|
+
return `- ${p} (${lines} lines)`;
|
|
1433
|
+
}).join("\n");
|
|
1434
|
+
const prompt = [
|
|
1435
|
+
"You are a code auditor. Review the files below under the specified lens.",
|
|
1436
|
+
"",
|
|
1437
|
+
"## Task",
|
|
1438
|
+
`task_id: ${task.task_id}`,
|
|
1439
|
+
`unit_id: ${task.unit_id}`,
|
|
1440
|
+
`pass_id: ${task.pass_id}`,
|
|
1441
|
+
`lens: ${task.lens}`,
|
|
1442
|
+
"",
|
|
1443
|
+
"## Files to read",
|
|
1444
|
+
"Use your Read tool. Paths are repo-relative from the current working directory.",
|
|
1445
|
+
fileList,
|
|
1446
|
+
"",
|
|
1447
|
+
`## Lens: ${task.lens}`,
|
|
1448
|
+
lensDef?.description ?? task.lens,
|
|
1449
|
+
"",
|
|
1450
|
+
`Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
|
|
1451
|
+
"",
|
|
1452
|
+
"## Output",
|
|
1453
|
+
`Write a single JSON object to: ${outputPath}`,
|
|
1454
|
+
"",
|
|
1455
|
+
"Required fields:",
|
|
1456
|
+
" task_id copy from task metadata above",
|
|
1457
|
+
" unit_id copy from task metadata above",
|
|
1458
|
+
" pass_id copy from task metadata above",
|
|
1459
|
+
" lens copy from task metadata above",
|
|
1460
|
+
" file_coverage [{path, total_lines}] — one entry per file; use the line counts listed above",
|
|
1461
|
+
" findings [] or array of finding objects (see below)",
|
|
1462
|
+
"",
|
|
1463
|
+
"Each finding object:",
|
|
1464
|
+
" id unique ID, e.g. \"COR-001\"",
|
|
1465
|
+
" title short title",
|
|
1466
|
+
" category correctness|architecture|maintainability|security|reliability|performance|data_integrity|tests|operability|config_deployment",
|
|
1467
|
+
" severity critical|high|medium|low|info",
|
|
1468
|
+
" confidence high|medium|low",
|
|
1469
|
+
` lens "${task.lens}" — must match task lens exactly`,
|
|
1470
|
+
" summary 1–2 sentence description",
|
|
1471
|
+
" affected_files [{path, line_start?, line_end?, symbol?}] — objects, not strings; min 1 entry",
|
|
1472
|
+
" evidence [\"path/to/file.ts:42 — description of what you see there\"] — min 1 entry",
|
|
1473
|
+
"",
|
|
1474
|
+
"Constraints:",
|
|
1475
|
+
"1. line_end must not exceed the file's actual line count (use counts listed above)",
|
|
1476
|
+
"2. affected_files entries are OBJECTS with a \"path\" key — NOT plain strings",
|
|
1477
|
+
"3. Only reference files from the list above",
|
|
1478
|
+
"4. findings: [] is correct when you find nothing genuine",
|
|
1479
|
+
"",
|
|
1480
|
+
"## Validate",
|
|
1481
|
+
"After writing your result, run:",
|
|
1482
|
+
` audit-code validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir ${artifactsDir}`,
|
|
1483
|
+
"",
|
|
1484
|
+
"Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.",
|
|
1485
|
+
].join("\n");
|
|
1486
|
+
await writeFile(promptPath, prompt, "utf8");
|
|
1487
|
+
const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalLines} lines) — ${task.lens} lens`;
|
|
1488
|
+
plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt_path: promptPath });
|
|
1489
|
+
}
|
|
1490
|
+
await writeJsonFile(dispatchPlanPath, plan);
|
|
1491
|
+
console.log(`Wrote dispatch-plan.json — ${plan.length} tasks ready for dispatch`);
|
|
1492
|
+
if (largestTask)
|
|
1493
|
+
console.log(`Largest task: ${largestTask} (~${largestLines} lines)`);
|
|
1494
|
+
}
|
|
1495
|
+
async function cmdMergeAndIngest(argv) {
|
|
1496
|
+
const runId = getFlag(argv, "--run-id");
|
|
1497
|
+
if (!runId)
|
|
1498
|
+
throw new Error("merge-and-ingest requires --run-id <run_id>");
|
|
1499
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
1500
|
+
const runDir = join(artifactsDir, "runs", runId);
|
|
1501
|
+
const taskResultsDir = join(runDir, "task-results");
|
|
1502
|
+
const auditResultsPath = join(runDir, "audit-results.json");
|
|
1503
|
+
const taskPath = join(runDir, "task.json");
|
|
1504
|
+
// Merge: collect all per-task result files (skip .prompt.md files)
|
|
1505
|
+
let files;
|
|
1506
|
+
try {
|
|
1507
|
+
files = (await readdir(taskResultsDir))
|
|
1508
|
+
.filter(f => f.endsWith(".json"))
|
|
1509
|
+
.sort();
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
files = [];
|
|
1513
|
+
}
|
|
1514
|
+
const passing = [];
|
|
1515
|
+
const failing = [];
|
|
1516
|
+
for (const filename of files) {
|
|
1517
|
+
const filePath = join(taskResultsDir, filename);
|
|
1518
|
+
let obj;
|
|
1519
|
+
try {
|
|
1520
|
+
obj = JSON.parse(await readFile(filePath, "utf8"));
|
|
1521
|
+
}
|
|
1522
|
+
catch (e) {
|
|
1523
|
+
failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const r = obj;
|
|
1527
|
+
const missing = ["task_id", "unit_id", "pass_id", "lens", "file_coverage", "findings"].filter(f => !(f in r));
|
|
1528
|
+
if (missing.length > 0) {
|
|
1529
|
+
failing.push({ task_id: String(r.task_id ?? filename), errors: [`Missing required fields: ${missing.join(", ")}`] });
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
passing.push(obj);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
await writeJsonFile(auditResultsPath, passing);
|
|
1536
|
+
if (failing.length > 0) {
|
|
1537
|
+
await writeJsonFile(join(runDir, "failed-tasks.json"), failing);
|
|
1538
|
+
process.stderr.write(`${failing.length} task(s) excluded — see ${join(runDir, "failed-tasks.json")}\n`);
|
|
1539
|
+
}
|
|
1540
|
+
process.stderr.write(`✓ ${passing.length}/${files.length} results merged → ${auditResultsPath}\n`);
|
|
1541
|
+
// Ingest: run worker-run logic against the merged results file
|
|
1542
|
+
await cmdWorkerRun([argv[0], argv[1], "worker-run", "--task", taskPath, "--artifacts-dir", artifactsDir]);
|
|
1543
|
+
}
|
|
1544
|
+
const VALID_LENSES_SET = new Set([
|
|
1545
|
+
"correctness", "architecture", "maintainability", "security", "reliability",
|
|
1546
|
+
"performance", "data_integrity", "tests", "operability", "config_deployment",
|
|
1547
|
+
]);
|
|
1548
|
+
const VALID_SEVERITIES_SET = new Set(["critical", "high", "medium", "low", "info"]);
|
|
1549
|
+
const VALID_CONFIDENCES_SET = new Set(["high", "medium", "low"]);
|
|
1550
|
+
async function cmdValidateResult(argv) {
|
|
1551
|
+
const runId = getFlag(argv, "--run-id");
|
|
1552
|
+
const taskId = getFlag(argv, "--task-id");
|
|
1553
|
+
if (!runId || !taskId)
|
|
1554
|
+
throw new Error("validate-result requires --run-id and --task-id");
|
|
1555
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
1556
|
+
const sanitized = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1557
|
+
const resultPath = join(artifactsDir, "runs", runId, "task-results", `${sanitized}.json`);
|
|
1558
|
+
const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
|
|
1559
|
+
let raw;
|
|
1560
|
+
try {
|
|
1561
|
+
raw = await readFile(resultPath, "utf8");
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
console.error(`File not found: ${resultPath}`);
|
|
1565
|
+
process.exitCode = 1;
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
let obj;
|
|
1569
|
+
try {
|
|
1570
|
+
obj = JSON.parse(raw);
|
|
1571
|
+
}
|
|
1572
|
+
catch (e) {
|
|
1573
|
+
console.error(`Invalid JSON: ${e.message}`);
|
|
1574
|
+
process.exitCode = 1;
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const errors = [];
|
|
1578
|
+
// Required top-level fields
|
|
1579
|
+
for (const field of ["task_id", "unit_id", "pass_id", "lens", "file_coverage", "findings"]) {
|
|
1580
|
+
if (!(field in obj))
|
|
1581
|
+
errors.push(`Missing required field: ${field}`);
|
|
1582
|
+
}
|
|
1583
|
+
if (errors.length > 0) {
|
|
1584
|
+
console.error(`✗ invalid: ${taskId}`);
|
|
1585
|
+
for (const e of errors)
|
|
1586
|
+
console.error(` ${e}`);
|
|
1587
|
+
process.exitCode = 1;
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
// Lens
|
|
1591
|
+
if (typeof obj.lens !== "string" || !VALID_LENSES_SET.has(obj.lens)) {
|
|
1592
|
+
errors.push(`lens must be one of: ${[...VALID_LENSES_SET].join("|")}`);
|
|
1593
|
+
}
|
|
1594
|
+
// file_coverage
|
|
1595
|
+
if (!Array.isArray(obj.file_coverage) || obj.file_coverage.length === 0) {
|
|
1596
|
+
errors.push("file_coverage must be a non-empty array");
|
|
1597
|
+
}
|
|
1598
|
+
else {
|
|
1599
|
+
for (const fc of obj.file_coverage) {
|
|
1600
|
+
const entry = fc;
|
|
1601
|
+
if (typeof entry.path !== "string")
|
|
1602
|
+
errors.push(`file_coverage entry missing string 'path'`);
|
|
1603
|
+
if (typeof entry.total_lines !== "number")
|
|
1604
|
+
errors.push(`file_coverage entry missing numeric 'total_lines'`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
// findings
|
|
1608
|
+
if (!Array.isArray(obj.findings)) {
|
|
1609
|
+
errors.push("findings must be an array");
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
for (const f of obj.findings) {
|
|
1613
|
+
const finding = f;
|
|
1614
|
+
for (const field of ["id", "title", "category", "severity", "confidence", "lens", "summary"]) {
|
|
1615
|
+
if (typeof finding[field] !== "string")
|
|
1616
|
+
errors.push(`finding missing string '${field}'`);
|
|
1617
|
+
}
|
|
1618
|
+
if (typeof finding.severity === "string" && !VALID_SEVERITIES_SET.has(finding.severity)) {
|
|
1619
|
+
errors.push(`finding '${finding.id}': invalid severity '${finding.severity}'`);
|
|
1620
|
+
}
|
|
1621
|
+
if (typeof finding.confidence === "string" && !VALID_CONFIDENCES_SET.has(finding.confidence)) {
|
|
1622
|
+
errors.push(`finding '${finding.id}': invalid confidence '${finding.confidence}'`);
|
|
1623
|
+
}
|
|
1624
|
+
if (!Array.isArray(finding.affected_files) || finding.affected_files.length === 0) {
|
|
1625
|
+
errors.push(`finding '${finding.id}': affected_files must be a non-empty array`);
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
for (const af of finding.affected_files) {
|
|
1629
|
+
if (typeof af.path !== "string") {
|
|
1630
|
+
errors.push(`finding '${finding.id}': affected_files entries must be objects with a 'path' key`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (!Array.isArray(finding.evidence) || finding.evidence.length === 0) {
|
|
1635
|
+
errors.push(`finding '${finding.id}': evidence must be a non-empty array`);
|
|
1636
|
+
}
|
|
1637
|
+
if (typeof finding.lens === "string" && finding.lens !== obj.lens) {
|
|
1638
|
+
errors.push(`finding '${finding.id}': lens '${finding.lens}' does not match task lens '${obj.lens}'`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
// Line range bounds (load from pending-audit-tasks.json if available)
|
|
1643
|
+
let fileLineCounts = {};
|
|
1644
|
+
try {
|
|
1645
|
+
const tasks = await readJsonFile(tasksPath);
|
|
1646
|
+
const task = tasks.find(t => t.task_id === taskId);
|
|
1647
|
+
fileLineCounts = task?.file_line_counts ?? {};
|
|
1648
|
+
}
|
|
1649
|
+
catch { /* ignore */ }
|
|
1650
|
+
if (Array.isArray(obj.file_coverage) && Array.isArray(obj.findings)) {
|
|
1651
|
+
const coverageMap = new Map(obj.file_coverage.map(fc => [fc.path, fc.total_lines]));
|
|
1652
|
+
const allowedPaths = new Set(coverageMap.keys());
|
|
1653
|
+
for (const f of obj.findings) {
|
|
1654
|
+
for (const af of (f.affected_files ?? [])) {
|
|
1655
|
+
const p = af.path;
|
|
1656
|
+
if (!allowedPaths.has(p))
|
|
1657
|
+
errors.push(`finding '${f.id}': path '${p}' not in file_coverage`);
|
|
1658
|
+
if (typeof af.line_end === "number") {
|
|
1659
|
+
const max = coverageMap.get(p) ?? fileLineCounts[p] ?? Infinity;
|
|
1660
|
+
if (af.line_end > max)
|
|
1661
|
+
errors.push(`finding '${f.id}': line_end ${af.line_end} exceeds total_lines ${max} for ${p}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (errors.length === 0) {
|
|
1667
|
+
console.log(`✓ valid: ${taskId}`);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
console.error(`✗ invalid: ${taskId}`);
|
|
1671
|
+
for (const e of errors)
|
|
1672
|
+
console.error(` ${e}`);
|
|
1673
|
+
process.exitCode = 1;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1396
1676
|
async function cmdImportExternalAnalyzer(argv) {
|
|
1397
1677
|
const artifactsDir = getArtifactsDir(argv);
|
|
1398
1678
|
const sourcePath = getFlag(argv, "--external-analyzer-results", `${artifactsDir}/external_analyzer_results.json`);
|
|
@@ -1641,9 +1921,18 @@ async function main(argv) {
|
|
|
1641
1921
|
case "mcp":
|
|
1642
1922
|
await cmdMcp(argv);
|
|
1643
1923
|
return;
|
|
1924
|
+
case "prepare-dispatch":
|
|
1925
|
+
await cmdPrepareDispatch(argv);
|
|
1926
|
+
return;
|
|
1927
|
+
case "merge-and-ingest":
|
|
1928
|
+
await cmdMergeAndIngest(argv);
|
|
1929
|
+
return;
|
|
1930
|
+
case "validate-result":
|
|
1931
|
+
await cmdValidateResult(argv);
|
|
1932
|
+
return;
|
|
1644
1933
|
default:
|
|
1645
1934
|
console.error(`Unknown command: ${command}`);
|
|
1646
|
-
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");
|
|
1935
|
+
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");
|
|
1647
1936
|
process.exitCode = 1;
|
|
1648
1937
|
}
|
|
1649
1938
|
}
|
|
@@ -8,60 +8,28 @@ export function renderWorkerPrompt(task) {
|
|
|
8
8
|
const tasksPath = task.pending_audit_tasks_path ??
|
|
9
9
|
`${task.artifacts_dir}/audit_tasks.json`;
|
|
10
10
|
const lines = [
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
"",
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"For each task:",
|
|
19
|
-
" 1. Read every file listed in file_paths in full using your file-reading tool.",
|
|
20
|
-
" If line_ranges are present, they are a focus hint — still read the whole file.",
|
|
21
|
-
" 2. Review the content under the specified lens.",
|
|
22
|
-
" 3. Emit one AuditResult with:",
|
|
23
|
-
" task_id, unit_id, pass_id, lens",
|
|
24
|
-
" file_coverage: [{path, total_lines}] for every assigned file you reviewed",
|
|
25
|
-
" findings: array (empty if nothing found)",
|
|
26
|
-
" If the task includes file_line_counts, use those values for file_coverage.total_lines.",
|
|
27
|
-
" total_lines must match the file's current total line count.",
|
|
28
|
-
" Each finding must include:",
|
|
29
|
-
" id, title, category, severity, confidence, lens, summary",
|
|
30
|
-
" affected_files: [{path, line_start?, line_end?, symbol?}] — path is repo-relative, NOT a plain string",
|
|
31
|
-
" evidence: array of plain strings only, at least one excerpt or line reference from the file you read",
|
|
32
|
-
" Example evidence entry: src/foo.ts:42 - variable overwritten before use",
|
|
33
|
-
" Example affected_files entry: {\"path\": \"src/foo.ts\", \"line_start\": 42, \"line_end\": 55, \"symbol\": \"myFunction\"}",
|
|
34
|
-
" Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
|
|
35
|
-
" Low-priority tasks still require a real review. Use findings: [] only when you genuinely found nothing notable.",
|
|
36
|
-
task.timeout_ms
|
|
37
|
-
? ` Time budget for this task: ${task.timeout_ms} ms.`
|
|
38
|
-
: " Keep the task bounded to the assigned files only.",
|
|
39
|
-
`Reference schemas: ${task.artifacts_dir}/dispatch/audit-result.schema.json and ${task.artifacts_dir}/dispatch/finding.schema.json`,
|
|
40
|
-
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
11
|
+
`Audit run: ${task.run_id}`,
|
|
12
|
+
`Read: ${tasksPath}`,
|
|
13
|
+
"For each task: read all file_paths in full, review under the specified lens,",
|
|
14
|
+
"and emit one AuditResult with: task_id, unit_id, pass_id, lens, file_coverage,",
|
|
15
|
+
"findings. Each finding: id, title, category, severity, confidence, lens, summary,",
|
|
16
|
+
"affected_files (path, line_start, line_end, symbol), evidence (plain strings).",
|
|
17
|
+
`Write to: ${task.audit_results_path}`,
|
|
41
18
|
];
|
|
42
19
|
if (usesDeferredWorkerCommand(task)) {
|
|
43
|
-
lines.push("
|
|
20
|
+
lines.push("Deferred mode: write results, do not execute worker_command.");
|
|
44
21
|
}
|
|
45
22
|
else {
|
|
46
|
-
lines.push("
|
|
23
|
+
lines.push("Then execute worker_command from task.json exactly.", `Command: ${commandArgv}`);
|
|
47
24
|
}
|
|
48
25
|
return lines.join("\n");
|
|
49
26
|
}
|
|
50
27
|
return [
|
|
51
|
-
|
|
52
|
-
`Run ID: ${task.run_id}`,
|
|
53
|
-
`Repository root: ${task.repo_root}`,
|
|
54
|
-
`Obligation: ${task.obligation_id ?? "unknown"}`,
|
|
28
|
+
`Task: ${task.run_id}`,
|
|
55
29
|
`Executor: ${task.preferred_executor}`,
|
|
56
|
-
"Execute
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"Do not choose another task.",
|
|
61
|
-
task.timeout_ms
|
|
62
|
-
? `The worker command is budgeted for ${task.timeout_ms} ms.`
|
|
63
|
-
: "If the command hangs or fails, stop and let the supervisor handle it.",
|
|
64
|
-
`The command must write the worker result JSON to: ${task.result_path}`,
|
|
65
|
-
"After the command completes, stop.",
|
|
30
|
+
"Execute worker_command from task.json exactly.",
|
|
31
|
+
`Command: ${commandArgv}`,
|
|
32
|
+
"Write result to: " + task.result_path,
|
|
33
|
+
"Stop after completion.",
|
|
66
34
|
].join("\n");
|
|
67
35
|
}
|
package/dist/providers/index.js
CHANGED
|
@@ -29,6 +29,14 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
|
|
|
29
29
|
const lookupCommand = options.commandExists ?? commandExists;
|
|
30
30
|
const inVSCode = (env.TERM_PROGRAM ?? "").toLowerCase() === "vscode";
|
|
31
31
|
const insideClaudeCode = Boolean(env.CLAUDECODE);
|
|
32
|
+
const insideOpenCode = Boolean(env.OPENCODE);
|
|
33
|
+
// If we're inside a specific IDE/conversation, use that as the provider
|
|
34
|
+
if (insideOpenCode) {
|
|
35
|
+
return "opencode";
|
|
36
|
+
}
|
|
37
|
+
if (insideClaudeCode) {
|
|
38
|
+
return "claude-code";
|
|
39
|
+
}
|
|
32
40
|
if (inVSCode && hasEntries(sessionConfig.vscode_task?.command_template)) {
|
|
33
41
|
return "vscode-task";
|
|
34
42
|
}
|
|
@@ -38,6 +38,8 @@ export interface AuditCodeHandoff {
|
|
|
38
38
|
interactive_provider_hint: string | null;
|
|
39
39
|
artifact_paths: AuditCodeHandoffArtifactPaths;
|
|
40
40
|
active_review_run?: ActiveReviewRun;
|
|
41
|
+
quick_start?: string;
|
|
42
|
+
file_map?: Record<string, string>;
|
|
41
43
|
}
|
|
42
44
|
export declare function buildAuditCodeHandoff(params: {
|
|
43
45
|
root: string;
|
|
@@ -115,10 +115,10 @@ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, i
|
|
|
115
115
|
return null;
|
|
116
116
|
}
|
|
117
117
|
if (isConfigError) {
|
|
118
|
-
return `
|
|
118
|
+
return `Configuration error: Verify --root points to a repository root (with package.json, go.mod, etc.).`;
|
|
119
119
|
}
|
|
120
120
|
const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
|
|
121
|
-
return `
|
|
121
|
+
return `Provider: ${providerLabel}. For automatic LLM review, configure an interactive provider in ${sessionConfigPath}.`;
|
|
122
122
|
}
|
|
123
123
|
function renderMarkdown(handoff) {
|
|
124
124
|
const lines = [
|
|
@@ -208,7 +208,7 @@ export function buildAuditCodeHandoff(params) {
|
|
|
208
208
|
: null,
|
|
209
209
|
};
|
|
210
210
|
const suggestedInputs = buildSuggestedInputs(params.artifactsDir, params.state.status, isConfigError, params.activeReviewRun);
|
|
211
|
-
|
|
211
|
+
const handoff = {
|
|
212
212
|
status: params.state.status,
|
|
213
213
|
repo_root: params.root,
|
|
214
214
|
artifacts_dir: params.artifactsDir,
|
|
@@ -221,6 +221,17 @@ export function buildAuditCodeHandoff(params) {
|
|
|
221
221
|
artifact_paths: artifactPaths,
|
|
222
222
|
active_review_run: params.activeReviewRun,
|
|
223
223
|
};
|
|
224
|
+
// Add quick_start command and file map when blocked for review
|
|
225
|
+
if (params.state.status === BLOCKED_STATUS && params.activeReviewRun) {
|
|
226
|
+
handoff.quick_start = `audit-code worker-run --task ${params.activeReviewRun.task_path}`;
|
|
227
|
+
handoff.file_map = {
|
|
228
|
+
current_task: artifactPaths.current_task,
|
|
229
|
+
current_prompt: artifactPaths.current_prompt,
|
|
230
|
+
audit_results: params.activeReviewRun.audit_results_path,
|
|
231
|
+
final_report: join(params.root, "audit-report.md"),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return handoff;
|
|
224
235
|
}
|
|
225
236
|
export async function writeAuditCodeHandoffArtifacts(handoff) {
|
|
226
237
|
try {
|
package/package.json
CHANGED
|
@@ -21,10 +21,10 @@ Repeat Steps 1–5 until the audit status is `"complete"`.
|
|
|
21
21
|
Run:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
|
|
24
|
+
audit-code
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
_(
|
|
27
|
+
_(Inside the `auditor-lambda` repo itself, use `node audit-code.mjs` instead.)_
|
|
28
28
|
|
|
29
29
|
Parse the JSON output. Check `audit_state.status`:
|
|
30
30
|
|
|
@@ -38,13 +38,13 @@ Parse the JSON output. Check `audit_state.status`:
|
|
|
38
38
|
|
|
39
39
|
### Step 2 — Read the Task
|
|
40
40
|
|
|
41
|
-
Read `.audit-artifacts/dispatch/current-task.json`.
|
|
41
|
+
Read the file at `.audit-artifacts/dispatch/current-task.json`.
|
|
42
42
|
|
|
43
43
|
Note these fields:
|
|
44
44
|
- `run_id` — identifies this batch of audit work
|
|
45
45
|
- `artifacts_dir` — base artifacts directory
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
|
|
47
|
+
_(If `audit_state.blockers` contains a message that requires operator input rather than code review, stop and report the blocker verbatim to the user.)_
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -53,16 +53,14 @@ Note these fields:
|
|
|
53
53
|
Run:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
|
|
56
|
+
audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Read `dispatch-plan.json`. It is a JSON array where each entry has:
|
|
59
|
+
Read `<artifacts_dir>/runs/<run_id>/dispatch-plan.json`. It is a JSON array where each entry has:
|
|
62
60
|
- `task_id` — task identifier
|
|
63
61
|
- `description` — short label for the Agent call
|
|
64
62
|
- `output_path` — where the subagent writes its result
|
|
65
|
-
- `
|
|
63
|
+
- `prompt_path` — path to the complete subagent instructions file
|
|
66
64
|
|
|
67
65
|
---
|
|
68
66
|
|
|
@@ -71,25 +69,23 @@ Read `dispatch-plan.json`. It is a JSON array where each entry has:
|
|
|
71
69
|
**In a single message**, fire one `Agent` call per entry in `dispatch-plan.json`:
|
|
72
70
|
|
|
73
71
|
```
|
|
74
|
-
Agent({ description: entry.description, prompt: entry.
|
|
72
|
+
Agent({ description: entry.description, prompt: "Read and follow the audit instructions in: " + entry.prompt_path })
|
|
75
73
|
```
|
|
76
74
|
|
|
77
75
|
All calls must be sent simultaneously — never await one before firing the next. This is the critical performance constraint. Wait for all to complete before proceeding.
|
|
78
76
|
|
|
79
|
-
Each subagent reads its assigned
|
|
77
|
+
Each subagent reads its instruction file, reviews the assigned code, writes a validated JSON result to `output_path`, and self-validates. You do not need to inspect individual subagent output.
|
|
80
78
|
|
|
81
79
|
---
|
|
82
80
|
|
|
83
81
|
### Step 5 — Merge and Ingest
|
|
84
82
|
|
|
85
|
-
Run
|
|
83
|
+
Run:
|
|
86
84
|
|
|
87
85
|
```bash
|
|
88
|
-
|
|
86
|
+
audit-code merge-and-ingest --run-id <run_id> --artifacts-dir <artifacts_dir>
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
Then execute the `worker_command` from `current-task.json`. It is a JSON array — join the elements into a shell command and run it.
|
|
92
|
-
|
|
93
89
|
Loop back to **Step 1**.
|
|
94
90
|
|
|
95
91
|
---
|
|
@@ -98,14 +94,12 @@ Loop back to **Step 1**.
|
|
|
98
94
|
|
|
99
95
|
When `audit_state.status` is `"complete"`, stop the loop. Do **not** run the orchestrator again.
|
|
100
96
|
|
|
101
|
-
Read `audit-report.md` and present the completed audit to the user. Lead with the work blocks — they are the primary remediation handoff.
|
|
97
|
+
Read `audit-report.md` and present the completed audit to the user. Lead with the work blocks — they are the primary remediation handoff.
|
|
102
98
|
|
|
103
99
|
---
|
|
104
100
|
|
|
105
101
|
## Edge Cases
|
|
106
102
|
|
|
107
|
-
**
|
|
108
|
-
|
|
109
|
-
**Large task warnings:** `prepare-dispatch.mjs` warns about tasks exceeding ~1500 lines. If a subagent hits a quota limit and fails to produce output, `merge-results.mjs` excludes it silently — those tasks remain pending and are picked up in the next loop iteration. No manual intervention needed.
|
|
103
|
+
**Large task warnings:** `prepare-dispatch` warns about tasks exceeding ~1500 lines. If a subagent hits a quota limit and fails to produce output, `merge-and-ingest` excludes it silently — those tasks remain pending and are picked up in the next loop iteration. No manual intervention needed.
|
|
110
104
|
|
|
111
|
-
**Failed validation:** Subagents self-validate and retry up to 3 times before
|
|
105
|
+
**Failed validation:** Subagents self-validate and retry up to 3 times before finishing. `merge-and-ingest` excludes any results that still lack required fields and writes `failed-tasks.json`. Those tasks are requeued automatically in the next cycle.
|