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.
Files changed (36) hide show
  1. package/README.md +2 -1
  2. package/audit-code-wrapper-lib.mjs +208 -198
  3. package/dist/cli.d.ts +5 -0
  4. package/dist/cli.js +65 -101
  5. package/dist/extractors/risk.js +6 -4
  6. package/dist/io/artifacts.d.ts +2 -0
  7. package/dist/io/artifacts.js +1 -0
  8. package/dist/io/toolingManifest.d.ts +1 -0
  9. package/dist/io/toolingManifest.js +1 -1
  10. package/dist/mcp/server.d.ts +71 -0
  11. package/dist/mcp/server.js +261 -222
  12. package/dist/orchestrator/artifactFreshness.d.ts +4 -0
  13. package/dist/orchestrator/artifactFreshness.js +45 -0
  14. package/dist/orchestrator/artifactMetadata.js +2 -51
  15. package/dist/orchestrator/dependencyMap.js +14 -0
  16. package/dist/orchestrator/internalExecutors.js +8 -0
  17. package/dist/orchestrator/staleness.js +2 -46
  18. package/dist/orchestrator/state.js +1 -1
  19. package/dist/orchestrator/syntaxResolutionExecutor.js +121 -13
  20. package/dist/orchestrator/unitBuilder.js +2 -1
  21. package/dist/providers/spawnLoggedCommand.js +71 -18
  22. package/dist/providers/types.d.ts +5 -0
  23. package/dist/quota/scheduler.js +10 -2
  24. package/dist/quota/state.js +6 -2
  25. package/dist/supervisor/operatorHandoff.js +1 -1
  26. package/dist/types/externalAnalyzer.d.ts +10 -0
  27. package/dist/types/sessionConfig.d.ts +1 -0
  28. package/dist/types/workerSession.js +1 -2
  29. package/dist/validation/artifacts.js +36 -0
  30. package/dist/validation/sessionConfig.js +4 -0
  31. package/package.json +1 -1
  32. package/schemas/audit_task.schema.json +2 -2
  33. package/schemas/risk_register.schema.json +1 -1
  34. package/schemas/unit_manifest.schema.json +2 -1
  35. package/scripts/postinstall.mjs +10 -41
  36. 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
- function stableStringify(value) {
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 createHash("sha256")
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.external_analyzer_results))));
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 results;
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 results;
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
- process.stderr.write(`[syntax-resolution] tsc output could not be parsed: ${output.slice(0, 200)}\n`);
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 results;
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 results;
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 results;
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 results;
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
- process.stderr.write(`[syntax-resolution] eslint output could not be parsed: ${output.slice(0, 200)}\n`);
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 results;
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
- items.push(...runTsc(root));
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
- items.push(...runEslint(root));
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: ["external_analyzer_results.json"],
149
- progress_summary: `Phase 2 Syntax Resolution complete. Extracted ${items.length} unfixable syntax/lint errors, triggering high-priority LLM resolution tasks.`,
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 tee(write, chunk) {
7
- write.write(chunk);
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
- const cleanup = () => {
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
- stdoutLog.end();
38
- stderrLog.end();
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
- cleanup();
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
- tee(stderrLog, message);
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
- tee(stdoutLog, chunk);
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
- tee(stderrLog, chunk);
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
- settle(() => resolve({
110
- accepted: true,
111
- processId: spawnedChild.pid,
112
- exitCode: code,
113
- signal,
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;
@@ -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
- // No learned data: use requestedConcurrency and let 429 outcomes train the cap
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);
@@ -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
- // File not found or malformed — start fresh
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 after the host reports capabilities.");
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;
@@ -1,5 +1,4 @@
1
1
  export const WORKER_COMMAND_MODES = ["run", "deferred"];
2
2
  export function usesDeferredWorkerCommand(task) {
3
- return (task.worker_command_mode === "deferred" ||
4
- task.skip_worker_command === true);
3
+ return task.worker_command_mode === "deferred";
5
4
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.30",
3
+ "version": "0.3.33",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -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
  }