auditor-lambda 0.2.18 → 0.3.1
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 +18 -0
- 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 +295 -6
- package/dist/prompts/renderWorkerPrompt.js +19 -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 +18 -24
|
@@ -260,6 +260,9 @@ 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 per-task prompt files and dispatch-plan.json for parallel subagent dispatch',
|
|
264
|
+
'- merge-and-ingest --run-id <id> [--root <dir>] [--artifacts-dir <dir>] merges per-task results and ingests them into the coverage matrix',
|
|
265
|
+
'- validate-result --run-id <id> --task-id <id> [--artifacts-dir <dir>] validates a single task result against the schema and line counts',
|
|
263
266
|
'',
|
|
264
267
|
'Primary usage:',
|
|
265
268
|
'- from the repository root, run the wrapper with no arguments',
|
|
@@ -2195,6 +2198,21 @@ export async function runAuditCodeWrapper({
|
|
|
2195
2198
|
return;
|
|
2196
2199
|
}
|
|
2197
2200
|
|
|
2201
|
+
if (argv[0] === 'prepare-dispatch') {
|
|
2202
|
+
await runDistCommand('prepare-dispatch', argv.slice(1));
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
if (argv[0] === 'validate-result') {
|
|
2207
|
+
await runDistCommand('validate-result', argv.slice(1));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if (argv[0] === 'merge-and-ingest') {
|
|
2212
|
+
await runDistCommand('merge-and-ingest', argv.slice(1), { ensureArtifactsDir: true });
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2198
2216
|
const wrapperArgs = [...argv];
|
|
2199
2217
|
if (defaultSingleStep && !hasFlag(wrapperArgs, '--single-step')) {
|
|
2200
2218
|
wrapperArgs.push('--single-step');
|
|
@@ -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 = {
|
|
@@ -111,7 +112,7 @@ function getExplicitProvider(argv) {
|
|
|
111
112
|
return getFlag(argv, "--provider");
|
|
112
113
|
}
|
|
113
114
|
function resolveRunProviderName(argv, sessionConfig) {
|
|
114
|
-
return resolveFreshSessionProviderName(getExplicitProvider(argv)
|
|
115
|
+
return resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig);
|
|
115
116
|
}
|
|
116
117
|
function chunkArray(arr, size) {
|
|
117
118
|
const chunkSize = normalizePositiveInteger(size);
|
|
@@ -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";
|
|
@@ -646,7 +648,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
646
648
|
throw error;
|
|
647
649
|
}
|
|
648
650
|
const explicitProvider = getExplicitProvider(argv);
|
|
649
|
-
const provider = createFreshSessionProvider(explicitProvider
|
|
651
|
+
const provider = createFreshSessionProvider(explicitProvider, sessionConfig);
|
|
650
652
|
const uiMode = getUiMode(argv, sessionConfig.ui_mode ?? "headless");
|
|
651
653
|
const maxRuns = getMaxRuns(argv);
|
|
652
654
|
const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
|
|
@@ -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
|
+
` "${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" 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,33 @@ 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
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
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:",
|
|
15
|
+
" task_id, unit_id, pass_id, lens (copy from task),",
|
|
16
|
+
" file_coverage: [{path, total_lines}] — use file_line_counts[path] from the task for each file,",
|
|
17
|
+
" findings: [] or array of finding objects.",
|
|
18
|
+
"Each finding: id, title, category, severity, confidence, lens, summary,",
|
|
19
|
+
" affected_files [{path, line_start, line_end, symbol}] (objects, not strings; min 1 entry),",
|
|
20
|
+
" evidence [strings] (min 1 entry).",
|
|
21
|
+
"Constraint: line_end must not exceed total_lines for that file.",
|
|
22
|
+
`Write all results as a JSON array to: ${task.audit_results_path}`,
|
|
41
23
|
];
|
|
42
24
|
if (usesDeferredWorkerCommand(task)) {
|
|
43
|
-
lines.push("
|
|
25
|
+
lines.push("Deferred mode: write results, do not execute worker_command.");
|
|
44
26
|
}
|
|
45
27
|
else {
|
|
46
|
-
lines.push("
|
|
28
|
+
lines.push("Then execute worker_command from task.json exactly.", `Command: ${commandArgv}`);
|
|
47
29
|
}
|
|
48
30
|
return lines.join("\n");
|
|
49
31
|
}
|
|
50
32
|
return [
|
|
51
|
-
|
|
52
|
-
`Run ID: ${task.run_id}`,
|
|
53
|
-
`Repository root: ${task.repo_root}`,
|
|
54
|
-
`Obligation: ${task.obligation_id ?? "unknown"}`,
|
|
33
|
+
`Task: ${task.run_id}`,
|
|
55
34
|
`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.",
|
|
35
|
+
"Execute worker_command from task.json exactly.",
|
|
36
|
+
`Command: ${commandArgv}`,
|
|
37
|
+
"Write result to: " + task.result_path,
|
|
38
|
+
"Stop after completion.",
|
|
66
39
|
].join("\n");
|
|
67
40
|
}
|
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
|
|
|
@@ -36,15 +36,13 @@ Parse the JSON output. Check `audit_state.status`:
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
### Step 2 —
|
|
39
|
+
### Step 2 — Extract the Task IDs
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Parse these fields directly from the Step 1 JSON output:
|
|
42
|
+
- `run_id` — from `handoff.active_review_run.run_id`
|
|
43
|
+
- `artifacts_dir` — from `handoff.artifacts_dir`
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
- `run_id` — identifies this batch of audit work
|
|
45
|
-
- `artifacts_dir` — base artifacts directory
|
|
46
|
-
- `pending_audit_tasks_path` — path to the pending task list
|
|
47
|
-
- `worker_command` — JSON array; run this after the audit work is complete
|
|
45
|
+
_(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
46
|
|
|
49
47
|
---
|
|
50
48
|
|
|
@@ -53,16 +51,14 @@ Note these fields:
|
|
|
53
51
|
Run:
|
|
54
52
|
|
|
55
53
|
```bash
|
|
56
|
-
|
|
54
|
+
audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
|
|
57
55
|
```
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Read `dispatch-plan.json`. It is a JSON array where each entry has:
|
|
57
|
+
Read `<artifacts_dir>/runs/<run_id>/dispatch-plan.json`. It is a JSON array where each entry has:
|
|
62
58
|
- `task_id` — task identifier
|
|
63
59
|
- `description` — short label for the Agent call
|
|
64
60
|
- `output_path` — where the subagent writes its result
|
|
65
|
-
- `
|
|
61
|
+
- `prompt_path` — path to the complete subagent instructions file
|
|
66
62
|
|
|
67
63
|
---
|
|
68
64
|
|
|
@@ -71,25 +67,23 @@ Read `dispatch-plan.json`. It is a JSON array where each entry has:
|
|
|
71
67
|
**In a single message**, fire one `Agent` call per entry in `dispatch-plan.json`:
|
|
72
68
|
|
|
73
69
|
```
|
|
74
|
-
Agent({ description: entry.description, prompt: entry.
|
|
70
|
+
Agent({ description: entry.description, prompt: "Read and follow the audit instructions in: " + entry.prompt_path })
|
|
75
71
|
```
|
|
76
72
|
|
|
77
73
|
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
74
|
|
|
79
|
-
Each subagent reads its assigned
|
|
75
|
+
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
76
|
|
|
81
77
|
---
|
|
82
78
|
|
|
83
79
|
### Step 5 — Merge and Ingest
|
|
84
80
|
|
|
85
|
-
Run
|
|
81
|
+
Run:
|
|
86
82
|
|
|
87
83
|
```bash
|
|
88
|
-
|
|
84
|
+
audit-code merge-and-ingest --run-id <run_id> --artifacts-dir <artifacts_dir>
|
|
89
85
|
```
|
|
90
86
|
|
|
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
87
|
Loop back to **Step 1**.
|
|
94
88
|
|
|
95
89
|
---
|
|
@@ -98,14 +92,14 @@ Loop back to **Step 1**.
|
|
|
98
92
|
|
|
99
93
|
When `audit_state.status` is `"complete"`, stop the loop. Do **not** run the orchestrator again.
|
|
100
94
|
|
|
101
|
-
Read `audit-report.md` and present the completed audit to the user. Lead with the work blocks — they are the primary remediation handoff.
|
|
95
|
+
Read `audit-report.md` and present the completed audit to the user. Lead with the work blocks — they are the primary remediation handoff.
|
|
102
96
|
|
|
103
97
|
---
|
|
104
98
|
|
|
105
99
|
## Edge Cases
|
|
106
100
|
|
|
107
|
-
**
|
|
101
|
+
**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.
|
|
108
102
|
|
|
109
|
-
**
|
|
103
|
+
**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.
|
|
110
104
|
|
|
111
|
-
**
|
|
105
|
+
**Command failures:** If `prepare-dispatch` or `merge-and-ingest` exits non-zero, **STOP immediately** and report the exact error output to the user. Do NOT improvise manual dispatch, manually split tasks, manually create directories, manually construct prompts, or manually merge results. These scripts are the canonical mechanism — operating without them produces incorrect output. Fix the underlying issue and re-run the failed command.
|