auditor-lambda 0.3.30 → 0.3.33
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 +2 -1
- package/audit-code-wrapper-lib.mjs +208 -198
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +65 -101
- package/dist/extractors/risk.js +6 -4
- package/dist/io/artifacts.d.ts +2 -0
- package/dist/io/artifacts.js +1 -0
- package/dist/io/toolingManifest.d.ts +1 -0
- package/dist/io/toolingManifest.js +1 -1
- package/dist/mcp/server.d.ts +71 -0
- package/dist/mcp/server.js +261 -222
- package/dist/orchestrator/artifactFreshness.d.ts +4 -0
- package/dist/orchestrator/artifactFreshness.js +45 -0
- package/dist/orchestrator/artifactMetadata.js +2 -51
- package/dist/orchestrator/dependencyMap.js +14 -0
- package/dist/orchestrator/internalExecutors.js +8 -0
- package/dist/orchestrator/staleness.js +2 -46
- package/dist/orchestrator/state.js +1 -1
- package/dist/orchestrator/syntaxResolutionExecutor.js +121 -13
- package/dist/orchestrator/unitBuilder.js +2 -1
- package/dist/providers/spawnLoggedCommand.js +71 -18
- package/dist/providers/types.d.ts +5 -0
- package/dist/quota/scheduler.js +10 -2
- package/dist/quota/state.js +6 -2
- package/dist/supervisor/operatorHandoff.js +1 -1
- package/dist/types/externalAnalyzer.d.ts +10 -0
- package/dist/types/sessionConfig.d.ts +1 -0
- package/dist/types/workerSession.js +1 -2
- package/dist/validation/artifacts.js +36 -0
- package/dist/validation/sessionConfig.js +4 -0
- package/package.json +1 -1
- package/schemas/audit_task.schema.json +2 -2
- package/schemas/risk_register.schema.json +1 -1
- package/schemas/unit_manifest.schema.json +2 -1
- package/scripts/postinstall.mjs +10 -41
- package/skills/audit-code/audit-code.prompt.md +5 -0
|
@@ -62,6 +62,20 @@ export const ARTIFACT_DEPENDENCY_MAP = {
|
|
|
62
62
|
"runtime_validation_report.json",
|
|
63
63
|
"audit-report.md",
|
|
64
64
|
],
|
|
65
|
+
"external_analyzer_results.json": [
|
|
66
|
+
"coverage_matrix.json",
|
|
67
|
+
"flow_coverage.json",
|
|
68
|
+
"audit_tasks.json",
|
|
69
|
+
"audit_plan_metrics.json",
|
|
70
|
+
"review_packets.json",
|
|
71
|
+
"requeue_tasks.json",
|
|
72
|
+
"runtime_validation_tasks.json",
|
|
73
|
+
"runtime_validation_report.json",
|
|
74
|
+
"audit-report.md",
|
|
75
|
+
],
|
|
76
|
+
"syntax_resolution_status.json": [
|
|
77
|
+
"audit-report.md",
|
|
78
|
+
],
|
|
65
79
|
"audit_results.jsonl": [
|
|
66
80
|
"coverage_matrix.json",
|
|
67
81
|
"flow_coverage.json",
|
|
@@ -426,6 +426,14 @@ export function runExternalAnalyzerImportExecutor(bundle, externalResults) {
|
|
|
426
426
|
updated: {
|
|
427
427
|
...bundle,
|
|
428
428
|
external_analyzer_results: externalResults,
|
|
429
|
+
coverage_matrix: undefined,
|
|
430
|
+
flow_coverage: undefined,
|
|
431
|
+
runtime_validation_tasks: undefined,
|
|
432
|
+
runtime_validation_report: undefined,
|
|
433
|
+
audit_tasks: undefined,
|
|
434
|
+
audit_plan_metrics: undefined,
|
|
435
|
+
review_packets: undefined,
|
|
436
|
+
requeue_tasks: undefined,
|
|
429
437
|
audit_report: undefined,
|
|
430
438
|
},
|
|
431
439
|
artifacts_written: ["external_analyzer_results.json"],
|
|
@@ -1,56 +1,12 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { getArtifactValue } from "../io/artifacts.js";
|
|
3
2
|
import { ARTIFACT_DEPENDENCY_MAP } from "./dependencyMap.js";
|
|
4
3
|
import { present } from "./artifactMetadata.js";
|
|
5
|
-
|
|
6
|
-
if (value === undefined)
|
|
7
|
-
return "null";
|
|
8
|
-
if (value === null || typeof value !== "object")
|
|
9
|
-
return JSON.stringify(value);
|
|
10
|
-
if (Array.isArray(value))
|
|
11
|
-
return `[${value.map((item) => stableStringify(item ?? null)).join(",")}]`;
|
|
12
|
-
const entries = Object.entries(value)
|
|
13
|
-
.filter(([, item]) => item !== undefined)
|
|
14
|
-
.sort(([a], [b]) => a.localeCompare(b));
|
|
15
|
-
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
|
|
16
|
-
}
|
|
17
|
-
function normalizeForMetadataHash(artifactName, value) {
|
|
18
|
-
if (artifactName === "repo_manifest.json" &&
|
|
19
|
-
value &&
|
|
20
|
-
typeof value === "object" &&
|
|
21
|
-
!Array.isArray(value)) {
|
|
22
|
-
const record = value;
|
|
23
|
-
const { generated_at: _generatedAt, ...rest } = record;
|
|
24
|
-
return rest;
|
|
25
|
-
}
|
|
26
|
-
if (artifactName === "tooling_manifest.json" &&
|
|
27
|
-
value &&
|
|
28
|
-
typeof value === "object" &&
|
|
29
|
-
!Array.isArray(value)) {
|
|
30
|
-
const record = value;
|
|
31
|
-
const { generated_at: _generatedAt, ...rest } = record;
|
|
32
|
-
return rest;
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
4
|
+
import { buildReverseDependencyMap, hashArtifactValue, stableStringify, } from "./artifactFreshness.js";
|
|
36
5
|
function computeContentHash(artifactName, bundle) {
|
|
37
6
|
const value = getArtifactValue(bundle, artifactName);
|
|
38
7
|
if (value === undefined || value === null)
|
|
39
8
|
return undefined;
|
|
40
|
-
return
|
|
41
|
-
.update(stableStringify(normalizeForMetadataHash(artifactName, value)))
|
|
42
|
-
.digest("hex");
|
|
43
|
-
}
|
|
44
|
-
function buildReverseDependencyMap() {
|
|
45
|
-
const reverse = {};
|
|
46
|
-
for (const [upstream, downstreamList] of Object.entries(ARTIFACT_DEPENDENCY_MAP)) {
|
|
47
|
-
reverse[upstream] ??= [];
|
|
48
|
-
for (const downstream of downstreamList) {
|
|
49
|
-
reverse[downstream] ??= [];
|
|
50
|
-
reverse[downstream].push(upstream);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return reverse;
|
|
9
|
+
return hashArtifactValue(artifactName, value);
|
|
54
10
|
}
|
|
55
11
|
const REVERSE_DEPENDENCY_MAP = buildReverseDependencyMap();
|
|
56
12
|
export function computeStaleArtifacts(bundle) {
|
|
@@ -16,7 +16,7 @@ export function deriveAuditState(bundle) {
|
|
|
16
16
|
obligations.push(obligation("repo_manifest", has(bundle.repo_manifest) ? "satisfied" : "missing"));
|
|
17
17
|
obligations.push(obligation("file_disposition", staleOrSatisfied(staleArtifacts, ["file_disposition.json"], has(bundle.file_disposition))));
|
|
18
18
|
obligations.push(obligation("auto_fixes_applied", staleOrSatisfied(staleArtifacts, ["file_disposition.json"], has(bundle.auto_fixes_applied))));
|
|
19
|
-
obligations.push(obligation("syntax_resolved", staleOrSatisfied(staleArtifacts, ["auto_fixes_applied.json"], has(bundle.
|
|
19
|
+
obligations.push(obligation("syntax_resolved", staleOrSatisfied(staleArtifacts, ["auto_fixes_applied.json", "syntax_resolution_status.json"], has(bundle.syntax_resolution_status))));
|
|
20
20
|
const structureReady = has(bundle.unit_manifest) &&
|
|
21
21
|
has(bundle.surface_manifest) &&
|
|
22
22
|
has(bundle.graph_bundle) &&
|
|
@@ -39,6 +39,9 @@ function hasEslintConfig(root) {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
function snippet(value) {
|
|
43
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
44
|
+
}
|
|
42
45
|
function runTsc(root) {
|
|
43
46
|
const results = [];
|
|
44
47
|
const command = runFirstAvailableCommand(root, [
|
|
@@ -46,7 +49,17 @@ function runTsc(root) {
|
|
|
46
49
|
{ command: "tsc", args: ["--noEmit"], display: "tsc --noEmit" },
|
|
47
50
|
]);
|
|
48
51
|
if (!command || command.error) {
|
|
49
|
-
return
|
|
52
|
+
return {
|
|
53
|
+
results,
|
|
54
|
+
status: {
|
|
55
|
+
tool: "tsc",
|
|
56
|
+
command: command?.candidate.display,
|
|
57
|
+
resolved: Boolean(command),
|
|
58
|
+
status: command?.error ? "spawn_error" : "not_resolved",
|
|
59
|
+
exit_code: command?.exitCode,
|
|
60
|
+
error: command?.error?.message,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
50
63
|
}
|
|
51
64
|
const output = [command.stdout, command.stderr].filter(Boolean).join("\n");
|
|
52
65
|
const lines = output.split("\n");
|
|
@@ -65,17 +78,54 @@ function runTsc(root) {
|
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
if (command.exitCode === 0 && output.trim().length === 0) {
|
|
68
|
-
return
|
|
81
|
+
return {
|
|
82
|
+
results,
|
|
83
|
+
status: {
|
|
84
|
+
tool: "tsc",
|
|
85
|
+
command: command.candidate.display,
|
|
86
|
+
resolved: true,
|
|
87
|
+
status: "success",
|
|
88
|
+
exit_code: command.exitCode,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
69
91
|
}
|
|
70
92
|
if (results.length === 0 && output.trim().length > 0) {
|
|
71
|
-
|
|
93
|
+
const outputSnippet = snippet(output);
|
|
94
|
+
process.stderr.write(`[syntax-resolution] tsc output could not be parsed: ${outputSnippet}\n`);
|
|
95
|
+
return {
|
|
96
|
+
results,
|
|
97
|
+
status: {
|
|
98
|
+
tool: "tsc",
|
|
99
|
+
command: command.candidate.display,
|
|
100
|
+
resolved: true,
|
|
101
|
+
status: "parse_error",
|
|
102
|
+
exit_code: command.exitCode,
|
|
103
|
+
output_snippet: outputSnippet,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
72
106
|
}
|
|
73
|
-
return
|
|
107
|
+
return {
|
|
108
|
+
results,
|
|
109
|
+
status: {
|
|
110
|
+
tool: "tsc",
|
|
111
|
+
command: command.candidate.display,
|
|
112
|
+
resolved: true,
|
|
113
|
+
status: results.length > 0 ? "findings" : "failed",
|
|
114
|
+
exit_code: command.exitCode,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
74
117
|
}
|
|
75
118
|
function runEslint(root) {
|
|
76
119
|
const results = [];
|
|
77
120
|
if (!hasEslintConfig(root)) {
|
|
78
|
-
return
|
|
121
|
+
return {
|
|
122
|
+
results,
|
|
123
|
+
status: {
|
|
124
|
+
tool: "eslint",
|
|
125
|
+
resolved: false,
|
|
126
|
+
status: "skipped",
|
|
127
|
+
},
|
|
128
|
+
};
|
|
79
129
|
}
|
|
80
130
|
const command = runFirstAvailableCommand(root, [
|
|
81
131
|
...resolveNodeTool(root, join("node_modules", "eslint", "bin", "eslint.js"), [".", "--ext", ".ts,.js,.tsx,.jsx", "--format", "json"], "eslint . --ext .ts,.js,.tsx,.jsx --format json"),
|
|
@@ -86,11 +136,30 @@ function runEslint(root) {
|
|
|
86
136
|
},
|
|
87
137
|
]);
|
|
88
138
|
if (!command || command.error) {
|
|
89
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
results,
|
|
141
|
+
status: {
|
|
142
|
+
tool: "eslint",
|
|
143
|
+
command: command?.candidate.display,
|
|
144
|
+
resolved: Boolean(command),
|
|
145
|
+
status: command?.error ? "spawn_error" : "not_resolved",
|
|
146
|
+
exit_code: command?.exitCode,
|
|
147
|
+
error: command?.error?.message,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
90
150
|
}
|
|
91
151
|
const output = [command.stdout, command.stderr].filter(Boolean).join("\n").trim();
|
|
92
152
|
if (output.length === 0) {
|
|
93
|
-
return
|
|
153
|
+
return {
|
|
154
|
+
results,
|
|
155
|
+
status: {
|
|
156
|
+
tool: "eslint",
|
|
157
|
+
command: command.candidate.display,
|
|
158
|
+
resolved: true,
|
|
159
|
+
status: "success",
|
|
160
|
+
exit_code: command.exitCode,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
94
163
|
}
|
|
95
164
|
try {
|
|
96
165
|
const parsed = JSON.parse(output);
|
|
@@ -111,18 +180,44 @@ function runEslint(root) {
|
|
|
111
180
|
}
|
|
112
181
|
}
|
|
113
182
|
catch {
|
|
114
|
-
|
|
183
|
+
const outputSnippet = snippet(output);
|
|
184
|
+
process.stderr.write(`[syntax-resolution] eslint output could not be parsed: ${outputSnippet}\n`);
|
|
185
|
+
return {
|
|
186
|
+
results,
|
|
187
|
+
status: {
|
|
188
|
+
tool: "eslint",
|
|
189
|
+
command: command.candidate.display,
|
|
190
|
+
resolved: true,
|
|
191
|
+
status: "parse_error",
|
|
192
|
+
exit_code: command.exitCode,
|
|
193
|
+
output_snippet: outputSnippet,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
115
196
|
}
|
|
116
|
-
return
|
|
197
|
+
return {
|
|
198
|
+
results,
|
|
199
|
+
status: {
|
|
200
|
+
tool: "eslint",
|
|
201
|
+
command: command.candidate.display,
|
|
202
|
+
resolved: true,
|
|
203
|
+
status: results.length > 0 ? "findings" : "success",
|
|
204
|
+
exit_code: command.exitCode,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
117
207
|
}
|
|
118
208
|
export function runSyntaxResolutionExecutor(bundle, root) {
|
|
119
209
|
const items = [];
|
|
210
|
+
const toolStatuses = [];
|
|
120
211
|
if (hasTypeScriptConfig(root) &&
|
|
121
212
|
bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts"))) {
|
|
122
|
-
|
|
213
|
+
const tsc = runTsc(root);
|
|
214
|
+
items.push(...tsc.results);
|
|
215
|
+
toolStatuses.push(tsc.status);
|
|
123
216
|
}
|
|
124
217
|
if (bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts") || f.path.endsWith(".js"))) {
|
|
125
|
-
|
|
218
|
+
const eslint = runEslint(root);
|
|
219
|
+
items.push(...eslint.results);
|
|
220
|
+
toolStatuses.push(eslint.status);
|
|
126
221
|
}
|
|
127
222
|
const existing = bundle.external_analyzer_results?.results ?? [];
|
|
128
223
|
const merged = [...existing, ...items];
|
|
@@ -139,13 +234,26 @@ export function runSyntaxResolutionExecutor(bundle, root) {
|
|
|
139
234
|
const resultsArtifact = {
|
|
140
235
|
tool: "syntax_resolution_executor",
|
|
141
236
|
results: deduped,
|
|
237
|
+
tool_statuses: toolStatuses,
|
|
142
238
|
};
|
|
239
|
+
const diagnosticCount = toolStatuses.filter((status) => ["not_resolved", "spawn_error", "parse_error", "failed"].includes(status.status)).length;
|
|
143
240
|
return {
|
|
144
241
|
updated: {
|
|
145
242
|
...bundle,
|
|
146
243
|
external_analyzer_results: resultsArtifact,
|
|
244
|
+
syntax_resolution_status: {
|
|
245
|
+
tool: "syntax_resolution_executor",
|
|
246
|
+
completed_at: new Date().toISOString(),
|
|
247
|
+
tool_statuses: toolStatuses,
|
|
248
|
+
},
|
|
147
249
|
},
|
|
148
|
-
artifacts_written: [
|
|
149
|
-
|
|
250
|
+
artifacts_written: [
|
|
251
|
+
"external_analyzer_results.json",
|
|
252
|
+
"syntax_resolution_status.json",
|
|
253
|
+
],
|
|
254
|
+
progress_summary: `Phase 2 Syntax Resolution complete. Extracted ${items.length} unfixable syntax/lint errors` +
|
|
255
|
+
(diagnosticCount > 0
|
|
256
|
+
? ` with ${diagnosticCount} analyzer diagnostic(s).`
|
|
257
|
+
: ", triggering high-priority LLM resolution tasks."),
|
|
150
258
|
};
|
|
151
259
|
}
|
|
@@ -15,6 +15,7 @@ const LENS_MAP = {
|
|
|
15
15
|
generated_vendor: ["maintainability"],
|
|
16
16
|
unknown: ["correctness"],
|
|
17
17
|
};
|
|
18
|
+
const MAX_RISK_SCORE = 10;
|
|
18
19
|
function inferUnitKind(path, isBrowserExtensionProject = false) {
|
|
19
20
|
if (isBrowserExtensionProject) {
|
|
20
21
|
const extensionKind = inferBrowserExtensionUnitKind(path);
|
|
@@ -171,7 +172,7 @@ export function buildUnitManifest(repoManifest, disposition) {
|
|
|
171
172
|
(assignment.buckets.includes("security_sensitive") ? 3 : 0) +
|
|
172
173
|
(assignment.buckets.includes("interface") ? 1 : 0) +
|
|
173
174
|
(assignment.buckets.includes("data_layer") ? 1 : 0);
|
|
174
|
-
existing.risk_score = Math.max(existing.risk_score ?? 0, riskScore);
|
|
175
|
+
existing.risk_score = Math.min(MAX_RISK_SCORE, Math.max(existing.risk_score ?? 0, riskScore));
|
|
175
176
|
existing.files = existing.files.sort((a, b) => a.localeCompare(b));
|
|
176
177
|
existing.critical_flows = inferCriticalFlows(existing.files, existing.required_lenses);
|
|
177
178
|
units.set(unitId, existing);
|
|
@@ -3,8 +3,8 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
const TERMINATION_SIGNAL = "SIGTERM";
|
|
4
4
|
const FORCE_KILL_SIGNAL = "SIGKILL";
|
|
5
5
|
const FORCE_KILL_GRACE_MS = 1_000;
|
|
6
|
-
function
|
|
7
|
-
|
|
6
|
+
function formatCommand(command, args) {
|
|
7
|
+
return [command, ...args].join(" ");
|
|
8
8
|
}
|
|
9
9
|
// On Windows `command` must be the resolved .cmd / .exe path because `spawn`
|
|
10
10
|
// does not consult PATH for executables without a shell. Callers should use
|
|
@@ -24,7 +24,12 @@ export async function spawnLoggedCommand(command, args, input, env, options = {}
|
|
|
24
24
|
let timer;
|
|
25
25
|
let heartbeat;
|
|
26
26
|
let forceKillTimer;
|
|
27
|
-
|
|
27
|
+
let pendingLogWrites = 0;
|
|
28
|
+
let childClosed = false;
|
|
29
|
+
let closeCode = null;
|
|
30
|
+
let closeSignal = null;
|
|
31
|
+
let logsEnded = false;
|
|
32
|
+
const clearTimers = () => {
|
|
28
33
|
if (timer) {
|
|
29
34
|
clearTimeout(timer);
|
|
30
35
|
}
|
|
@@ -34,16 +39,30 @@ export async function spawnLoggedCommand(command, args, input, env, options = {}
|
|
|
34
39
|
if (forceKillTimer) {
|
|
35
40
|
clearTimeout(forceKillTimer);
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
};
|
|
43
|
+
const endLogs = (callback) => {
|
|
44
|
+
if (logsEnded) {
|
|
45
|
+
callback();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
logsEnded = true;
|
|
49
|
+
let remaining = 2;
|
|
50
|
+
const done = () => {
|
|
51
|
+
remaining -= 1;
|
|
52
|
+
if (remaining === 0) {
|
|
53
|
+
callback();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
stdoutLog.end(done);
|
|
57
|
+
stderrLog.end(done);
|
|
39
58
|
};
|
|
40
59
|
const settle = (callback) => {
|
|
41
60
|
if (settled) {
|
|
42
61
|
return;
|
|
43
62
|
}
|
|
44
63
|
settled = true;
|
|
45
|
-
|
|
46
|
-
callback
|
|
64
|
+
clearTimers();
|
|
65
|
+
endLogs(callback);
|
|
47
66
|
};
|
|
48
67
|
const fail = (error) => {
|
|
49
68
|
if (child && !child.killed) {
|
|
@@ -52,6 +71,37 @@ export async function spawnLoggedCommand(command, args, input, env, options = {}
|
|
|
52
71
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
53
72
|
settle(() => reject(normalized));
|
|
54
73
|
};
|
|
74
|
+
const writeLog = (write, chunk) => {
|
|
75
|
+
pendingLogWrites += 1;
|
|
76
|
+
write.write(chunk, () => {
|
|
77
|
+
pendingLogWrites -= 1;
|
|
78
|
+
maybeSettleFromClose();
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const maybeSettleFromClose = () => {
|
|
82
|
+
if (!childClosed || pendingLogWrites > 0 || settled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (timedOut) {
|
|
86
|
+
settle(() => reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`)));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
settle(() => resolve({
|
|
90
|
+
accepted: closeCode === 0 && closeSignal === null,
|
|
91
|
+
processId: spawnedChild.pid,
|
|
92
|
+
exitCode: closeCode,
|
|
93
|
+
signal: closeSignal,
|
|
94
|
+
command: formatCommand(command, args),
|
|
95
|
+
args,
|
|
96
|
+
stdoutPath: input.stdoutPath,
|
|
97
|
+
stderrPath: input.stderrPath,
|
|
98
|
+
error: closeCode === 0 && closeSignal === null
|
|
99
|
+
? undefined
|
|
100
|
+
: closeSignal
|
|
101
|
+
? `Provider command exited with signal ${closeSignal}.`
|
|
102
|
+
: `Provider command exited with code ${closeCode}.`,
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
55
105
|
stdoutLog.on("error", fail);
|
|
56
106
|
stderrLog.on("error", fail);
|
|
57
107
|
let spawnedChild;
|
|
@@ -83,35 +133,38 @@ export async function spawnLoggedCommand(command, args, input, env, options = {}
|
|
|
83
133
|
heartbeat = setInterval(() => {
|
|
84
134
|
const elapsedMs = Date.now() - startedAt;
|
|
85
135
|
const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
|
|
86
|
-
|
|
136
|
+
writeLog(stderrLog, message);
|
|
87
137
|
if (input.uiMode === "visible") {
|
|
88
138
|
process.stderr.write(message);
|
|
89
139
|
}
|
|
90
140
|
}, 30_000);
|
|
91
141
|
spawnedChild.stdout.on("data", (chunk) => {
|
|
92
|
-
|
|
142
|
+
writeLog(stdoutLog, chunk);
|
|
93
143
|
if (input.uiMode === "visible") {
|
|
94
144
|
process.stdout.write(chunk);
|
|
95
145
|
}
|
|
96
146
|
});
|
|
97
147
|
spawnedChild.stderr.on("data", (chunk) => {
|
|
98
|
-
|
|
148
|
+
writeLog(stderrLog, chunk);
|
|
99
149
|
if (input.uiMode === "visible") {
|
|
100
150
|
process.stderr.write(chunk);
|
|
101
151
|
}
|
|
102
152
|
});
|
|
103
153
|
spawnedChild.on("error", fail);
|
|
104
154
|
spawnedChild.on("exit", (code, signal) => {
|
|
105
|
-
if (timedOut) {
|
|
106
|
-
settle(() => reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`)));
|
|
155
|
+
if (!timedOut) {
|
|
107
156
|
return;
|
|
108
157
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
158
|
+
childClosed = true;
|
|
159
|
+
closeCode = code;
|
|
160
|
+
closeSignal = signal;
|
|
161
|
+
maybeSettleFromClose();
|
|
162
|
+
});
|
|
163
|
+
spawnedChild.on("close", (code, signal) => {
|
|
164
|
+
childClosed = true;
|
|
165
|
+
closeCode = code;
|
|
166
|
+
closeSignal = signal;
|
|
167
|
+
maybeSettleFromClose();
|
|
115
168
|
});
|
|
116
169
|
});
|
|
117
170
|
}
|
|
@@ -15,6 +15,11 @@ export interface LaunchFreshSessionResult {
|
|
|
15
15
|
processId?: number;
|
|
16
16
|
exitCode?: number | null;
|
|
17
17
|
signal?: string | null;
|
|
18
|
+
command?: string;
|
|
19
|
+
args?: string[];
|
|
20
|
+
stdoutPath?: string;
|
|
21
|
+
stderrPath?: string;
|
|
22
|
+
error?: string;
|
|
18
23
|
}
|
|
19
24
|
export interface FreshSessionProvider {
|
|
20
25
|
name: string;
|
package/dist/quota/scheduler.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveLimits } from "./limits.js";
|
|
1
|
+
import { classifyProvider, resolveLimits } from "./limits.js";
|
|
2
2
|
import { computeMaxSafeConcurrency } from "./state.js";
|
|
3
3
|
export function scheduleWave(options) {
|
|
4
4
|
const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedPacketTokens = 0, quotaStateEntry = null, hostConcurrencyLimit = null, } = options;
|
|
@@ -56,7 +56,15 @@ export function scheduleWave(options) {
|
|
|
56
56
|
const learnedCap = computeMaxSafeConcurrency(quotaStateEntry, halfLifeHours);
|
|
57
57
|
waveSize = Math.min(waveSize, learnedCap);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
else {
|
|
60
|
+
const providerType = classifyProvider(providerName);
|
|
61
|
+
const fallbackCap = providerType === "local"
|
|
62
|
+
? quota.unknown_local_concurrency
|
|
63
|
+
: (quota.unknown_hosted_concurrency ?? 1);
|
|
64
|
+
if (typeof fallbackCap === "number" && Number.isFinite(fallbackCap)) {
|
|
65
|
+
waveSize = Math.min(waveSize, Math.max(1, Math.floor(fallbackCap)));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
waveSize = applyHostConcurrencyLimit(waveSize);
|
|
62
70
|
waveSize = Math.max(1, waveSize);
|
package/dist/quota/state.js
CHANGED
|
@@ -39,9 +39,13 @@ export async function readQuotaState() {
|
|
|
39
39
|
const parsed = JSON.parse(raw);
|
|
40
40
|
if (isQuotaState(parsed))
|
|
41
41
|
return parsed;
|
|
42
|
+
process.stderr.write(`[quota] ignoring invalid quota state at ${STATE_PATH}: expected { version: 1, entries: object }\n`);
|
|
42
43
|
}
|
|
43
|
-
catch {
|
|
44
|
-
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === "ENOENT") {
|
|
46
|
+
return { version: 1, entries: {} };
|
|
47
|
+
}
|
|
48
|
+
process.stderr.write(`[quota] ignoring unreadable quota state at ${STATE_PATH}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
45
49
|
}
|
|
46
50
|
return { version: 1, entries: {} };
|
|
47
51
|
}
|
|
@@ -168,7 +168,7 @@ function renderMarkdown(handoff) {
|
|
|
168
168
|
lines.push(`- ${command}`);
|
|
169
169
|
}
|
|
170
170
|
if (handoff.active_review_run) {
|
|
171
|
-
lines.push("- Use next-step so the backend renders either packet dispatch or single-task fallback
|
|
171
|
+
lines.push("- Use next-step so the backend renders either packet dispatch or single-task fallback from CLI flags, session config, environment, or the default single-task path.");
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
if (handoff.active_review_run) {
|
|
@@ -19,10 +19,20 @@ export interface ExternalAnalyzerOwnershipRoot {
|
|
|
19
19
|
confidence?: number;
|
|
20
20
|
reason?: string;
|
|
21
21
|
}
|
|
22
|
+
export interface ExternalAnalyzerToolStatus {
|
|
23
|
+
tool: string;
|
|
24
|
+
command?: string;
|
|
25
|
+
resolved: boolean;
|
|
26
|
+
status: "skipped" | "success" | "findings" | "not_resolved" | "spawn_error" | "parse_error" | "failed";
|
|
27
|
+
exit_code?: number | null;
|
|
28
|
+
error?: string;
|
|
29
|
+
output_snippet?: string;
|
|
30
|
+
}
|
|
22
31
|
/** Imported analyzer output captured at a single generation time. */
|
|
23
32
|
export interface ExternalAnalyzerResults {
|
|
24
33
|
tool: string;
|
|
25
34
|
generated_at?: string;
|
|
26
35
|
ownership_roots?: ExternalAnalyzerOwnershipRoot[];
|
|
36
|
+
tool_statuses?: ExternalAnalyzerToolStatus[];
|
|
27
37
|
results: ExternalAnalyzerResultItem[];
|
|
28
38
|
}
|
|
@@ -63,6 +63,7 @@ export interface SessionConfig {
|
|
|
63
63
|
provider?: ProviderName;
|
|
64
64
|
timeout_ms?: number;
|
|
65
65
|
ui_mode?: SessionUiMode;
|
|
66
|
+
host_can_dispatch_subagents?: boolean;
|
|
66
67
|
subprocess_template?: SubprocessTemplateConfig;
|
|
67
68
|
claude_code?: ClaudeCodeConfig;
|
|
68
69
|
opencode?: OpenCodeConfig;
|
|
@@ -5,6 +5,9 @@ function pushIssue(issues, path, message) {
|
|
|
5
5
|
function asArray(value) {
|
|
6
6
|
return Array.isArray(value) ? value : [];
|
|
7
7
|
}
|
|
8
|
+
function hasOwnProperty(value, key) {
|
|
9
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
10
|
+
}
|
|
8
11
|
export function validateArtifactBundle(bundle) {
|
|
9
12
|
const issues = [];
|
|
10
13
|
if (bundle.repo_manifest) {
|
|
@@ -50,6 +53,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
50
53
|
"task_ids",
|
|
51
54
|
"lenses",
|
|
52
55
|
"file_paths",
|
|
56
|
+
"file_line_counts",
|
|
53
57
|
]));
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -66,6 +70,9 @@ export function validateArtifactBundle(bundle) {
|
|
|
66
70
|
const runtimeValidationTasks = asArray(bundle.runtime_validation_tasks?.tasks);
|
|
67
71
|
const runtimeValidationResults = asArray(bundle.runtime_validation_report?.results);
|
|
68
72
|
const externalAnalyzerResults = asArray(bundle.external_analyzer_results?.results);
|
|
73
|
+
const auditTasks = asArray(bundle.audit_tasks);
|
|
74
|
+
const requeueTasks = asArray(bundle.requeue_tasks);
|
|
75
|
+
const reviewPackets = asArray(bundle.review_packets);
|
|
69
76
|
const coverageFiles = asArray(bundle.coverage_matrix?.files);
|
|
70
77
|
const repoPaths = new Set(repoManifestFiles.map((file) => file.path));
|
|
71
78
|
const dispositionMap = new Map(fileDispositionEntries.map((item) => [item.path, item.status]));
|
|
@@ -216,5 +223,34 @@ export function validateArtifactBundle(bundle) {
|
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
}
|
|
226
|
+
const taskGroups = [
|
|
227
|
+
{ artifactPath: "audit_tasks", tasks: auditTasks },
|
|
228
|
+
{ artifactPath: "requeue_tasks", tasks: requeueTasks },
|
|
229
|
+
];
|
|
230
|
+
for (const { artifactPath, tasks } of taskGroups) {
|
|
231
|
+
for (const task of tasks) {
|
|
232
|
+
for (const [rangeIndex, range] of (task.line_ranges ?? []).entries()) {
|
|
233
|
+
const path = `${artifactPath}:${task.task_id}.line_ranges:${rangeIndex}`;
|
|
234
|
+
if (range.start < 1) {
|
|
235
|
+
pushIssue(issues, path, "Line range start must be a positive 1-based integer");
|
|
236
|
+
}
|
|
237
|
+
if (range.end < 1) {
|
|
238
|
+
pushIssue(issues, path, "Line range end must be a positive 1-based integer");
|
|
239
|
+
}
|
|
240
|
+
if (range.end < range.start) {
|
|
241
|
+
pushIssue(issues, path, "Line range end must be greater than or equal to start");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (bundle.review_packets) {
|
|
247
|
+
for (const [index, packet] of reviewPackets.entries()) {
|
|
248
|
+
const filePaths = asArray(packet.file_paths);
|
|
249
|
+
const missingPaths = filePaths.filter((path) => !hasOwnProperty(packet.file_line_counts ?? {}, path));
|
|
250
|
+
if (missingPaths.length > 0) {
|
|
251
|
+
pushIssue(issues, `review_packets:${packet.packet_id ?? index}`, `Every listed file must have a corresponding file_line_counts entry; missing ${missingPaths.join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
219
255
|
return issues;
|
|
220
256
|
}
|
|
@@ -151,6 +151,10 @@ export function validateSessionConfig(value) {
|
|
|
151
151
|
pushIssue(issues, "ui_mode", `ui_mode must be one of: ${Array.from(VALID_UI_MODES).join(", ")}.`);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
if (value.host_can_dispatch_subagents !== undefined &&
|
|
155
|
+
typeof value.host_can_dispatch_subagents !== "boolean") {
|
|
156
|
+
pushIssue(issues, "host_can_dispatch_subagents", "host_can_dispatch_subagents must be a boolean when provided.");
|
|
157
|
+
}
|
|
154
158
|
validateTemplateProviderSection(value.subprocess_template, "subprocess_template", issues, provider === "subprocess-template");
|
|
155
159
|
validateTemplateProviderSection(value.vscode_task, "vscode_task", issues, provider === "vscode-task");
|
|
156
160
|
validateAgentProviderSection(value.claude_code, "claude_code", issues);
|
package/package.json
CHANGED
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"required": ["path", "start", "end"],
|
|
39
39
|
"properties": {
|
|
40
40
|
"path": { "type": "string" },
|
|
41
|
-
"start": { "type": "integer" },
|
|
42
|
-
"end": { "type": "integer" }
|
|
41
|
+
"start": { "type": "integer", "minimum": 1 },
|
|
42
|
+
"end": { "type": "integer", "minimum": 1 }
|
|
43
43
|
},
|
|
44
44
|
"additionalProperties": false
|
|
45
45
|
}
|