auditor-lambda 0.2.6 → 0.2.9

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 (125) hide show
  1. package/README.md +29 -7
  2. package/audit-code-wrapper-lib.mjs +1605 -330
  3. package/dist/adapters/eslint.js +9 -5
  4. package/dist/cli.d.ts +42 -1
  5. package/dist/cli.js +192 -80
  6. package/dist/coverage.d.ts +2 -2
  7. package/dist/coverage.js +5 -5
  8. package/dist/extractors/bucketing.d.ts +4 -0
  9. package/dist/extractors/bucketing.js +6 -2
  10. package/dist/extractors/disposition.d.ts +4 -0
  11. package/dist/extractors/disposition.js +15 -2
  12. package/dist/extractors/fileInventory.js +24 -28
  13. package/dist/extractors/flows.d.ts +5 -0
  14. package/dist/extractors/flows.js +25 -39
  15. package/dist/extractors/pathPatterns.d.ts +13 -3
  16. package/dist/extractors/pathPatterns.js +116 -53
  17. package/dist/extractors/risk.js +7 -1
  18. package/dist/extractors/surfaces.d.ts +4 -0
  19. package/dist/extractors/surfaces.js +11 -11
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/io/artifacts.d.ts +59 -44
  23. package/dist/io/artifacts.js +80 -120
  24. package/dist/io/json.d.ts +2 -0
  25. package/dist/io/json.js +65 -19
  26. package/dist/io/runArtifacts.d.ts +2 -1
  27. package/dist/io/runArtifacts.js +44 -7
  28. package/dist/mcp/server.d.ts +1 -0
  29. package/dist/mcp/server.js +579 -0
  30. package/dist/orchestrator/advance.js +84 -56
  31. package/dist/orchestrator/dependencyMap.js +9 -13
  32. package/dist/orchestrator/executors.js +7 -2
  33. package/dist/orchestrator/flowCoverage.js +11 -5
  34. package/dist/orchestrator/flowPlanning.d.ts +7 -2
  35. package/dist/orchestrator/flowPlanning.js +46 -21
  36. package/dist/orchestrator/flowRequeue.js +29 -9
  37. package/dist/orchestrator/internalExecutors.d.ts +2 -1
  38. package/dist/orchestrator/internalExecutors.js +130 -69
  39. package/dist/orchestrator/planning.js +25 -3
  40. package/dist/orchestrator/requeue.js +20 -5
  41. package/dist/orchestrator/resultIngestion.js +5 -6
  42. package/dist/orchestrator/runtimeValidation.d.ts +7 -2
  43. package/dist/orchestrator/runtimeValidation.js +61 -49
  44. package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
  45. package/dist/orchestrator/state.js +18 -13
  46. package/dist/orchestrator/taskBuilder.d.ts +4 -2
  47. package/dist/orchestrator/taskBuilder.js +153 -52
  48. package/dist/orchestrator/trivialAudit.js +8 -5
  49. package/dist/orchestrator/unitBuilder.d.ts +3 -1
  50. package/dist/orchestrator/unitBuilder.js +24 -16
  51. package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
  52. package/dist/prompts/renderWorkerPrompt.js +19 -10
  53. package/dist/providers/claudeCodeProvider.d.ts +4 -1
  54. package/dist/providers/claudeCodeProvider.js +8 -5
  55. package/dist/providers/localSubprocessProvider.d.ts +4 -0
  56. package/dist/providers/localSubprocessProvider.js +7 -2
  57. package/dist/providers/spawnLoggedCommand.d.ts +9 -1
  58. package/dist/providers/spawnLoggedCommand.js +77 -29
  59. package/dist/reporting/mergeFindings.js +0 -11
  60. package/dist/reporting/synthesis.d.ts +26 -21
  61. package/dist/reporting/synthesis.js +97 -61
  62. package/dist/reporting/workBlocks.d.ts +12 -3
  63. package/dist/reporting/workBlocks.js +124 -70
  64. package/dist/supervisor/operatorHandoff.js +48 -18
  65. package/dist/supervisor/runLedger.d.ts +1 -1
  66. package/dist/supervisor/runLedger.js +112 -5
  67. package/dist/supervisor/sessionConfig.js +10 -10
  68. package/dist/types/externalAnalyzer.d.ts +3 -0
  69. package/dist/types/flowCoverage.d.ts +5 -1
  70. package/dist/types/flowCoverage.js +5 -1
  71. package/dist/types/flows.d.ts +6 -0
  72. package/dist/types/flows.js +1 -1
  73. package/dist/types/runLedger.d.ts +5 -1
  74. package/dist/types/runLedger.js +6 -1
  75. package/dist/types/runtimeValidation.d.ts +13 -3
  76. package/dist/types/runtimeValidation.js +16 -1
  77. package/dist/types/sessionConfig.d.ts +15 -2
  78. package/dist/types/sessionConfig.js +15 -1
  79. package/dist/types/surfaces.d.ts +4 -1
  80. package/dist/types/surfaces.js +1 -1
  81. package/dist/types/workerSession.d.ts +9 -0
  82. package/dist/types/workerSession.js +5 -1
  83. package/dist/types.d.ts +4 -7
  84. package/dist/validation/artifacts.d.ts +1 -1
  85. package/dist/validation/artifacts.js +33 -20
  86. package/dist/validation/auditResults.d.ts +2 -2
  87. package/dist/validation/auditResults.js +71 -114
  88. package/dist/validation/basic.d.ts +9 -1
  89. package/dist/validation/basic.js +40 -3
  90. package/dist/validation/sessionConfig.d.ts +4 -2
  91. package/dist/validation/sessionConfig.js +62 -15
  92. package/docs/agent-integrations.md +67 -38
  93. package/docs/artifacts.md +16 -56
  94. package/docs/bootstrap-install.md +60 -30
  95. package/docs/contract.md +22 -205
  96. package/docs/next-steps.md +76 -44
  97. package/docs/packaging.md +27 -3
  98. package/docs/product-direction.md +22 -0
  99. package/docs/production-launch-bar.md +4 -2
  100. package/docs/production-readiness.md +9 -5
  101. package/docs/releasing.md +98 -0
  102. package/docs/remediation-baseline.md +75 -0
  103. package/docs/run-flow.md +23 -11
  104. package/docs/session-config.md +50 -5
  105. package/docs/supervisor.md +7 -0
  106. package/docs/workflow-refactor-brief.md +177 -0
  107. package/package.json +4 -1
  108. package/schemas/audit_result.schema.json +8 -7
  109. package/schemas/audit_task.schema.json +3 -1
  110. package/schemas/coverage_matrix.schema.json +3 -3
  111. package/schemas/critical_flows.schema.json +6 -2
  112. package/schemas/file_disposition.schema.json +2 -2
  113. package/schemas/finding.schema.json +9 -4
  114. package/schemas/flow_coverage.schema.json +2 -2
  115. package/schemas/repo_manifest.schema.json +4 -4
  116. package/schemas/risk_register.schema.json +2 -2
  117. package/schemas/runtime_validation_report.schema.json +3 -3
  118. package/schemas/runtime_validation_tasks.schema.json +8 -2
  119. package/schemas/surface_manifest.schema.json +6 -3
  120. package/schemas/unit_manifest.schema.json +3 -2
  121. package/skills/audit-code/SKILL.md +16 -2
  122. package/skills/audit-code/audit-code.prompt.md +5 -8
  123. package/schemas/merged_findings.schema.json +0 -19
  124. package/schemas/root_cause_clusters.schema.json +0 -28
  125. package/schemas/synthesis_report.schema.json +0 -61
@@ -60,15 +60,23 @@ export function deriveAuditState(bundle) {
60
60
  has(bundle.audit_tasks)) {
61
61
  obligations.push(obligation("audit_tasks_completed", "satisfied"));
62
62
  }
63
- obligations.push(obligation("audit_results_ingested", has(bundle.audit_results) ? "present" : "missing"));
64
- obligations.push(obligation("runtime_validation_current", staleOrSatisfied(staleArtifacts, ["runtime_validation_report.json"], has(bundle.runtime_validation_report))));
65
- obligations.push(obligation("synthesis_current", staleOrSatisfied(staleArtifacts, [
66
- "merged_findings.json",
67
- "root_cause_clusters.json",
68
- "synthesis_report.json",
69
- ], has(bundle.merged_findings) &&
70
- has(bundle.root_cause_clusters) &&
71
- has(bundle.synthesis_report))));
63
+ obligations.push(obligation("audit_results_ingested", (bundle.audit_tasks?.length ?? 0) === 0 || has(bundle.audit_results)
64
+ ? "present"
65
+ : "missing"));
66
+ const runtimeTasks = bundle.runtime_validation_tasks?.tasks ?? [];
67
+ const runtimeResults = bundle.runtime_validation_report?.results ?? [];
68
+ const runtimeReady = runtimeTasks.length === 0 ||
69
+ (runtimeTasks.length > 0 &&
70
+ runtimeTasks.every((task) => runtimeResults.some((result) => result.task_id === task.id &&
71
+ result.status !== "pending")));
72
+ obligations.push(obligation("runtime_validation_current", runtimeReady
73
+ ? "satisfied"
74
+ : has(bundle.runtime_validation_report)
75
+ ? "missing"
76
+ : "missing", runtimeTasks.length === 0
77
+ ? "No deterministic runtime validation tasks were planned."
78
+ : undefined));
79
+ obligations.push(obligation("synthesis_current", staleOrSatisfied(staleArtifacts, ["audit-report.md"], has(bundle.audit_report))));
72
80
  let status = "not_started";
73
81
  if (!has(bundle.repo_manifest)) {
74
82
  status = "not_started";
@@ -80,10 +88,7 @@ export function deriveAuditState(bundle) {
80
88
  status = "active";
81
89
  }
82
90
  const incomplete = obligations.some((o) => o.state === "missing" || o.state === "stale");
83
- if (!incomplete &&
84
- has(bundle.synthesis_report) &&
85
- has(bundle.merged_findings) &&
86
- has(bundle.root_cause_clusters)) {
91
+ if (!incomplete && has(bundle.audit_report)) {
87
92
  status = "complete";
88
93
  }
89
94
  return {
@@ -1,5 +1,6 @@
1
1
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
2
- import type { AuditTask, CoverageMatrix, Lens, UnitManifest } from "../types.js";
2
+ import type { AuditTask, CoverageMatrix, Lens } from "../types.js";
3
+ import type { CriticalFlowManifest } from "../types/flows.js";
3
4
  export interface UnitLineIndex {
4
5
  [path: string]: number;
5
6
  }
@@ -12,6 +13,7 @@ export interface BuildChunkedTaskOptions {
12
13
  file_split_threshold?: number;
13
14
  limit_lenses?: Lens[];
14
15
  external_analyzer_results?: ExternalAnalyzerResults;
16
+ critical_flows?: CriticalFlowManifest;
15
17
  }
16
- export declare function buildChunkedAuditTasks(unitManifest: UnitManifest, unitLineIndex: UnitLineIndex, options?: BuildChunkedTaskOptions): AuditTask[];
18
+ export declare function buildChunkedAuditTasks(coverageMatrix: CoverageMatrix, unitLineIndex: UnitLineIndex, options?: BuildChunkedTaskOptions): AuditTask[];
17
19
  export declare function buildExternalSignalTasks(coverageMatrix: CoverageMatrix, _unitLineIndex: UnitLineIndex, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
@@ -1,5 +1,12 @@
1
+ import { claimFlowReviewBlocks } from "./flowPlanning.js";
1
2
  import { isTrivialAuditPath } from "./trivialAudit.js";
2
- function taskPriority(hasExternalSignal, lens) {
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
- export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}) {
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 = new Set((options.external_analyzer_results?.results ?? []).map((item) => item.path));
49
- for (const unit of unitManifest.units) {
50
- const minimumLenses = ["correctness", "security"];
51
- const lensesToRun = Array.isArray(unit.required_lenses) && unit.required_lenses.length >= 2
52
- ? unit.required_lenses
53
- : minimumLenses;
54
- for (const lens of lensesToRun) {
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
- const hasExternalSignal = unit.files.some((f) => externalPaths.has(f));
59
- const priority = taskPriority(hasExternalSignal, lens);
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
- // Oversized files each get their own task so the agent isn't overwhelmed.
86
- for (const filePath of oversizedFiles) {
87
- const id = `${unit.unit_id}:${lens}:${filePath}`;
88
- if (seen.has(id))
89
- continue;
90
- seen.add(id);
91
- const fileHasSignal = externalPaths.has(filePath);
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: id,
94
- unit_id: unit.unit_id,
95
- pass_id: `pass:${lens}`,
96
- lens,
97
- file_paths: [filePath],
98
- rationale: `Audit ${filePath} (large file, split from unit) under the ${lens} lens.${fileHasSignal ? " External analyzer signals raise priority." : ""}`,
99
- priority: taskPriority(fileHasSignal, lens),
100
- tags: fileHasSignal ? ["external_analyzer_signal", "large_file"] : ["large_file"],
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
- for (const result of externalAnalyzerResults.results) {
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 = coverageMatrix.files.find((file) => file.path === result.path);
228
+ const coverage = coverageByPath.get(result.path);
128
229
  if (!coverage || coverage.audit_status === "excluded") {
129
230
  continue;
130
231
  }
@@ -20,6 +20,9 @@ export function isTrivialAuditPath(path, lineCount, hasExternalSignal = false) {
20
20
  if (name === "__init__.py" && lineCount <= 3) {
21
21
  return true;
22
22
  }
23
+ if (lineCount <= 1) {
24
+ return true;
25
+ }
23
26
  return false;
24
27
  }
25
28
  export function autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults) {
@@ -35,11 +38,11 @@ export function autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyze
35
38
  if (file.required_lenses.length === 0) {
36
39
  continue;
37
40
  }
38
- file.completed_lenses = [...new Set(file.required_lenses)];
39
- file.audit_status = "complete";
40
- if (file.classification_status === "unclassified") {
41
- file.classification_status = "classified";
42
- }
41
+ file.completed_lenses = [];
42
+ file.required_lenses = [];
43
+ file.audit_status = "excluded";
44
+ file.classification_status = "excluded_trivial";
45
+ file.unit_ids = [];
43
46
  skipped.push(file.path);
44
47
  }
45
48
  return skipped.sort();
@@ -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 order.filter((lens) => set.has(lens));
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 bucket of assignment.buckets) {
127
- for (const lens of LENS_MAP[bucket]) {
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 { WorkerTask } from "../types/workerSession.js";
1
+ import { type WorkerTask } from "../types/workerSession.js";
2
2
  export declare function renderWorkerPrompt(task: WorkerTask): string;
@@ -1,8 +1,9 @@
1
- function shellQuote(arg) {
2
- return JSON.stringify(arg);
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 command = task.worker_command.map(shellQuote).join(" ");
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`;
@@ -20,22 +21,26 @@ export function renderWorkerPrompt(task) {
20
21
  " 2. Review the content under the specified lens.",
21
22
  " 3. Emit one AuditResult with:",
22
23
  " task_id, unit_id, pass_id, lens",
23
- " reviewed_ranges: [{path, start, end, line_count}] covering what you read",
24
+ " file_coverage: [{path, total_lines}] for every assigned file you reviewed",
24
25
  " findings: array (empty if nothing found)",
25
- " line_count must match the file's current total line count so ingestion can verify the cited ranges.",
26
+ " total_lines must match the file's current total line count.",
26
27
  " Each finding must include:",
27
28
  " id, title, category, severity, confidence, lens, summary, affected_files,",
28
29
  " evidence (an array of plain strings only, at least one excerpt or line reference from the file you read)",
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.",
36
+ `Reference schema: ${task.artifacts_dir}/dispatch/audit-result.schema.json`,
32
37
  `Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
33
38
  ];
34
- if (task.skip_worker_command) {
35
- 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.");
36
41
  }
37
42
  else {
38
- lines.push("", "Then run this command exactly:", command, "Stop after the command completes.");
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.");
39
44
  }
40
45
  return lines.join("\n");
41
46
  }
@@ -45,10 +50,14 @@ export function renderWorkerPrompt(task) {
45
50
  `Repository root: ${task.repo_root}`,
46
51
  `Obligation: ${task.obligation_id ?? "unknown"}`,
47
52
  `Executor: ${task.preferred_executor}`,
48
- "Execute the following command exactly, without modification:",
49
- command,
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}`,
50
56
  "Do not continue the audit recursively.",
51
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.",
52
61
  `The command must write the worker result JSON to: ${task.result_path}`,
53
62
  "After the command completes, stop.",
54
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
- constructor(config?: ClaudeCodeConfig);
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
- constructor(config = {}) {
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("claude-code provider cannot be used inside an active Claude Code session. " +
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 spawnLoggedCommand(command, args, input);
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("local-subprocess provider requires task.worker_command.");
13
+ throw new Error(MISSING_WORKER_COMMAND_MESSAGE);
9
14
  }
10
15
  const [command, ...args] = task.worker_command;
11
- return await spawnLoggedCommand(command, args, input);
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
- export declare function spawnLoggedCommand(command: string, args: string[], input: LaunchFreshSessionInput, env?: Record<string, string>): Promise<LaunchFreshSessionResult>;
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 {};