auditor-lambda 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/audit-code-wrapper-lib.mjs +1 -1
- package/dist/adapters/eslint.js +9 -5
- package/dist/cli.d.ts +42 -1
- package/dist/cli.js +234 -63
- package/dist/extractors/bucketing.d.ts +4 -0
- package/dist/extractors/bucketing.js +6 -2
- package/dist/extractors/disposition.d.ts +4 -0
- package/dist/extractors/disposition.js +6 -2
- package/dist/extractors/fileInventory.js +24 -28
- package/dist/extractors/flows.d.ts +5 -0
- package/dist/extractors/flows.js +18 -38
- package/dist/extractors/pathPatterns.d.ts +10 -3
- package/dist/extractors/pathPatterns.js +109 -61
- package/dist/extractors/surfaces.d.ts +4 -0
- package/dist/extractors/surfaces.js +11 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/io/artifacts.d.ts +55 -40
- package/dist/io/artifacts.js +73 -110
- package/dist/io/json.js +52 -21
- package/dist/io/runArtifacts.d.ts +1 -1
- package/dist/io/runArtifacts.js +26 -3
- package/dist/orchestrator/advance.js +83 -62
- package/dist/orchestrator/autoFixExecutor.js +32 -15
- package/dist/orchestrator/flowCoverage.js +11 -5
- package/dist/orchestrator/flowPlanning.d.ts +7 -2
- package/dist/orchestrator/flowPlanning.js +46 -21
- package/dist/orchestrator/flowRequeue.js +28 -8
- package/dist/orchestrator/internalExecutors.js +12 -8
- package/dist/orchestrator/localCommands.d.ts +14 -0
- package/dist/orchestrator/localCommands.js +124 -0
- package/dist/orchestrator/planning.js +25 -3
- package/dist/orchestrator/requeue.js +11 -1
- package/dist/orchestrator/syntaxResolutionExecutor.js +60 -59
- package/dist/orchestrator/taskBuilder.d.ts +4 -2
- package/dist/orchestrator/taskBuilder.js +153 -52
- package/dist/orchestrator/unitBuilder.d.ts +3 -1
- package/dist/orchestrator/unitBuilder.js +24 -16
- package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
- package/dist/prompts/renderWorkerPrompt.js +16 -8
- package/dist/providers/claudeCodeProvider.d.ts +4 -1
- package/dist/providers/claudeCodeProvider.js +8 -5
- package/dist/providers/localSubprocessProvider.d.ts +4 -0
- package/dist/providers/localSubprocessProvider.js +7 -2
- package/dist/providers/spawnLoggedCommand.d.ts +9 -1
- package/dist/providers/spawnLoggedCommand.js +77 -29
- package/dist/reporting/synthesis.d.ts +2 -0
- package/dist/reporting/synthesis.js +12 -9
- package/dist/supervisor/operatorHandoff.d.ts +1 -1
- package/dist/supervisor/operatorHandoff.js +56 -18
- package/dist/supervisor/runLedger.d.ts +1 -1
- package/dist/supervisor/runLedger.js +112 -5
- package/dist/supervisor/sessionConfig.js +10 -10
- package/dist/types/externalAnalyzer.d.ts +3 -0
- package/dist/types/flowCoverage.d.ts +5 -1
- package/dist/types/flowCoverage.js +5 -1
- package/dist/types/flows.d.ts +5 -1
- package/dist/types/flows.js +1 -1
- package/dist/types/runLedger.d.ts +5 -1
- package/dist/types/runLedger.js +6 -1
- package/dist/types/runtimeValidation.d.ts +12 -3
- package/dist/types/runtimeValidation.js +16 -1
- package/dist/types/sessionConfig.d.ts +15 -2
- package/dist/types/sessionConfig.js +15 -1
- package/dist/types/surfaces.d.ts +4 -1
- package/dist/types/surfaces.js +1 -1
- package/dist/types/workerSession.d.ts +9 -0
- package/dist/types/workerSession.js +5 -1
- package/dist/validation/artifacts.d.ts +1 -1
- package/dist/validation/artifacts.js +33 -20
- package/dist/validation/auditResults.d.ts +2 -2
- package/dist/validation/auditResults.js +7 -15
- package/dist/validation/basic.d.ts +9 -1
- package/dist/validation/basic.js +40 -3
- package/dist/validation/sessionConfig.d.ts +4 -2
- package/dist/validation/sessionConfig.js +62 -15
- package/docs/agent-integrations.md +31 -11
- package/docs/next-steps.md +21 -4
- package/docs/packaging.md +14 -0
- package/docs/product-direction.md +22 -0
- package/docs/production-launch-bar.md +2 -0
- package/docs/releasing.md +17 -0
- package/docs/remediation-baseline.md +75 -0
- package/docs/run-flow.md +23 -11
- package/docs/session-config.md +53 -6
- package/docs/supervisor.md +7 -0
- package/docs/workflow-refactor-brief.md +177 -0
- package/package.json +1 -1
- package/schemas/audit-code-v1alpha1.schema.json +1 -0
- package/schemas/audit_result.schema.json +4 -1
- package/schemas/audit_task.schema.json +3 -1
- package/schemas/coverage_matrix.schema.json +3 -3
- package/schemas/critical_flows.schema.json +6 -2
- package/schemas/file_disposition.schema.json +2 -2
- package/schemas/finding.schema.json +9 -4
- package/schemas/flow_coverage.schema.json +2 -2
- package/schemas/repo_manifest.schema.json +4 -4
- package/schemas/risk_register.schema.json +2 -2
- package/schemas/runtime_validation_report.schema.json +2 -2
- package/schemas/runtime_validation_tasks.schema.json +8 -2
- package/schemas/surface_manifest.schema.json +6 -3
- package/schemas/unit_manifest.schema.json +3 -2
- package/skills/audit-code/SKILL.md +5 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { claimFlowReviewBlocks } from "./flowPlanning.js";
|
|
1
2
|
import { isTrivialAuditPath } from "./trivialAudit.js";
|
|
2
|
-
|
|
3
|
+
import { LENS_ORDER } from "./unitBuilder.js";
|
|
4
|
+
function taskPriority(hasExternalSignal, lens, isCriticalFlow = false) {
|
|
5
|
+
if (isCriticalFlow) {
|
|
6
|
+
return lens === "security" || lens === "reliability" || lens === "correctness"
|
|
7
|
+
? "high"
|
|
8
|
+
: "medium";
|
|
9
|
+
}
|
|
3
10
|
if (hasExternalSignal &&
|
|
4
11
|
(lens === "security" || lens === "data_integrity" || lens === "reliability")) {
|
|
5
12
|
return "high";
|
|
@@ -39,68 +46,161 @@ function pickAnalyzerLens(category) {
|
|
|
39
46
|
return "correctness";
|
|
40
47
|
}
|
|
41
48
|
const DEFAULT_FILE_SPLIT_THRESHOLD = 3000;
|
|
42
|
-
|
|
49
|
+
function buildCoverageIndex(coverageMatrix) {
|
|
50
|
+
return new Map(coverageMatrix.files.map((file) => [file.path, file]));
|
|
51
|
+
}
|
|
52
|
+
function getExternalSignalPaths(externalAnalyzerResults) {
|
|
53
|
+
const results = Array.isArray(externalAnalyzerResults?.results)
|
|
54
|
+
? externalAnalyzerResults.results
|
|
55
|
+
: [];
|
|
56
|
+
return new Set(results
|
|
57
|
+
.map((item) => item && typeof item.path === "string" && item.path.length > 0
|
|
58
|
+
? item.path
|
|
59
|
+
: null)
|
|
60
|
+
.filter((path) => path !== null));
|
|
61
|
+
}
|
|
62
|
+
function getExternalSignalResults(externalAnalyzerResults) {
|
|
63
|
+
if (!Array.isArray(externalAnalyzerResults?.results)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return externalAnalyzerResults.results.filter((item) => Boolean(item) &&
|
|
67
|
+
typeof item.path === "string" &&
|
|
68
|
+
typeof item.category === "string" &&
|
|
69
|
+
typeof item.summary === "string" &&
|
|
70
|
+
typeof item.id === "string");
|
|
71
|
+
}
|
|
72
|
+
export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options = {}) {
|
|
43
73
|
const fileSplitThreshold = options.file_split_threshold ?? DEFAULT_FILE_SPLIT_THRESHOLD;
|
|
44
74
|
const allowed = new Set(options.limit_lenses ?? []);
|
|
45
75
|
const enforceLensFilter = allowed.size > 0;
|
|
46
76
|
const tasks = [];
|
|
47
77
|
const seen = new Set();
|
|
48
|
-
const externalPaths =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
const externalPaths = getExternalSignalPaths(options.external_analyzer_results);
|
|
79
|
+
const coverageByPath = new Map(coverageMatrix.files.map((file) => [file.path, file]));
|
|
80
|
+
const pendingByLens = new Map();
|
|
81
|
+
for (const file of coverageMatrix.files) {
|
|
82
|
+
if (file.audit_status === "excluded") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const lens of file.required_lenses) {
|
|
86
|
+
if (file.completed_lenses.includes(lens)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
55
89
|
if (enforceLensFilter && !allowed.has(lens)) {
|
|
56
90
|
continue;
|
|
57
91
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
|
|
61
|
-
const candidateFiles = unit.files.filter((filePath) => !isTrivialAuditPath(filePath, unitLineIndex[filePath] ?? 0, externalPaths.has(filePath)));
|
|
62
|
-
// Split files that are individually too large to group; everything else
|
|
63
|
-
// goes into one task so the agent can reason across file boundaries.
|
|
64
|
-
const oversizedFiles = fileSplitThreshold > 0
|
|
65
|
-
? candidateFiles.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
|
|
66
|
-
: [];
|
|
67
|
-
const normalFiles = candidateFiles.filter((f) => !oversizedFiles.includes(f));
|
|
68
|
-
// One task for all normal-sized files in this unit under this lens.
|
|
69
|
-
if (normalFiles.length > 0) {
|
|
70
|
-
const id = `${unit.unit_id}:${lens}`;
|
|
71
|
-
if (!seen.has(id)) {
|
|
72
|
-
seen.add(id);
|
|
73
|
-
tasks.push({
|
|
74
|
-
task_id: id,
|
|
75
|
-
unit_id: unit.unit_id,
|
|
76
|
-
pass_id: `pass:${lens}`,
|
|
77
|
-
lens,
|
|
78
|
-
file_paths: normalFiles,
|
|
79
|
-
rationale: `Audit ${unit.unit_id} (${normalFiles.length} file${normalFiles.length === 1 ? "" : "s"}) under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
|
|
80
|
-
priority,
|
|
81
|
-
tags,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
92
|
+
if (isTrivialAuditPath(file.path, unitLineIndex[file.path] ?? 0, externalPaths.has(file.path))) {
|
|
93
|
+
continue;
|
|
84
94
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
const pending = pendingByLens.get(lens) ?? new Set();
|
|
96
|
+
pending.add(file.path);
|
|
97
|
+
pendingByLens.set(lens, pending);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function addTaskBlock(params) {
|
|
101
|
+
const oversizedFiles = fileSplitThreshold > 0
|
|
102
|
+
? params.filePaths.filter((path) => (unitLineIndex[path] ?? 0) > fileSplitThreshold)
|
|
103
|
+
: [];
|
|
104
|
+
const oversizedSet = new Set(oversizedFiles);
|
|
105
|
+
const normalFiles = params.filePaths.filter((path) => !oversizedSet.has(path));
|
|
106
|
+
if (normalFiles.length > 0) {
|
|
107
|
+
const taskId = `${params.scopeId}:${params.lens}`;
|
|
108
|
+
if (!seen.has(taskId)) {
|
|
109
|
+
seen.add(taskId);
|
|
92
110
|
tasks.push({
|
|
93
|
-
task_id:
|
|
94
|
-
unit_id:
|
|
95
|
-
pass_id:
|
|
96
|
-
lens,
|
|
97
|
-
file_paths:
|
|
98
|
-
rationale:
|
|
99
|
-
priority:
|
|
100
|
-
tags:
|
|
111
|
+
task_id: taskId,
|
|
112
|
+
unit_id: params.unitId,
|
|
113
|
+
pass_id: params.passId,
|
|
114
|
+
lens: params.lens,
|
|
115
|
+
file_paths: normalFiles,
|
|
116
|
+
rationale: params.rationale(normalFiles, false),
|
|
117
|
+
priority: params.priority,
|
|
118
|
+
tags: params.tags.length > 0 ? params.tags : undefined,
|
|
101
119
|
});
|
|
102
120
|
}
|
|
103
121
|
}
|
|
122
|
+
for (const filePath of oversizedFiles) {
|
|
123
|
+
const taskId = `${params.scopeId}:${params.lens}:${filePath}`;
|
|
124
|
+
if (seen.has(taskId)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
seen.add(taskId);
|
|
128
|
+
tasks.push({
|
|
129
|
+
task_id: taskId,
|
|
130
|
+
unit_id: params.unitId,
|
|
131
|
+
pass_id: params.passId,
|
|
132
|
+
lens: params.lens,
|
|
133
|
+
file_paths: [filePath],
|
|
134
|
+
rationale: params.rationale([filePath], true),
|
|
135
|
+
priority: params.priority,
|
|
136
|
+
tags: params.tags.length > 0
|
|
137
|
+
? [...new Set([...params.tags, "large_file"])]
|
|
138
|
+
: ["large_file"],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const assigned = new Set();
|
|
143
|
+
const flowBlocks = options.critical_flows
|
|
144
|
+
? claimFlowReviewBlocks(options.critical_flows, pendingByLens, assigned)
|
|
145
|
+
: [];
|
|
146
|
+
for (const block of flowBlocks) {
|
|
147
|
+
const hasExternalSignal = block.file_paths.some((path) => externalPaths.has(path));
|
|
148
|
+
addTaskBlock({
|
|
149
|
+
scopeId: `flow:${block.flow_id}`,
|
|
150
|
+
unitId: `flow:${block.flow_id}`,
|
|
151
|
+
passId: `flow-pass:${block.lens}`,
|
|
152
|
+
lens: block.lens,
|
|
153
|
+
filePaths: block.file_paths,
|
|
154
|
+
priority: taskPriority(hasExternalSignal, block.lens, true),
|
|
155
|
+
tags: hasExternalSignal
|
|
156
|
+
? ["critical_flow", `critical_flow:${block.flow_id}`, "external_analyzer_signal"]
|
|
157
|
+
: ["critical_flow", `critical_flow:${block.flow_id}`],
|
|
158
|
+
rationale: (filePaths, splitFromBlock) => splitFromBlock
|
|
159
|
+
? `Audit ${filePaths[0]} (large file from critical flow ${block.flow_id}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`
|
|
160
|
+
: `Audit critical flow ${block.flow_id} (${filePaths.length} file${filePaths.length === 1 ? "" : "s"}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const groupedRemainders = new Map();
|
|
164
|
+
for (const lens of LENS_ORDER) {
|
|
165
|
+
const pendingPaths = pendingByLens.get(lens);
|
|
166
|
+
if (!pendingPaths || pendingPaths.size === 0) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const path of [...pendingPaths].sort((a, b) => a.localeCompare(b))) {
|
|
170
|
+
if (assigned.has(`${lens}:${path}`)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const record = coverageByPath.get(path);
|
|
174
|
+
const unitId = record?.unit_ids[0] ?? `review:${path.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
175
|
+
const key = `${lens}|${unitId}`;
|
|
176
|
+
const current = groupedRemainders.get(key) ?? {
|
|
177
|
+
lens,
|
|
178
|
+
unitId,
|
|
179
|
+
filePaths: [],
|
|
180
|
+
};
|
|
181
|
+
current.filePaths.push(path);
|
|
182
|
+
groupedRemainders.set(key, current);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const block of [...groupedRemainders.values()].sort((a, b) => {
|
|
186
|
+
const lensDelta = LENS_ORDER.indexOf(a.lens) - LENS_ORDER.indexOf(b.lens);
|
|
187
|
+
if (lensDelta !== 0)
|
|
188
|
+
return lensDelta;
|
|
189
|
+
return a.unitId.localeCompare(b.unitId);
|
|
190
|
+
})) {
|
|
191
|
+
const hasExternalSignal = block.filePaths.some((path) => externalPaths.has(path));
|
|
192
|
+
addTaskBlock({
|
|
193
|
+
scopeId: block.unitId,
|
|
194
|
+
unitId: block.unitId,
|
|
195
|
+
passId: `pass:${block.lens}`,
|
|
196
|
+
lens: block.lens,
|
|
197
|
+
filePaths: block.filePaths,
|
|
198
|
+
priority: taskPriority(hasExternalSignal, block.lens),
|
|
199
|
+
tags: hasExternalSignal ? ["external_analyzer_signal"] : [],
|
|
200
|
+
rationale: (filePaths, splitFromBlock) => splitFromBlock
|
|
201
|
+
? `Audit ${filePaths[0]} (large file split from ${block.unitId}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`
|
|
202
|
+
: `Audit ${block.unitId} (${filePaths.length} file${filePaths.length === 1 ? "" : "s"}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
|
|
203
|
+
});
|
|
104
204
|
}
|
|
105
205
|
return tasks.sort((a, b) => {
|
|
106
206
|
const priorityDelta = priorityRank(b.priority) - priorityRank(a.priority);
|
|
@@ -119,12 +219,13 @@ export function buildExternalSignalTasks(coverageMatrix, _unitLineIndex, externa
|
|
|
119
219
|
}
|
|
120
220
|
const tasks = [];
|
|
121
221
|
const seen = new Set();
|
|
122
|
-
|
|
222
|
+
const coverageByPath = buildCoverageIndex(coverageMatrix);
|
|
223
|
+
for (const result of getExternalSignalResults(externalAnalyzerResults)) {
|
|
123
224
|
const safeCategory = sanitizeField(result.category, 80);
|
|
124
225
|
const safePath = sanitizeField(result.path ?? "", 260);
|
|
125
226
|
const safeSummary = sanitizeField(result.summary ?? "", 200);
|
|
126
227
|
const lens = pickAnalyzerLens(safeCategory);
|
|
127
|
-
const coverage =
|
|
228
|
+
const coverage = coverageByPath.get(result.path);
|
|
128
229
|
if (!coverage || coverage.audit_status === "excluded") {
|
|
129
230
|
continue;
|
|
130
231
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type { RepoManifest, UnitManifest } from "../types.js";
|
|
1
|
+
import type { Lens, RepoManifest, UnitManifest } from "../types.js";
|
|
2
2
|
import type { FileDisposition } from "../types/disposition.js";
|
|
3
|
+
export declare const LENS_ORDER: Lens[];
|
|
4
|
+
export declare function deriveRequiredLensesForPath(path: string): Lens[];
|
|
3
5
|
export declare function buildUnitManifest(repoManifest: RepoManifest, disposition?: FileDisposition): UnitManifest;
|
|
@@ -60,20 +60,30 @@ function inferUnitId(path, kind) {
|
|
|
60
60
|
}
|
|
61
61
|
return `${kind}-${path.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
62
62
|
}
|
|
63
|
+
export const LENS_ORDER = [
|
|
64
|
+
"security",
|
|
65
|
+
"correctness",
|
|
66
|
+
"reliability",
|
|
67
|
+
"data_integrity",
|
|
68
|
+
"performance",
|
|
69
|
+
"operability",
|
|
70
|
+
"config_deployment",
|
|
71
|
+
"maintainability",
|
|
72
|
+
"tests",
|
|
73
|
+
];
|
|
63
74
|
function sortLenses(lenses) {
|
|
64
|
-
const order = [
|
|
65
|
-
"security",
|
|
66
|
-
"correctness",
|
|
67
|
-
"reliability",
|
|
68
|
-
"data_integrity",
|
|
69
|
-
"performance",
|
|
70
|
-
"operability",
|
|
71
|
-
"config_deployment",
|
|
72
|
-
"maintainability",
|
|
73
|
-
"tests",
|
|
74
|
-
];
|
|
75
75
|
const set = new Set(lenses);
|
|
76
|
-
return
|
|
76
|
+
return LENS_ORDER.filter((lens) => set.has(lens));
|
|
77
|
+
}
|
|
78
|
+
export function deriveRequiredLensesForPath(path) {
|
|
79
|
+
const assignment = bucketFile(path);
|
|
80
|
+
const required = new Set();
|
|
81
|
+
for (const bucket of assignment.buckets) {
|
|
82
|
+
for (const lens of LENS_MAP[bucket]) {
|
|
83
|
+
required.add(lens);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return sortLenses(required);
|
|
77
87
|
}
|
|
78
88
|
function inferCriticalFlows(files, requiredLenses) {
|
|
79
89
|
const flows = new Set();
|
|
@@ -123,10 +133,8 @@ export function buildUnitManifest(repoManifest, disposition) {
|
|
|
123
133
|
}
|
|
124
134
|
const assignment = bucketFile(file.path);
|
|
125
135
|
const required = new Set(existing.required_lenses);
|
|
126
|
-
for (const
|
|
127
|
-
|
|
128
|
-
required.add(lens);
|
|
129
|
-
}
|
|
136
|
+
for (const lens of deriveRequiredLensesForPath(file.path)) {
|
|
137
|
+
required.add(lens);
|
|
130
138
|
}
|
|
131
139
|
existing.required_lenses = sortLenses(required);
|
|
132
140
|
const riskScore = new Set(assignment.buckets).size +
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type WorkerTask } from "../types/workerSession.js";
|
|
2
2
|
export declare function renderWorkerPrompt(task: WorkerTask): string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { usesDeferredWorkerCommand, } from "../types/workerSession.js";
|
|
2
|
+
function renderArgv(task) {
|
|
3
|
+
return JSON.stringify(task.worker_command);
|
|
3
4
|
}
|
|
4
5
|
export function renderWorkerPrompt(task) {
|
|
5
|
-
const
|
|
6
|
+
const commandArgv = renderArgv(task);
|
|
6
7
|
if (task.preferred_executor === "agent" && task.audit_results_path) {
|
|
7
8
|
const tasksPath = task.pending_audit_tasks_path ??
|
|
8
9
|
`${task.artifacts_dir}/audit_tasks.json`;
|
|
@@ -29,14 +30,17 @@ export function renderWorkerPrompt(task) {
|
|
|
29
30
|
" Example evidence entry: src/foo.ts:42 - variable overwritten before use",
|
|
30
31
|
" Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
|
|
31
32
|
" Low-priority tasks still require a real review. Use findings: [] only when you genuinely found nothing notable.",
|
|
33
|
+
task.timeout_ms
|
|
34
|
+
? ` Time budget for this task: ${task.timeout_ms} ms.`
|
|
35
|
+
: " Keep the task bounded to the assigned files only.",
|
|
32
36
|
`Reference schema: ${task.artifacts_dir}/dispatch/audit-result.schema.json`,
|
|
33
37
|
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
34
38
|
];
|
|
35
|
-
if (task
|
|
36
|
-
lines.push("", "Stop after writing the results file.");
|
|
39
|
+
if (usesDeferredWorkerCommand(task)) {
|
|
40
|
+
lines.push("", "This run is using deferred worker-command ingestion.", "Do not execute worker_command in this session.", "Stop after writing the results file.");
|
|
37
41
|
}
|
|
38
42
|
else {
|
|
39
|
-
lines.push("", "Then
|
|
43
|
+
lines.push("", "Then execute the worker_command array from task.json exactly as written.", "Preserve argv boundaries instead of reconstructing shell quoting.", `worker_command argv JSON: ${commandArgv}`, "Stop after the command completes.");
|
|
40
44
|
}
|
|
41
45
|
return lines.join("\n");
|
|
42
46
|
}
|
|
@@ -46,10 +50,14 @@ export function renderWorkerPrompt(task) {
|
|
|
46
50
|
`Repository root: ${task.repo_root}`,
|
|
47
51
|
`Obligation: ${task.obligation_id ?? "unknown"}`,
|
|
48
52
|
`Executor: ${task.preferred_executor}`,
|
|
49
|
-
"Execute the
|
|
50
|
-
|
|
53
|
+
"Execute the worker_command array from task.json exactly as written.",
|
|
54
|
+
"Preserve argv boundaries instead of reconstructing shell quoting.",
|
|
55
|
+
`worker_command argv JSON: ${commandArgv}`,
|
|
51
56
|
"Do not continue the audit recursively.",
|
|
52
57
|
"Do not choose another task.",
|
|
58
|
+
task.timeout_ms
|
|
59
|
+
? `The worker command is budgeted for ${task.timeout_ms} ms.`
|
|
60
|
+
: "If the command hangs or fails, stop and let the supervisor handle it.",
|
|
53
61
|
`The command must write the worker result JSON to: ${task.result_path}`,
|
|
54
62
|
"After the command completes, stop.",
|
|
55
63
|
].join("\n");
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { FreshSessionProvider, LaunchFreshSessionInput } from "./types.js";
|
|
2
2
|
import type { ClaudeCodeConfig } from "../types/sessionConfig.js";
|
|
3
|
+
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
4
|
+
export declare const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE: string;
|
|
3
5
|
export declare class ClaudeCodeProvider implements FreshSessionProvider {
|
|
4
6
|
name: string;
|
|
5
7
|
private readonly config;
|
|
6
|
-
|
|
8
|
+
private readonly launchCommand;
|
|
9
|
+
constructor(config?: ClaudeCodeConfig, launchCommand?: typeof spawnLoggedCommand);
|
|
7
10
|
launch(input: LaunchFreshSessionInput): Promise<import("./types.js").LaunchFreshSessionResult>;
|
|
8
11
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
3
|
+
export const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE = "claude-code provider cannot be used inside an active Claude Code session. " +
|
|
4
|
+
'Set provider to "local-subprocess" in .audit-artifacts/session-config.json, ' +
|
|
5
|
+
"then run /audit-code conversationally and follow the dispatch prompts manually.";
|
|
3
6
|
export class ClaudeCodeProvider {
|
|
4
7
|
name = "claude-code";
|
|
5
8
|
config;
|
|
6
|
-
|
|
9
|
+
launchCommand;
|
|
10
|
+
constructor(config = {}, launchCommand = spawnLoggedCommand) {
|
|
7
11
|
this.config = config;
|
|
12
|
+
this.launchCommand = launchCommand;
|
|
8
13
|
}
|
|
9
14
|
async launch(input) {
|
|
10
15
|
if (process.env.CLAUDECODE) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
"Set provider to \"local-subprocess\" in .audit-artifacts/session-config.json, " +
|
|
13
|
-
"then run /audit-code conversationally and follow the dispatch prompts manually.");
|
|
16
|
+
throw new Error(ACTIVE_CLAUDE_CODE_SESSION_MESSAGE);
|
|
14
17
|
}
|
|
15
18
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
16
19
|
const command = this.config.command ?? "claude";
|
|
@@ -20,6 +23,6 @@ export class ClaudeCodeProvider {
|
|
|
20
23
|
...(this.config.extra_args ?? []),
|
|
21
24
|
"--dangerously-skip-permissions",
|
|
22
25
|
];
|
|
23
|
-
return await
|
|
26
|
+
return await this.launchCommand(command, args, input);
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { FreshSessionProvider, LaunchFreshSessionInput } from "./types.js";
|
|
2
|
+
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
3
|
+
export declare const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
2
4
|
export declare class LocalSubprocessProvider implements FreshSessionProvider {
|
|
3
5
|
name: string;
|
|
6
|
+
private readonly launchCommand;
|
|
7
|
+
constructor(launchCommand?: typeof spawnLoggedCommand);
|
|
4
8
|
launch(input: LaunchFreshSessionInput): Promise<import("./types.js").LaunchFreshSessionResult>;
|
|
5
9
|
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { readJsonFile } from "../io/json.js";
|
|
2
2
|
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
3
|
+
export const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
3
4
|
export class LocalSubprocessProvider {
|
|
4
5
|
name = "local-subprocess";
|
|
6
|
+
launchCommand;
|
|
7
|
+
constructor(launchCommand = spawnLoggedCommand) {
|
|
8
|
+
this.launchCommand = launchCommand;
|
|
9
|
+
}
|
|
5
10
|
async launch(input) {
|
|
6
11
|
const task = await readJsonFile(input.taskPath);
|
|
7
12
|
if (!task.worker_command.length) {
|
|
8
|
-
throw new Error(
|
|
13
|
+
throw new Error(MISSING_WORKER_COMMAND_MESSAGE);
|
|
9
14
|
}
|
|
10
15
|
const [command, ...args] = task.worker_command;
|
|
11
|
-
return await
|
|
16
|
+
return await this.launchCommand(command, args, input);
|
|
12
17
|
}
|
|
13
18
|
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
1
3
|
import type { LaunchFreshSessionInput, LaunchFreshSessionResult } from "./types.js";
|
|
2
|
-
|
|
4
|
+
interface SpawnLoggedCommandOptions {
|
|
5
|
+
createWriteStream?: typeof createWriteStream;
|
|
6
|
+
spawn?: typeof spawn;
|
|
7
|
+
killGraceMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function spawnLoggedCommand(command: string, args: string[], input: LaunchFreshSessionInput, env?: Record<string, string>, options?: SpawnLoggedCommandOptions): Promise<LaunchFreshSessionResult>;
|
|
10
|
+
export {};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
const TERMINATION_SIGNAL = "SIGTERM";
|
|
4
|
+
const FORCE_KILL_SIGNAL = "SIGKILL";
|
|
5
|
+
const FORCE_KILL_GRACE_MS = 1_000;
|
|
3
6
|
function tee(write, chunk) {
|
|
4
7
|
write.write(chunk);
|
|
5
8
|
}
|
|
@@ -7,22 +10,77 @@ function tee(write, chunk) {
|
|
|
7
10
|
// does not consult PATH for executables without a shell. Callers should use
|
|
8
11
|
// `platformCommand()` (scripts/smoke-packaged-audit-code.mjs) or similar to
|
|
9
12
|
// supply the correct command form for the host OS.
|
|
10
|
-
export async function spawnLoggedCommand(command, args, input, env) {
|
|
13
|
+
export async function spawnLoggedCommand(command, args, input, env, options = {}) {
|
|
14
|
+
const openWriteStream = options.createWriteStream ?? createWriteStream;
|
|
15
|
+
const spawnProcess = options.spawn ?? spawn;
|
|
16
|
+
const killGraceMs = options.killGraceMs ?? FORCE_KILL_GRACE_MS;
|
|
11
17
|
return await new Promise((resolve, reject) => {
|
|
12
|
-
const stdoutLog =
|
|
13
|
-
const stderrLog =
|
|
18
|
+
const stdoutLog = openWriteStream(input.stdoutPath, { flags: "a" });
|
|
19
|
+
const stderrLog = openWriteStream(input.stderrPath, { flags: "a" });
|
|
14
20
|
const startedAt = Date.now();
|
|
15
21
|
let timedOut = false;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
let settled = false;
|
|
23
|
+
let child = null;
|
|
24
|
+
let timer;
|
|
25
|
+
let heartbeat;
|
|
26
|
+
let forceKillTimer;
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (timer) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
if (heartbeat) {
|
|
32
|
+
clearInterval(heartbeat);
|
|
33
|
+
}
|
|
34
|
+
if (forceKillTimer) {
|
|
35
|
+
clearTimeout(forceKillTimer);
|
|
36
|
+
}
|
|
37
|
+
stdoutLog.end();
|
|
38
|
+
stderrLog.end();
|
|
39
|
+
};
|
|
40
|
+
const settle = (callback) => {
|
|
41
|
+
if (settled) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
settled = true;
|
|
45
|
+
cleanup();
|
|
46
|
+
callback();
|
|
47
|
+
};
|
|
48
|
+
const fail = (error) => {
|
|
49
|
+
if (child && !child.killed) {
|
|
50
|
+
child.kill(FORCE_KILL_SIGNAL);
|
|
51
|
+
}
|
|
52
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
settle(() => reject(normalized));
|
|
54
|
+
};
|
|
55
|
+
stdoutLog.on("error", fail);
|
|
56
|
+
stderrLog.on("error", fail);
|
|
57
|
+
let spawnedChild;
|
|
58
|
+
try {
|
|
59
|
+
spawnedChild = spawnProcess(command, args, {
|
|
60
|
+
cwd: input.repoRoot,
|
|
61
|
+
env: { ...process.env, ...env },
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
child = spawnedChild;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
fail(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!spawnedChild.stdout || !spawnedChild.stderr) {
|
|
71
|
+
fail(new Error(`Fresh session spawn for run ${input.runId} did not provide pipe-backed stdout/stderr streams.`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
timer = setTimeout(() => {
|
|
22
75
|
timedOut = true;
|
|
23
|
-
|
|
76
|
+
spawnedChild.kill(TERMINATION_SIGNAL);
|
|
77
|
+
forceKillTimer = setTimeout(() => {
|
|
78
|
+
if (!settled) {
|
|
79
|
+
spawnedChild.kill(FORCE_KILL_SIGNAL);
|
|
80
|
+
}
|
|
81
|
+
}, killGraceMs);
|
|
24
82
|
}, input.timeoutMs);
|
|
25
|
-
|
|
83
|
+
heartbeat = setInterval(() => {
|
|
26
84
|
const elapsedMs = Date.now() - startedAt;
|
|
27
85
|
const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
|
|
28
86
|
tee(stderrLog, message);
|
|
@@ -30,40 +88,30 @@ export async function spawnLoggedCommand(command, args, input, env) {
|
|
|
30
88
|
process.stderr.write(message);
|
|
31
89
|
}
|
|
32
90
|
}, 30_000);
|
|
33
|
-
|
|
91
|
+
spawnedChild.stdout.on("data", (chunk) => {
|
|
34
92
|
tee(stdoutLog, chunk);
|
|
35
93
|
if (input.uiMode === "visible") {
|
|
36
94
|
process.stdout.write(chunk);
|
|
37
95
|
}
|
|
38
96
|
});
|
|
39
|
-
|
|
97
|
+
spawnedChild.stderr.on("data", (chunk) => {
|
|
40
98
|
tee(stderrLog, chunk);
|
|
41
99
|
if (input.uiMode === "visible") {
|
|
42
100
|
process.stderr.write(chunk);
|
|
43
101
|
}
|
|
44
102
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
clearInterval(heartbeat);
|
|
48
|
-
stdoutLog.end();
|
|
49
|
-
stderrLog.end();
|
|
50
|
-
reject(error);
|
|
51
|
-
});
|
|
52
|
-
child.on("exit", (code, signal) => {
|
|
53
|
-
clearTimeout(timer);
|
|
54
|
-
clearInterval(heartbeat);
|
|
55
|
-
stdoutLog.end();
|
|
56
|
-
stderrLog.end();
|
|
103
|
+
spawnedChild.on("error", fail);
|
|
104
|
+
spawnedChild.on("exit", (code, signal) => {
|
|
57
105
|
if (timedOut) {
|
|
58
|
-
reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`));
|
|
106
|
+
settle(() => reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`)));
|
|
59
107
|
return;
|
|
60
108
|
}
|
|
61
|
-
resolve({
|
|
109
|
+
settle(() => resolve({
|
|
62
110
|
accepted: true,
|
|
63
|
-
processId:
|
|
111
|
+
processId: spawnedChild.pid,
|
|
64
112
|
exitCode: code,
|
|
65
113
|
signal,
|
|
66
|
-
});
|
|
114
|
+
}));
|
|
67
115
|
});
|
|
68
116
|
});
|
|
69
117
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
|
|
2
|
+
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
2
3
|
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
3
4
|
import type { GraphBundle } from "../types/graph.js";
|
|
4
5
|
import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
|
|
@@ -23,5 +24,6 @@ export declare function buildAuditReportModel(params: {
|
|
|
23
24
|
criticalFlows?: CriticalFlowManifest;
|
|
24
25
|
coverageMatrix?: CoverageMatrix;
|
|
25
26
|
runtimeValidationReport?: RuntimeValidationReport;
|
|
27
|
+
externalAnalyzerResults?: ExternalAnalyzerResults;
|
|
26
28
|
}): AuditReportModel;
|
|
27
29
|
export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { buildWorkBlocks } from "./workBlocks.js";
|
|
2
2
|
import { mergeFindings } from "./mergeFindings.js";
|
|
3
|
-
function
|
|
3
|
+
function countBy(items, selectKey) {
|
|
4
4
|
const breakdown = {};
|
|
5
|
-
for (const
|
|
6
|
-
|
|
5
|
+
for (const item of items) {
|
|
6
|
+
const key = selectKey(item);
|
|
7
|
+
if (!key) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
breakdown[key] = (breakdown[key] ?? 0) + 1;
|
|
7
11
|
}
|
|
8
12
|
return breakdown;
|
|
9
13
|
}
|
|
14
|
+
function severityBreakdown(findings) {
|
|
15
|
+
return countBy(findings, (finding) => finding.severity);
|
|
16
|
+
}
|
|
10
17
|
function runtimeStatusBreakdown(report) {
|
|
11
|
-
|
|
12
|
-
for (const result of report?.results ?? []) {
|
|
13
|
-
breakdown[result.status] = (breakdown[result.status] ?? 0) + 1;
|
|
14
|
-
}
|
|
15
|
-
return breakdown;
|
|
18
|
+
return countBy(report?.results ?? [], (result) => result.status);
|
|
16
19
|
}
|
|
17
20
|
function coverageSummary(coverage) {
|
|
18
21
|
const files = coverage?.files ?? [];
|
|
@@ -29,7 +32,7 @@ function formatSeverityList(summary) {
|
|
|
29
32
|
return parts.length > 0 ? parts.join(", ") : "none";
|
|
30
33
|
}
|
|
31
34
|
export function buildAuditReportModel(params) {
|
|
32
|
-
const findings = mergeFindings(params.results, params.runtimeValidationReport);
|
|
35
|
+
const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults);
|
|
33
36
|
const workBlocks = buildWorkBlocks({
|
|
34
37
|
findings,
|
|
35
38
|
unitManifest: params.unitManifest,
|