auditor-lambda 0.3.3 → 0.3.5

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 (47) hide show
  1. package/README.md +6 -1
  2. package/audit-code-wrapper-lib.mjs +87 -7
  3. package/dist/cli.js +517 -91
  4. package/dist/extractors/graph.d.ts +5 -1
  5. package/dist/extractors/graph.js +223 -3
  6. package/dist/extractors/pathPatterns.d.ts +3 -2
  7. package/dist/extractors/pathPatterns.js +97 -24
  8. package/dist/io/artifacts.d.ts +5 -0
  9. package/dist/io/artifacts.js +2 -0
  10. package/dist/orchestrator/advance.js +1 -1
  11. package/dist/orchestrator/dependencyMap.js +18 -0
  12. package/dist/orchestrator/fileAnchors.d.ts +32 -0
  13. package/dist/orchestrator/fileAnchors.js +217 -0
  14. package/dist/orchestrator/internalExecutors.d.ts +1 -1
  15. package/dist/orchestrator/internalExecutors.js +120 -33
  16. package/dist/orchestrator/reviewPackets.d.ts +14 -0
  17. package/dist/orchestrator/reviewPackets.js +310 -0
  18. package/dist/orchestrator/selectiveDeepening.d.ts +14 -0
  19. package/dist/orchestrator/selectiveDeepening.js +392 -0
  20. package/dist/orchestrator/state.js +6 -1
  21. package/dist/orchestrator/taskBuilder.d.ts +16 -0
  22. package/dist/orchestrator/taskBuilder.js +68 -11
  23. package/dist/prompts/renderWorkerPrompt.js +2 -1
  24. package/dist/providers/claudeCodeProvider.js +3 -1
  25. package/dist/providers/index.js +2 -1
  26. package/dist/supervisor/operatorHandoff.js +22 -11
  27. package/dist/types/graph.d.ts +1 -0
  28. package/dist/types/reviewPlanning.d.ts +41 -0
  29. package/dist/types/reviewPlanning.js +1 -0
  30. package/dist/types/sessionConfig.d.ts +1 -0
  31. package/dist/validation/artifacts.js +13 -0
  32. package/dist/validation/auditResults.js +50 -2
  33. package/dist/validation/sessionConfig.js +5 -0
  34. package/docs/agent-integrations.md +4 -1
  35. package/docs/bootstrap-install.md +3 -0
  36. package/docs/contract.md +3 -0
  37. package/docs/dispatch-implementation-plan.md +220 -489
  38. package/docs/next-steps.md +13 -8
  39. package/docs/product-direction.md +5 -3
  40. package/docs/run-flow.md +25 -30
  41. package/docs/session-config.md +15 -4
  42. package/docs/supervisor.md +5 -3
  43. package/docs/workflow-refactor-brief.md +114 -176
  44. package/package.json +1 -1
  45. package/schemas/finding.schema.json +1 -15
  46. package/schemas/graph_bundle.schema.json +16 -0
  47. package/skills/audit-code/audit-code.prompt.md +11 -6
@@ -0,0 +1,392 @@
1
+ import { createHash } from "node:crypto";
2
+ const DEFAULT_MAX_DEEPENING_TASKS = 6;
3
+ const DEEPENING_TAG = "selective_deepening";
4
+ const SEVERITY_RANK = {
5
+ critical: 5,
6
+ high: 4,
7
+ medium: 3,
8
+ low: 2,
9
+ info: 1,
10
+ };
11
+ const CONFIDENCE_RANK = {
12
+ high: 3,
13
+ medium: 2,
14
+ low: 1,
15
+ };
16
+ function priorityRank(priority) {
17
+ switch (priority) {
18
+ case "high":
19
+ return 3;
20
+ case "medium":
21
+ return 2;
22
+ case "low":
23
+ default:
24
+ return 1;
25
+ }
26
+ }
27
+ function isDeepeningTask(task) {
28
+ return task?.tags?.includes(DEEPENING_TAG) ?? false;
29
+ }
30
+ function sanitizeSegment(value) {
31
+ const sanitized = value
32
+ .replace(/[^a-zA-Z0-9_-]+/g, "-")
33
+ .replace(/^-+|-+$/g, "");
34
+ return sanitized.length > 0 ? sanitized : "followup";
35
+ }
36
+ function shortHash(value) {
37
+ return createHash("sha1").update(value).digest("hex").slice(0, 10);
38
+ }
39
+ function resultLineIndex(result) {
40
+ return Object.fromEntries(result.file_coverage.map((coverage) => [
41
+ coverage.path,
42
+ coverage.total_lines,
43
+ ]));
44
+ }
45
+ function lineCountForPath(path, task, result, lineIndex) {
46
+ return (task?.file_line_counts?.[path] ??
47
+ resultLineIndex(result)[path] ??
48
+ lineIndex?.[path] ??
49
+ 0);
50
+ }
51
+ function uniqueSorted(values) {
52
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
53
+ }
54
+ function intersects(left, right) {
55
+ const rightSet = new Set(right);
56
+ return left.some((value) => rightSet.has(value));
57
+ }
58
+ function pathsForFinding(finding, result, task) {
59
+ const assignedPaths = new Set([
60
+ ...(task?.file_paths ?? []),
61
+ ...result.file_coverage.map((coverage) => coverage.path),
62
+ ]);
63
+ const affected = finding.affected_files
64
+ .map((file) => file.path)
65
+ .filter((path) => assignedPaths.size === 0 || assignedPaths.has(path));
66
+ return uniqueSorted(affected.length > 0
67
+ ? affected
68
+ : result.file_coverage.map((coverage) => coverage.path));
69
+ }
70
+ function taskIdFor(prefix, values) {
71
+ return `deepening:${prefix}:${shortHash(values.join("\0"))}`;
72
+ }
73
+ function lineCountFromSources(path, tasks, results, lineIndex) {
74
+ for (const task of tasks) {
75
+ const count = task.file_line_counts?.[path];
76
+ if (count !== undefined) {
77
+ return count;
78
+ }
79
+ }
80
+ for (const result of results) {
81
+ const coverage = result.file_coverage.find((item) => item.path === path);
82
+ if (coverage) {
83
+ return coverage.total_lines;
84
+ }
85
+ }
86
+ return lineIndex?.[path] ?? 0;
87
+ }
88
+ function buildFindingFollowupTask(params) {
89
+ const paths = pathsForFinding(params.finding, params.result, params.task);
90
+ const triggerLabel = params.triggers.join("+");
91
+ const taskId = taskIdFor("finding", [
92
+ params.result.task_id,
93
+ params.finding.id,
94
+ triggerLabel,
95
+ ]);
96
+ const priority = SEVERITY_RANK[params.finding.severity] >= SEVERITY_RANK.high
97
+ ? "high"
98
+ : "medium";
99
+ return {
100
+ task_id: taskId,
101
+ unit_id: params.result.unit_id,
102
+ pass_id: `deepening:${params.result.pass_id}`,
103
+ lens: params.result.lens,
104
+ file_paths: paths,
105
+ file_line_counts: Object.fromEntries(paths.map((path) => [
106
+ path,
107
+ lineCountForPath(path, params.task, params.result, params.lineIndex),
108
+ ])),
109
+ rationale: `Follow up on ${params.finding.id} (${params.finding.severity}/${params.finding.confidence}) from ${params.result.task_id}. ` +
110
+ "Verify impact, evidence quality, affected scope, and whether the finding should stand, narrow, or be downgraded.",
111
+ priority,
112
+ tags: [
113
+ DEEPENING_TAG,
114
+ ...params.triggers.map((trigger) => `trigger:${trigger}`),
115
+ `source_task:${sanitizeSegment(params.result.task_id)}`,
116
+ `finding:${sanitizeSegment(params.finding.id)}`,
117
+ ],
118
+ status: "pending",
119
+ };
120
+ }
121
+ function buildConflictFollowupTask(params) {
122
+ const [first] = params.contexts;
123
+ const paths = uniqueSorted(params.contexts.flatMap((context) => context.paths));
124
+ const maxSeverity = Math.max(...params.contexts.map((context) => SEVERITY_RANK[context.finding.severity]));
125
+ const lineSources = new Map();
126
+ for (const context of params.contexts) {
127
+ for (const path of context.paths) {
128
+ if (!lineSources.has(path)) {
129
+ lineSources.set(path, { task: context.task, result: context.result });
130
+ }
131
+ }
132
+ }
133
+ const sourceTaskIds = uniqueSorted(params.contexts.map((context) => context.result.task_id));
134
+ const findingIds = uniqueSorted(params.contexts.map((context) => context.finding.id));
135
+ return {
136
+ task_id: taskIdFor("conflict", [
137
+ params.conflictKey,
138
+ ...sourceTaskIds,
139
+ ...findingIds,
140
+ ]),
141
+ unit_id: first?.result.unit_id ?? "selective-deepening",
142
+ pass_id: `deepening:${first?.result.pass_id ?? "conflict"}`,
143
+ lens: (first?.result.lens ?? "correctness"),
144
+ file_paths: paths,
145
+ file_line_counts: Object.fromEntries(paths.map((path) => {
146
+ const source = lineSources.get(path);
147
+ return [
148
+ path,
149
+ source
150
+ ? lineCountForPath(path, source.task, source.result, params.lineIndex)
151
+ : (params.lineIndex?.[path] ?? 0),
152
+ ];
153
+ })),
154
+ rationale: `Reconcile conflicting audit output for ${params.conflictKey}. ` +
155
+ `Compare source tasks ${sourceTaskIds.join(", ")} and decide the correct severity, confidence, and evidence-backed conclusion.`,
156
+ priority: maxSeverity >= SEVERITY_RANK.high ? "high" : "medium",
157
+ tags: [
158
+ DEEPENING_TAG,
159
+ "trigger:conflicting_output",
160
+ ...sourceTaskIds.slice(0, 3).map((id) => `source_task:${sanitizeSegment(id)}`),
161
+ ],
162
+ status: "pending",
163
+ };
164
+ }
165
+ function isHighRiskCleanResult(result, task) {
166
+ if (result.findings.length > 0 ||
167
+ result.requires_followup === false ||
168
+ isDeepeningTask(task)) {
169
+ return false;
170
+ }
171
+ if (!task) {
172
+ return (result.requires_followup === true &&
173
+ (result.lens === "security" || result.lens === "data_integrity"));
174
+ }
175
+ if (task.priority === "high") {
176
+ return true;
177
+ }
178
+ if (task.tags?.some((tag) => ["critical_flow", "external_analyzer_signal"].includes(tag))) {
179
+ return true;
180
+ }
181
+ return result.requires_followup === true && task.priority === "medium";
182
+ }
183
+ function buildHighRiskCleanFollowupTask(params) {
184
+ const paths = uniqueSorted((params.task?.file_paths.length ?? 0) > 0
185
+ ? (params.task?.file_paths ?? [])
186
+ : params.result.file_coverage.map((coverage) => coverage.path));
187
+ return {
188
+ task_id: taskIdFor("clean", [params.result.task_id, params.result.lens]),
189
+ unit_id: params.result.unit_id,
190
+ pass_id: `deepening:${params.result.pass_id}`,
191
+ lens: params.result.lens,
192
+ file_paths: paths,
193
+ file_line_counts: Object.fromEntries(paths.map((path) => [
194
+ path,
195
+ lineCountForPath(path, params.task, params.result, params.lineIndex),
196
+ ])),
197
+ rationale: `Sample high-risk no-finding result from ${params.result.task_id}. ` +
198
+ "Re-review the assigned files for missed edge cases, hidden runtime failures, and whether the clean conclusion should stand.",
199
+ priority: params.task?.priority === "high" ? "high" : "medium",
200
+ tags: [
201
+ DEEPENING_TAG,
202
+ "trigger:high_risk_no_finding",
203
+ `source_task:${sanitizeSegment(params.result.task_id)}`,
204
+ ],
205
+ status: "pending",
206
+ };
207
+ }
208
+ function runtimeResultNeedsFollowup(status) {
209
+ return status === "not_confirmed" || status === "inconclusive";
210
+ }
211
+ function pickRuntimeFollowupLens(relatedTasks) {
212
+ const preference = [
213
+ "security",
214
+ "data_integrity",
215
+ "reliability",
216
+ "correctness",
217
+ "tests",
218
+ "operability",
219
+ "config_deployment",
220
+ "performance",
221
+ "architecture",
222
+ "maintainability",
223
+ ];
224
+ for (const lens of preference) {
225
+ if (relatedTasks.some((task) => task.lens === lens)) {
226
+ return lens;
227
+ }
228
+ }
229
+ return "correctness";
230
+ }
231
+ function runtimeValidationHasStrongStaticFinding(runtimeTask, contexts) {
232
+ return contexts.some((context) => intersects(context.paths, runtimeTask.target_paths) &&
233
+ SEVERITY_RANK[context.finding.severity] >= SEVERITY_RANK.high);
234
+ }
235
+ function buildRuntimeValidationFollowupTask(params) {
236
+ const paths = uniqueSorted(params.runtimeTask.target_paths);
237
+ const lens = pickRuntimeFollowupLens(params.relatedTasks);
238
+ const firstRelated = params.relatedTasks[0];
239
+ return {
240
+ task_id: taskIdFor("runtime", [params.runtimeTask.id]),
241
+ unit_id: firstRelated?.unit_id ?? `runtime:${sanitizeSegment(params.runtimeTask.id)}`,
242
+ pass_id: `deepening:runtime:${sanitizeSegment(params.runtimeTask.id)}`,
243
+ lens,
244
+ file_paths: paths,
245
+ file_line_counts: Object.fromEntries(paths.map((path) => [
246
+ path,
247
+ lineCountFromSources(path, params.relatedTasks, params.results, params.lineIndex),
248
+ ])),
249
+ rationale: `Reconcile runtime validation ${params.runtimeTask.id} (${params.runtimeResultStatus}) with semantic audit output. ` +
250
+ "Verify the failing or inconclusive runtime evidence, map it to source behavior, and decide whether a finding should be added or escalated.",
251
+ priority: params.runtimeTask.priority === "high" ||
252
+ params.runtimeResultStatus === "not_confirmed"
253
+ ? "high"
254
+ : "medium",
255
+ tags: [
256
+ DEEPENING_TAG,
257
+ "trigger:runtime_validation_disagreement",
258
+ `runtime_task:${sanitizeSegment(params.runtimeTask.id)}`,
259
+ `runtime_status:${params.runtimeResultStatus}`,
260
+ ],
261
+ status: "pending",
262
+ };
263
+ }
264
+ function findingContexts(results, taskById) {
265
+ const contexts = [];
266
+ for (const result of results) {
267
+ const task = taskById.get(result.task_id);
268
+ if (isDeepeningTask(task)) {
269
+ continue;
270
+ }
271
+ for (const finding of result.findings) {
272
+ contexts.push({
273
+ result,
274
+ task,
275
+ finding,
276
+ paths: pathsForFinding(finding, result, task),
277
+ });
278
+ }
279
+ }
280
+ return contexts;
281
+ }
282
+ function conflictGroups(contexts) {
283
+ const groups = new Map();
284
+ for (const context of contexts) {
285
+ for (const path of context.paths) {
286
+ const key = [
287
+ context.result.lens,
288
+ context.finding.category,
289
+ path.toLowerCase(),
290
+ ].join(":");
291
+ const group = groups.get(key) ?? [];
292
+ group.push(context);
293
+ groups.set(key, group);
294
+ }
295
+ }
296
+ for (const [key, group] of groups) {
297
+ const uniqueTasks = new Set(group.map((context) => context.result.task_id));
298
+ const severities = group.map((context) => SEVERITY_RANK[context.finding.severity]);
299
+ const confidences = group.map((context) => CONFIDENCE_RANK[context.finding.confidence]);
300
+ const severitySpread = Math.max(...severities) - Math.min(...severities);
301
+ const confidenceSpread = Math.max(...confidences) - Math.min(...confidences);
302
+ if (uniqueTasks.size < 2 || (severitySpread < 2 && confidenceSpread < 2)) {
303
+ groups.delete(key);
304
+ }
305
+ }
306
+ return groups;
307
+ }
308
+ export function buildSelectiveDeepeningTasks(options) {
309
+ const taskById = new Map((options.existingTasks ?? []).map((task) => [task.task_id, task]));
310
+ const existingTasks = options.existingTasks ?? [];
311
+ const existingIds = new Set(taskById.keys());
312
+ const maxTasks = options.maxTasks ?? DEFAULT_MAX_DEEPENING_TASKS;
313
+ const created = [];
314
+ function pushIfNew(task) {
315
+ if (created.length >= maxTasks || existingIds.has(task.task_id)) {
316
+ return;
317
+ }
318
+ existingIds.add(task.task_id);
319
+ created.push(task);
320
+ }
321
+ const contexts = findingContexts(options.results, taskById);
322
+ for (const context of contexts) {
323
+ const triggers = [];
324
+ if (SEVERITY_RANK[context.finding.severity] >= SEVERITY_RANK.high) {
325
+ triggers.push("high_severity");
326
+ }
327
+ if (context.finding.confidence === "low") {
328
+ triggers.push("low_confidence");
329
+ }
330
+ if (triggers.length === 0) {
331
+ continue;
332
+ }
333
+ pushIfNew(buildFindingFollowupTask({
334
+ result: context.result,
335
+ task: context.task,
336
+ finding: context.finding,
337
+ triggers,
338
+ lineIndex: options.lineIndex,
339
+ }));
340
+ }
341
+ for (const [key, group] of [...conflictGroups(contexts).entries()].sort(([a], [b]) => a.localeCompare(b))) {
342
+ pushIfNew(buildConflictFollowupTask({
343
+ contexts: group,
344
+ conflictKey: key,
345
+ lineIndex: options.lineIndex,
346
+ }));
347
+ }
348
+ const runtimeTaskById = new Map((options.runtimeValidationTasks?.tasks ?? []).map((task) => [
349
+ task.id,
350
+ task,
351
+ ]));
352
+ for (const result of [...(options.runtimeValidationReport?.results ?? [])].sort((a, b) => a.task_id.localeCompare(b.task_id))) {
353
+ if (!runtimeResultNeedsFollowup(result.status)) {
354
+ continue;
355
+ }
356
+ const runtimeTask = runtimeTaskById.get(result.task_id);
357
+ if (!runtimeTask || runtimeTask.target_paths.length === 0) {
358
+ continue;
359
+ }
360
+ if (runtimeValidationHasStrongStaticFinding(runtimeTask, contexts)) {
361
+ continue;
362
+ }
363
+ const relatedTasks = existingTasks.filter((task) => !isDeepeningTask(task) && intersects(task.file_paths, runtimeTask.target_paths));
364
+ pushIfNew(buildRuntimeValidationFollowupTask({
365
+ runtimeTask,
366
+ runtimeResultStatus: result.status,
367
+ relatedTasks,
368
+ results: options.results,
369
+ lineIndex: options.lineIndex,
370
+ }));
371
+ }
372
+ const cleanResults = options.results
373
+ .map((result) => ({ result, task: taskById.get(result.task_id) }))
374
+ .filter(({ result, task }) => isHighRiskCleanResult(result, task))
375
+ .sort((a, b) => {
376
+ const priorityDelta = priorityRank(b.task?.priority) - priorityRank(a.task?.priority);
377
+ if (priorityDelta !== 0)
378
+ return priorityDelta;
379
+ return a.result.task_id.localeCompare(b.result.task_id);
380
+ });
381
+ for (const { result, task } of cleanResults) {
382
+ pushIfNew(buildHighRiskCleanFollowupTask({
383
+ result,
384
+ task,
385
+ lineIndex: options.lineIndex,
386
+ }));
387
+ }
388
+ return created;
389
+ }
390
+ export const selectiveDeepeningTestUtils = {
391
+ DEEPENING_TAG,
392
+ };
@@ -43,13 +43,18 @@ export function deriveAuditState(bundle) {
43
43
  "requeue_tasks.json",
44
44
  ], planningReady)));
45
45
  const hasRequiredCoverage = bundle.coverage_matrix?.files.every((f) => f.required_lenses.every((req) => f.completed_lenses.includes(req))) ?? true;
46
+ const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
47
+ const hasPendingAuditTasks = bundle.audit_tasks?.some((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id)) ?? false;
46
48
  const hasCompletedTaskStatuses = bundle.audit_tasks?.length
47
49
  ? bundle.audit_tasks.every((task) => task.status === "complete")
48
50
  : false;
49
51
  const hasResultForEveryTask = bundle.audit_tasks?.length && bundle.audit_results
50
52
  ? bundle.audit_tasks.every((task) => bundle.audit_results?.some((result) => result.task_id === task.task_id))
51
53
  : false;
52
- if (!hasRequiredCoverage &&
54
+ if (hasPendingAuditTasks) {
55
+ obligations.push(obligation("audit_tasks_completed", "missing"));
56
+ }
57
+ else if (!hasRequiredCoverage &&
53
58
  !hasCompletedTaskStatuses &&
54
59
  !hasResultForEveryTask &&
55
60
  has(bundle.audit_tasks) &&
@@ -11,6 +11,22 @@ export interface BuildChunkedTaskOptions {
11
11
  * splitting entirely.
12
12
  */
13
13
  file_split_threshold?: number;
14
+ /**
15
+ * Approximate total line budget for a review task. Multi-file blocks above
16
+ * this budget are split into multiple bounded review tasks. Default: 1500.
17
+ * Set to 0 to disable aggregate line-budget splitting.
18
+ */
19
+ max_task_lines?: number;
20
+ /**
21
+ * Maximum number of files in one review task. Default: 8. Set to 0 to
22
+ * disable aggregate file-count splitting.
23
+ */
24
+ max_task_files?: number;
25
+ /**
26
+ * Test files at or below this size can be batched across unit boundaries.
27
+ * Default: 250. Set to 0 to disable tiny-test batching.
28
+ */
29
+ tiny_test_file_lines?: number;
14
30
  limit_lenses?: Lens[];
15
31
  external_analyzer_results?: ExternalAnalyzerResults;
16
32
  critical_flows?: CriticalFlowManifest;
@@ -1,6 +1,7 @@
1
1
  import { claimFlowReviewBlocks } from "./flowPlanning.js";
2
2
  import { isTrivialAuditPath } from "./trivialAudit.js";
3
3
  import { LENS_ORDER } from "./unitBuilder.js";
4
+ import { isTestPath, normalizeExtractorPath, } from "../extractors/pathPatterns.js";
4
5
  function taskPriority(hasExternalSignal, lens, isCriticalFlow = false) {
5
6
  if (isCriticalFlow) {
6
7
  return lens === "security" || lens === "reliability" || lens === "correctness"
@@ -46,6 +47,10 @@ function pickAnalyzerLens(category) {
46
47
  return "correctness";
47
48
  }
48
49
  const DEFAULT_FILE_SPLIT_THRESHOLD = 3000;
50
+ const DEFAULT_MAX_TASK_LINES = 1500;
51
+ const DEFAULT_MAX_TASK_FILES = 8;
52
+ const DEFAULT_TINY_TEST_FILE_LINES = 250;
53
+ const TINY_TEST_UNIT_ID = "tests-tiny-files";
49
54
  function buildCoverageIndex(coverageMatrix) {
50
55
  return new Map(coverageMatrix.files.map((file) => [file.path, file]));
51
56
  }
@@ -71,6 +76,9 @@ function getExternalSignalResults(externalAnalyzerResults) {
71
76
  }
72
77
  export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options = {}) {
73
78
  const fileSplitThreshold = options.file_split_threshold ?? DEFAULT_FILE_SPLIT_THRESHOLD;
79
+ const maxTaskLines = options.max_task_lines ?? DEFAULT_MAX_TASK_LINES;
80
+ const maxTaskFiles = options.max_task_files ?? DEFAULT_MAX_TASK_FILES;
81
+ const tinyTestFileLines = options.tiny_test_file_lines ?? DEFAULT_TINY_TEST_FILE_LINES;
74
82
  const allowed = new Set(options.limit_lenses ?? []);
75
83
  const enforceLensFilter = allowed.size > 0;
76
84
  const tasks = [];
@@ -97,14 +105,48 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
97
105
  pendingByLens.set(lens, pending);
98
106
  }
99
107
  }
108
+ function chunkByTaskBudget(filePaths) {
109
+ if (filePaths.length === 0) {
110
+ return [];
111
+ }
112
+ if (maxTaskLines <= 0 && maxTaskFiles <= 0) {
113
+ return [filePaths];
114
+ }
115
+ const chunks = [];
116
+ let current = [];
117
+ let currentLines = 0;
118
+ for (const path of filePaths) {
119
+ const lineCount = unitLineIndex[path] ?? 0;
120
+ const wouldExceedFiles = maxTaskFiles > 0 && current.length >= maxTaskFiles;
121
+ const wouldExceedLines = maxTaskLines > 0 &&
122
+ current.length > 0 &&
123
+ currentLines + lineCount > maxTaskLines;
124
+ if (wouldExceedFiles || wouldExceedLines) {
125
+ chunks.push(current);
126
+ current = [];
127
+ currentLines = 0;
128
+ }
129
+ current.push(path);
130
+ currentLines += lineCount;
131
+ }
132
+ if (current.length > 0) {
133
+ chunks.push(current);
134
+ }
135
+ return chunks;
136
+ }
100
137
  function addTaskBlock(params) {
101
138
  const oversizedFiles = fileSplitThreshold > 0
102
139
  ? params.filePaths.filter((path) => (unitLineIndex[path] ?? 0) > fileSplitThreshold)
103
140
  : [];
104
141
  const oversizedSet = new Set(oversizedFiles);
105
142
  const normalFiles = params.filePaths.filter((path) => !oversizedSet.has(path));
106
- if (normalFiles.length > 0) {
107
- const taskId = `${params.scopeId}:${params.lens}`;
143
+ const normalChunks = chunkByTaskBudget(normalFiles);
144
+ for (let index = 0; index < normalChunks.length; index++) {
145
+ const chunk = normalChunks[index];
146
+ const splitKind = normalChunks.length > 1 ? "budget" : "none";
147
+ const taskId = splitKind === "budget"
148
+ ? `${params.scopeId}:${params.lens}:part-${index + 1}`
149
+ : `${params.scopeId}:${params.lens}`;
108
150
  if (!seen.has(taskId)) {
109
151
  seen.add(taskId);
110
152
  tasks.push({
@@ -112,10 +154,14 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
112
154
  unit_id: params.unitId,
113
155
  pass_id: params.passId,
114
156
  lens: params.lens,
115
- file_paths: normalFiles,
116
- rationale: params.rationale(normalFiles, false),
157
+ file_paths: chunk,
158
+ rationale: params.rationale(chunk, splitKind),
117
159
  priority: params.priority,
118
- tags: params.tags.length > 0 ? params.tags : undefined,
160
+ tags: splitKind === "budget"
161
+ ? [...new Set([...params.tags, "line_budget_split"])]
162
+ : params.tags.length > 0
163
+ ? params.tags
164
+ : undefined,
119
165
  });
120
166
  }
121
167
  }
@@ -131,7 +177,7 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
131
177
  pass_id: params.passId,
132
178
  lens: params.lens,
133
179
  file_paths: [filePath],
134
- rationale: params.rationale([filePath], true),
180
+ rationale: params.rationale([filePath], "large_file"),
135
181
  priority: params.priority,
136
182
  tags: params.tags.length > 0
137
183
  ? [...new Set([...params.tags, "large_file"])]
@@ -155,9 +201,11 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
155
201
  tags: hasExternalSignal
156
202
  ? ["critical_flow", `critical_flow:${block.flow_id}`, "external_analyzer_signal"]
157
203
  : ["critical_flow", `critical_flow:${block.flow_id}`],
158
- rationale: (filePaths, splitFromBlock) => splitFromBlock
204
+ rationale: (filePaths, splitKind) => splitKind === "large_file"
159
205
  ? `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." : ""}`,
206
+ : splitKind === "budget"
207
+ ? `Audit part of critical flow ${block.flow_id} (${filePaths.length} file${filePaths.length === 1 ? "" : "s"}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`
208
+ : `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
209
  });
162
210
  }
163
211
  const groupedRemainders = new Map();
@@ -170,8 +218,15 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
170
218
  if (assigned.has(`${lens}:${path}`)) {
171
219
  continue;
172
220
  }
221
+ const lineCount = unitLineIndex[path] ?? 0;
222
+ const isTinyTestReview = tinyTestFileLines > 0 &&
223
+ lineCount <= tinyTestFileLines &&
224
+ isTestPath(normalizeExtractorPath(path)) &&
225
+ !externalPaths.has(path);
173
226
  const record = coverageByPath.get(path);
174
- const unitId = record?.unit_ids[0] ?? `review:${path.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
227
+ const unitId = isTinyTestReview
228
+ ? TINY_TEST_UNIT_ID
229
+ : record?.unit_ids[0] ?? `review:${path.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
175
230
  const key = `${lens}|${unitId}`;
176
231
  const current = groupedRemainders.get(key) ?? {
177
232
  lens,
@@ -197,9 +252,11 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
197
252
  filePaths: block.filePaths,
198
253
  priority: taskPriority(hasExternalSignal, block.lens),
199
254
  tags: hasExternalSignal ? ["external_analyzer_signal"] : [],
200
- rationale: (filePaths, splitFromBlock) => splitFromBlock
255
+ rationale: (filePaths, splitKind) => splitKind === "large_file"
201
256
  ? `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." : ""}`,
257
+ : splitKind === "budget"
258
+ ? `Audit part of ${block.unitId} (${filePaths.length} file${filePaths.length === 1 ? "" : "s"}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`
259
+ : `Audit ${block.unitId} (${filePaths.length} file${filePaths.length === 1 ? "" : "s"}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
203
260
  });
204
261
  }
205
262
  return tasks.sort((a, b) => {
@@ -16,7 +16,8 @@ export function renderWorkerPrompt(task) {
16
16
  `Single-result schema: ${singleResultSchemaPath}`,
17
17
  "Scope: review only the tasks listed in the Read file. Do not add tasks,",
18
18
  "edit source files, remediate findings, run unrelated audits, or write result_path.",
19
- "For each listed task: read all file_paths in full, review under the specified lens,",
19
+ "For each listed task: read the assigned file_paths under the specified lens,",
20
+ "using targeted reads/searches where they give complete enough evidence without loading unrelated context,",
20
21
  "and emit exactly one AuditResult object with:",
21
22
  " task_id, unit_id, pass_id, lens (copy from task),",
22
23
  " file_coverage: [{path, total_lines}] — use file_line_counts[path] from the task for each file,",
@@ -21,7 +21,9 @@ export class ClaudeCodeProvider {
21
21
  "-p",
22
22
  prompt,
23
23
  ...(this.config.extra_args ?? []),
24
- "--dangerously-skip-permissions",
24
+ ...(this.config.dangerously_skip_permissions
25
+ ? ["--dangerously-skip-permissions"]
26
+ : []),
25
27
  ];
26
28
  return await this.launchCommand(command, args, input);
27
29
  }
@@ -9,7 +9,8 @@ function hasEntries(values) {
9
9
  }
10
10
  function hasConfiguredClaudeCode(sessionConfig) {
11
11
  return (Boolean(sessionConfig.claude_code?.command?.trim()) ||
12
- hasEntries(sessionConfig.claude_code?.extra_args));
12
+ hasEntries(sessionConfig.claude_code?.extra_args) ||
13
+ sessionConfig.claude_code?.dangerously_skip_permissions === true);
13
14
  }
14
15
  function hasConfiguredOpenCode(sessionConfig) {
15
16
  return (Boolean(sessionConfig.opencode?.command?.trim()) ||