auditor-lambda 0.3.4 → 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.
@@ -0,0 +1,217 @@
1
+ const MAX_ANCHORS = 160;
2
+ const KEYWORD_PATTERN = /\b(auth|token|password|secret|permission|role|sql|query|exec|spawn|eval|deserialize|encrypt|decrypt|cache|retry|timeout|transaction|lock|race|TODO|FIXME)\b/i;
3
+ const SYMBOL_PATTERNS = [
4
+ {
5
+ kind: "import",
6
+ pattern: /^\s*import\s+(?:type\s+)?(?:[^"'()]*?\s+from\s+)?["']([^"']+)["']/,
7
+ label: "import",
8
+ },
9
+ {
10
+ kind: "symbol",
11
+ pattern: /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\b/,
12
+ label: "function",
13
+ },
14
+ {
15
+ kind: "symbol",
16
+ pattern: /^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\b/,
17
+ label: "class",
18
+ },
19
+ {
20
+ kind: "symbol",
21
+ pattern: /^\s*(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)\b/,
22
+ label: "interface",
23
+ },
24
+ {
25
+ kind: "symbol",
26
+ pattern: /^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\b/,
27
+ label: "type",
28
+ },
29
+ {
30
+ kind: "symbol",
31
+ pattern: /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b/,
32
+ label: "binding",
33
+ },
34
+ {
35
+ kind: "export",
36
+ pattern: /^\s*export\s+(?:type\s+)?(?:[^"'()]*?\s+from\s+["']([^"']+)["']|(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type)\s+([A-Za-z_$][\w$-]*))/,
37
+ label: "export",
38
+ },
39
+ {
40
+ kind: "symbol",
41
+ pattern: /^\s*(?:export\s+)?def\s+([A-Za-z_][\w]*)\b/,
42
+ label: "function",
43
+ },
44
+ {
45
+ kind: "symbol",
46
+ pattern: /^\s*(?:export\s+)?func\s+(?:\([^)]+\)\s*)?([A-Za-z_][\w]*)\b/,
47
+ label: "function",
48
+ },
49
+ {
50
+ kind: "symbol",
51
+ pattern: /^\s*(?:pub\s+)?fn\s+([A-Za-z_][\w]*)\b/,
52
+ label: "function",
53
+ },
54
+ {
55
+ kind: "route",
56
+ pattern: /\b(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|use)\s*\(/i,
57
+ label: "route",
58
+ },
59
+ {
60
+ kind: "route",
61
+ pattern: /^\s*@(?:Get|Post|Put|Patch|Delete|Route|Controller)\b/,
62
+ label: "route",
63
+ },
64
+ ];
65
+ function normalizePath(path) {
66
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
67
+ }
68
+ function truncate(value, maxLength) {
69
+ const normalized = value.replace(/\s+/g, " ").trim();
70
+ return normalized.length > maxLength
71
+ ? `${normalized.slice(0, maxLength - 3)}...`
72
+ : normalized;
73
+ }
74
+ function addAnchor(anchors, seen, anchor) {
75
+ const key = `${anchor.kind}\0${anchor.line ?? ""}\0${anchor.name}\0${anchor.detail ?? ""}`;
76
+ if (seen.has(key)) {
77
+ return;
78
+ }
79
+ seen.add(key);
80
+ anchors.push(anchor);
81
+ }
82
+ function collectGraphEdges(graphBundle, path) {
83
+ if (!graphBundle?.graphs) {
84
+ return [];
85
+ }
86
+ const normalizedPath = normalizePath(path).toLowerCase();
87
+ const edges = [];
88
+ for (const key of ["imports", "calls", "references"]) {
89
+ const raw = graphBundle.graphs[key];
90
+ if (!Array.isArray(raw)) {
91
+ continue;
92
+ }
93
+ for (const item of raw) {
94
+ const record = item;
95
+ if (item &&
96
+ typeof item === "object" &&
97
+ !Array.isArray(item) &&
98
+ typeof record.from === "string" &&
99
+ typeof record.to === "string") {
100
+ const from = normalizePath(record.from).toLowerCase();
101
+ const to = normalizePath(record.to).toLowerCase();
102
+ if (from === normalizedPath || to === normalizedPath) {
103
+ edges.push({
104
+ from: record.from,
105
+ to: record.to,
106
+ kind: typeof record.kind === "string" ? record.kind : key,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ return edges.sort((a, b) => (a.kind ?? "").localeCompare(b.kind ?? "") ||
113
+ a.from.localeCompare(b.from) ||
114
+ a.to.localeCompare(b.to));
115
+ }
116
+ export function buildFileAnchorSummary(params) {
117
+ const anchors = [];
118
+ const seen = new Set();
119
+ const path = normalizePath(params.path);
120
+ const lines = params.content.split(/\r?\n/);
121
+ let symbolCount = 0;
122
+ let routeCount = 0;
123
+ let keywordCount = 0;
124
+ addAnchor(anchors, seen, {
125
+ kind: "boundary",
126
+ name: "file_start",
127
+ line: 1,
128
+ detail: "Start of isolated large-file review boundary.",
129
+ });
130
+ if (params.totalLines > 1) {
131
+ addAnchor(anchors, seen, {
132
+ kind: "boundary",
133
+ name: "file_end",
134
+ line: params.totalLines,
135
+ detail: "End of isolated large-file review boundary.",
136
+ });
137
+ }
138
+ lines.forEach((line, index) => {
139
+ const lineNumber = index + 1;
140
+ for (const { kind, pattern, label } of SYMBOL_PATTERNS) {
141
+ const match = line.match(pattern);
142
+ if (!match) {
143
+ continue;
144
+ }
145
+ const name = match.slice(1).find((value) => value && value.trim().length > 0) ?? label;
146
+ if (kind === "route") {
147
+ routeCount += 1;
148
+ }
149
+ else if (kind === "symbol") {
150
+ symbolCount += 1;
151
+ }
152
+ addAnchor(anchors, seen, {
153
+ kind,
154
+ name: truncate(name, 80),
155
+ line: lineNumber,
156
+ detail: truncate(`${label}: ${line}`, 180),
157
+ });
158
+ break;
159
+ }
160
+ if (KEYWORD_PATTERN.test(line)) {
161
+ keywordCount += 1;
162
+ addAnchor(anchors, seen, {
163
+ kind: "keyword",
164
+ name: truncate(line.match(KEYWORD_PATTERN)?.[1] ?? "keyword", 80),
165
+ line: lineNumber,
166
+ detail: truncate(line, 180),
167
+ });
168
+ }
169
+ });
170
+ const graphEdges = collectGraphEdges(params.graphBundle, path);
171
+ for (const edge of graphEdges) {
172
+ addAnchor(anchors, seen, {
173
+ kind: "graph",
174
+ name: edge.kind ?? "edge",
175
+ detail: normalizePath(edge.from).toLowerCase() === path.toLowerCase()
176
+ ? `outbound: ${edge.to}`
177
+ : `inbound: ${edge.from}`,
178
+ });
179
+ }
180
+ const analyzerSignals = (params.externalAnalyzerResults?.results ?? [])
181
+ .filter((result) => normalizePath(result.path).toLowerCase() === path.toLowerCase())
182
+ .sort((a, b) => (a.line_start ?? 0) - (b.line_start ?? 0) ||
183
+ a.id.localeCompare(b.id));
184
+ for (const signal of analyzerSignals) {
185
+ addAnchor(anchors, seen, {
186
+ kind: "analyzer_signal",
187
+ name: truncate(signal.rule ?? signal.category, 80),
188
+ line: signal.line_start,
189
+ detail: truncate(signal.summary, 180),
190
+ });
191
+ }
192
+ const sorted = anchors.sort((a, b) => (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER) ||
193
+ a.kind.localeCompare(b.kind) ||
194
+ a.name.localeCompare(b.name));
195
+ const boundedAnchors = sorted.slice(0, MAX_ANCHORS);
196
+ return {
197
+ contract_version: "audit-code-file-anchors/v1alpha1",
198
+ path,
199
+ total_lines: params.totalLines,
200
+ review_mode: "isolated_large_file",
201
+ scope_basis: [
202
+ "single assigned file",
203
+ "single review packet",
204
+ "mechanically extracted symbols, routes, graph edges, keywords, and analyzer signals",
205
+ "backend-owned submit-packet result write path",
206
+ ],
207
+ anchors: boundedAnchors,
208
+ omitted_anchor_count: Math.max(0, sorted.length - boundedAnchors.length),
209
+ counts: {
210
+ symbols: symbolCount,
211
+ routes: routeCount,
212
+ keywords: keywordCount,
213
+ graph_edges: graphEdges.length,
214
+ analyzer_signals: analyzerSignals.length,
215
+ },
216
+ };
217
+ }
@@ -105,6 +105,16 @@ function chunkPacketTasks(tasks, options) {
105
105
  const chunks = [];
106
106
  let current = [];
107
107
  for (const task of tasks.sort(compareTasksForPacket)) {
108
+ const isolatedLargeFileTask = task.file_paths.length === 1 &&
109
+ taskLineCount(task, options.lineIndex) > options.targetPacketLines;
110
+ if (isolatedLargeFileTask) {
111
+ if (current.length > 0) {
112
+ chunks.push(current);
113
+ current = [];
114
+ }
115
+ chunks.push([task]);
116
+ continue;
117
+ }
108
118
  const candidate = [...current, task];
109
119
  const uniquePaths = new Set(candidate.flatMap((item) => item.file_paths));
110
120
  const candidateLines = [...uniquePaths].reduce((sum, path) => {
@@ -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()) ||
@@ -69,13 +69,7 @@ function buildSuggestedInputs(artifactsDir, status, isConfigError, activeReviewR
69
69
  return [];
70
70
  }
71
71
  if (activeReviewRun) {
72
- return [
73
- {
74
- flag: "--results",
75
- suggested_path: activeReviewRun.audit_results_path,
76
- description: "Write structured audit-review results for the currently dispatched run, then execute the exact worker command below to ingest them.",
77
- },
78
- ];
72
+ return [];
79
73
  }
80
74
  const incomingDir = join(artifactsDir, INCOMING_DIRNAME);
81
75
  return [
@@ -101,12 +95,21 @@ function buildSuggestedInputs(artifactsDir, status, isConfigError, activeReviewR
101
95
  },
102
96
  ];
103
97
  }
104
- function buildSuggestedCommands(suggestedInputs, status, activeReviewRun) {
98
+ function buildSuggestedCommands(artifactsDir, suggestedInputs, status, activeReviewRun) {
105
99
  if (status !== BLOCKED_STATUS) {
106
100
  return [];
107
101
  }
108
102
  if (activeReviewRun) {
109
- return [renderShellCommand(activeReviewRun.worker_command)];
103
+ return [
104
+ renderShellCommand([
105
+ "audit-code",
106
+ "prepare-dispatch",
107
+ "--run-id",
108
+ activeReviewRun.run_id,
109
+ "--artifacts-dir",
110
+ artifactsDir,
111
+ ]),
112
+ ];
110
113
  }
111
114
  return suggestedInputs.map((item) => `audit-code ${item.flag} ${quoteShellPath(item.suggested_path)}`);
112
115
  }
@@ -216,17 +219,25 @@ export function buildAuditCodeHandoff(params) {
216
219
  summary: buildSummary(params.state.status, params.providerName ?? null, params.progressSummary),
217
220
  pending_obligations: buildPendingObligations(params.state),
218
221
  suggested_inputs: suggestedInputs,
219
- suggested_commands: buildSuggestedCommands(suggestedInputs, params.state.status, params.activeReviewRun),
222
+ suggested_commands: buildSuggestedCommands(params.artifactsDir, suggestedInputs, params.state.status, params.activeReviewRun),
220
223
  interactive_provider_hint: buildInteractiveProviderHint(params.state.status, params.providerName ?? null, artifactPaths.session_config, isConfigError),
221
224
  artifact_paths: artifactPaths,
222
225
  active_review_run: params.activeReviewRun,
223
226
  };
224
227
  // Add quick_start command and file map when blocked for review
225
228
  if (params.state.status === BLOCKED_STATUS && params.activeReviewRun) {
226
- handoff.quick_start = `audit-code worker-run --task ${params.activeReviewRun.task_path}`;
229
+ handoff.quick_start = renderShellCommand([
230
+ "audit-code",
231
+ "prepare-dispatch",
232
+ "--run-id",
233
+ params.activeReviewRun.run_id,
234
+ "--artifacts-dir",
235
+ params.artifactsDir,
236
+ ]);
227
237
  handoff.file_map = {
228
238
  current_task: artifactPaths.current_task,
229
239
  current_prompt: artifactPaths.current_prompt,
240
+ dispatch_plan: join(params.artifactsDir, "runs", params.activeReviewRun.run_id, "dispatch-plan.json"),
230
241
  audit_results: params.activeReviewRun.audit_results_path,
231
242
  final_report: join(params.root, "audit-report.md"),
232
243
  };
@@ -10,6 +10,7 @@ export interface SubprocessTemplateConfig {
10
10
  export interface ClaudeCodeConfig {
11
11
  command?: string;
12
12
  extra_args?: string[];
13
+ dangerously_skip_permissions?: boolean;
13
14
  }
14
15
  export interface OpenCodeConfig {
15
16
  command?: string;
@@ -57,6 +57,20 @@ function validateRequiredStringField(value, label, taskId, resultIndex, issues)
57
57
  });
58
58
  }
59
59
  }
60
+ function validateExpectedStringField(value, label, expected, taskId, resultIndex, issues) {
61
+ if (typeof value !== "string" || value.trim().length === 0) {
62
+ return;
63
+ }
64
+ if (value !== expected) {
65
+ pushIssue(issues, {
66
+ result_index: resultIndex,
67
+ task_id: taskId,
68
+ field: label,
69
+ message: `${label} must match the assigned task metadata ` +
70
+ `(expected '${expected}', got '${value}').`,
71
+ });
72
+ }
73
+ }
60
74
  function validateFinding(finding, label, taskId, resultIndex) {
61
75
  const issues = [];
62
76
  if (!isRecord(finding)) {
@@ -237,6 +251,11 @@ export function validateAuditResults(results, tasks, options = {}) {
237
251
  message: `Invalid lens '${result.lens}'. Must be one of: ${[...VALID_LENSES].join(", ")}.`,
238
252
  });
239
253
  }
254
+ if (task) {
255
+ validateExpectedStringField(result.unit_id, "unit_id", task.unit_id, taskId, i, issues);
256
+ validateExpectedStringField(result.pass_id, "pass_id", task.pass_id, taskId, i, issues);
257
+ validateExpectedStringField(result.lens, "lens", task.lens, taskId, i, issues);
258
+ }
240
259
  if (tasks.length > 0 && !task) {
241
260
  pushIssue(issues, {
242
261
  result_index: i,
@@ -248,6 +267,7 @@ export function validateAuditResults(results, tasks, options = {}) {
248
267
  }
249
268
  const fileCoverage = result.file_coverage;
250
269
  const normalizedFileCoverage = [];
270
+ const declaredAssignedCoveragePaths = new Set();
251
271
  if (!Array.isArray(fileCoverage) || fileCoverage.length === 0) {
252
272
  pushIssue(issues, {
253
273
  result_index: i,
@@ -281,7 +301,6 @@ export function validateAuditResults(results, tasks, options = {}) {
281
301
  pushIssue(issues, {
282
302
  result_index: i,
283
303
  task_id: taskId,
284
- severity: "warning",
285
304
  field: `file_coverage[${j}].path`,
286
305
  message: `file_coverage path '${entry.path}' is not listed in the task file_paths.`,
287
306
  });
@@ -297,6 +316,10 @@ export function validateAuditResults(results, tasks, options = {}) {
297
316
  else {
298
317
  seenCoveragePaths.add(entry.path);
299
318
  }
319
+ if (isNonEmptyString(entry.path) &&
320
+ (!task || task.file_paths.includes(entry.path))) {
321
+ declaredAssignedCoveragePaths.add(entry.path);
322
+ }
300
323
  if (!Number.isInteger(entry.total_lines)) {
301
324
  pushIssue(issues, {
302
325
  result_index: i,
@@ -330,7 +353,8 @@ export function validateAuditResults(results, tasks, options = {}) {
330
353
  }
331
354
  if (isNonEmptyString(entry.path) &&
332
355
  Number.isInteger(entry.total_lines) &&
333
- Number(entry.total_lines) >= 0) {
356
+ Number(entry.total_lines) >= 0 &&
357
+ (!task || task.file_paths.includes(entry.path))) {
334
358
  normalizedFileCoverage.push({
335
359
  path: entry.path,
336
360
  total_lines: Number(entry.total_lines),
@@ -367,11 +391,35 @@ export function validateAuditResults(results, tasks, options = {}) {
367
391
  if (!isRecord(finding) || !Array.isArray(finding.affected_files)) {
368
392
  continue;
369
393
  }
394
+ const expectedFindingLens = task?.lens ??
395
+ (typeof result.lens === "string" && VALID_LENSES.has(result.lens)
396
+ ? result.lens
397
+ : undefined);
398
+ if (expectedFindingLens &&
399
+ typeof finding.lens === "string" &&
400
+ finding.lens !== expectedFindingLens) {
401
+ pushIssue(issues, {
402
+ result_index: i,
403
+ task_id: taskId,
404
+ field: `${label}.lens`,
405
+ message: `${label}.lens must match the assigned task lens ` +
406
+ `(expected '${expectedFindingLens}', got '${finding.lens}').`,
407
+ });
408
+ }
370
409
  for (let k = 0; k < finding.affected_files.length; k++) {
371
410
  const affected = finding.affected_files[k];
372
411
  if (!isRecord(affected) || !isNonEmptyString(affected.path)) {
373
412
  continue;
374
413
  }
414
+ if (!declaredAssignedCoveragePaths.has(affected.path)) {
415
+ pushIssue(issues, {
416
+ result_index: i,
417
+ task_id: taskId,
418
+ field: `${label}.affected_files[${k}].path`,
419
+ message: `affected_files path '${affected.path}' is not in the declared assigned file_coverage.`,
420
+ });
421
+ continue;
422
+ }
375
423
  if (!Number.isInteger(affected.line_start)) {
376
424
  continue;
377
425
  }
@@ -74,6 +74,11 @@ function validateAgentProviderSection(value, path, issues) {
74
74
  if (value.extra_args !== undefined) {
75
75
  validateStringArray(value.extra_args, `${path}.extra_args`, "extra_args", issues, { allowEmptyArray: true });
76
76
  }
77
+ if (path === "claude_code" &&
78
+ value.dangerously_skip_permissions !== undefined &&
79
+ typeof value.dangerously_skip_permissions !== "boolean") {
80
+ pushIssue(issues, `${path}.dangerously_skip_permissions`, "dangerously_skip_permissions must be a boolean when provided.");
81
+ }
77
82
  }
78
83
  function commandExists(command) {
79
84
  const lookupCommand = process.platform === "win32" ? "where" : "which";
@@ -277,4 +277,7 @@ For a polished operator experience today:
277
277
  4. prefer `local-subprocess` unless you explicitly want a backend provider bridge
278
278
  5. use `subprocess-template` only when integrating a non-native editor or launcher surface
279
279
 
280
- If you intentionally want the backend fallback to bridge semantic review into another process, re-run with an explicit `--provider` flag after configuring the matching section in `.audit-artifacts/session-config.json`.
280
+ If you intentionally want the backend fallback to bridge semantic review into
281
+ another process, set the matching provider in
282
+ `.audit-artifacts/session-config.json` or re-run with an explicit `--provider`
283
+ flag after configuring the matching provider section.
package/docs/contract.md CHANGED
@@ -25,7 +25,10 @@ Workers submit `AuditResult[]` shaped by `schemas/audit_result.schema.json`.
25
25
  Important rules:
26
26
 
27
27
  - `file_coverage` is required and must include every assigned file.
28
+ - `file_coverage` must not include files outside the assigned task.
28
29
  - `file_coverage[].total_lines` must match the current file line count.
30
+ - `task_id`, `unit_id`, `pass_id`, and `lens` must match the assigned task.
31
+ - each finding lens must match the assigned task lens.
29
32
  - `findings[].affected_files` must be objects, not strings.
30
33
  - `findings[].evidence` must be an array of plain strings.
31
34
 
@@ -3,8 +3,8 @@
3
3
  This document describes the implemented review-dispatch path for `/audit-code`.
4
4
  The original dispatch plan was one agent per audit task. The current path keeps
5
5
  the existing `AuditTask` and `AuditResult` contracts, but groups related tasks
6
- into review packets so a worker can read a coherent file set once and produce
7
- one validated result file for each assigned task.
6
+ into review packets so a worker can read a coherent file set once and submit
7
+ one validated result for each assigned task through a backend-owned write path.
8
8
 
9
9
  ## Current Workflow
10
10
 
@@ -15,15 +15,16 @@ one validated result file for each assigned task.
15
15
 
16
16
  2. audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
17
17
  -> reads pending-audit-tasks.json and review planning artifacts
18
- -> writes dispatch-plan.json
18
+ -> writes a slim dispatch-plan.json
19
+ -> writes backend-owned dispatch-result-map.json
19
20
  -> writes one packet prompt per dispatch-plan entry
20
21
  -> prints one compact JSON envelope
21
22
 
22
23
  3. Conversation orchestrator reads only dispatch-plan.json
23
24
  -> launches one subagent per packet
24
25
  -> each subagent reads its packet prompt and assigned files
25
- -> each subagent writes one task-results/<task_id>.json per underlying task
26
- -> each subagent runs the validation commands in the prompt
26
+ -> each subagent pipes AuditResult[] to the submit-packet command in the prompt
27
+ -> submit-packet validates and writes only backend-assigned result files
27
28
  -> each subagent replies: valid: <packet_id>, findings=<n>
28
29
 
29
30
  4. audit-code merge-and-ingest --run-id <run_id> --artifacts-dir <artifacts_dir>
@@ -62,6 +63,7 @@ Packet planning is deterministic and compatibility-preserving:
62
63
  - graph edges from imports, calls, and references can merge related task groups
63
64
  - heuristic container edges do not force packet expansion
64
65
  - packet chunking respects task-count and line-budget limits
66
+ - a single file that exceeds the packet target is isolated rather than split
65
67
  - high-priority packets sort ahead of lower-priority packets
66
68
 
67
69
  Generated packets include:
@@ -88,7 +90,10 @@ audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
88
90
  Artifacts:
89
91
 
90
92
  - `<artifacts_dir>/runs/<run_id>/dispatch-plan.json`
93
+ - `<artifacts_dir>/runs/<run_id>/dispatch-result-map.json`
91
94
  - `<artifacts_dir>/runs/<run_id>/task-results/<packet_id>.prompt.md`
95
+ - `<artifacts_dir>/runs/<run_id>/task-results/<packet_id>.anchors.json`,
96
+ only for isolated large-file packets
92
97
  - `<artifacts_dir>/runs/<run_id>/dispatch-warnings.json`, only when warnings
93
98
  exist
94
99
 
@@ -115,18 +120,8 @@ The command prints a compact JSON envelope:
115
120
  ```json
116
121
  {
117
122
  "packet_id": "src-auth:security-correctness:packet-1-...",
118
- "task_id": "src-auth:security-correctness:packet-1-...",
119
- "task_ids": ["src-auth:security", "src-auth:correctness"],
120
123
  "description": "Audit 2 file(s), 2 task(s), 2 lens(es) (~70 lines)",
121
- "output_paths": {
122
- "src-auth:security": ".audit-artifacts/runs/run-1/task-results/src-auth_security.json",
123
- "src-auth:correctness": ".audit-artifacts/runs/run-1/task-results/src-auth_correctness.json"
124
- },
125
- "prompt_path": ".audit-artifacts/runs/run-1/task-results/src-auth_security-correctness_packet-1.prompt.md",
126
- "lenses": ["security", "correctness"],
127
- "file_paths": ["src/api/auth.ts", "src/lib/session.ts"],
128
- "total_lines": 70,
129
- "estimated_tokens": 1180
124
+ "prompt_path": ".audit-artifacts/runs/run-1/task-results/src-auth_security-correctness_packet-1_ab12cd34ef56.prompt.md"
130
125
  }
131
126
  ```
132
127
 
@@ -134,6 +129,27 @@ The orchestrator should launch one subagent per entry with the entry
134
129
  description and a prompt that tells the subagent to read and follow
135
130
  `entry.prompt_path`.
136
131
 
132
+ ## Large File Mode
133
+
134
+ The workflow does not impose a hard single-file size limit. When a packet is
135
+ large because it contains one large file, `prepare-dispatch` keeps that file in
136
+ an isolated packet and writes a mechanical anchor summary next to the packet
137
+ prompt. The anchor summary may include:
138
+
139
+ - file boundaries
140
+ - imports and exports
141
+ - top-level symbols
142
+ - route-like declarations
143
+ - risk keywords
144
+ - graph edges
145
+ - external analyzer signals
146
+
147
+ The packet prompt points the worker at the anchor file and asks for targeted
148
+ reads/searches within the assigned file. The backend still validates and writes
149
+ results through `submit-packet`. This keeps large-file review bounded by
150
+ mechanically generated structure without slicing files into arbitrary line
151
+ ranges.
152
+
137
153
  ## Packet Prompt Contract
138
154
 
139
155
  Each packet prompt tells the worker to:
@@ -141,20 +157,31 @@ Each packet prompt tells the worker to:
141
157
  - review the packet once
142
158
  - read only the listed repo-relative files
143
159
  - produce one JSON object per listed task
144
- - write each object to that task's exact `output_path`
160
+ - pipe one JSON array to the prompt's `submit-packet` command
145
161
  - preserve the existing `AuditResult` fields:
146
162
  `task_id`, `unit_id`, `pass_id`, `lens`, `file_coverage`, `findings`
147
163
  - keep `file_coverage[]` as `{ path, total_lines }`
148
164
  - keep every finding lens equal to the task lens
149
- - avoid source edits, remediation, extra task results, and unrelated audits
150
- - run the generated validation command for every task result
151
- - reply exactly `valid: <packet_id>, findings=<total finding count>` after all
152
- validation commands pass
165
+ - avoid direct file writes, source edits, remediation, extra task results, and
166
+ unrelated audits
167
+ - reply exactly `valid: <packet_id>, findings=<total finding count>` after the
168
+ submit command accepts the packet
153
169
 
154
170
  This keeps packet review efficient while leaving merge and ingestion
155
171
  mechanically deterministic.
156
172
 
157
- ## Validation
173
+ ## Submission and Validation
174
+
175
+ Packet submission is exposed through:
176
+
177
+ ```bash
178
+ audit-code submit-packet --run-id <run_id> --packet-id <packet_id> --artifacts-dir <artifacts_dir>
179
+ ```
180
+
181
+ The command reads `AuditResult[]` from stdin, validates the complete assigned
182
+ packet, and writes only the backend-assigned per-task result paths from
183
+ `dispatch-result-map.json`. This keeps result writes out of the LLM prompt and
184
+ prevents swapped or unknown task result files from being ingested.
158
185
 
159
186
  Per-task validation is exposed through:
160
187
 
@@ -162,6 +189,10 @@ Per-task validation is exposed through:
162
189
  audit-code validate-result --run-id <run_id> --task-id <task_id> --artifacts-dir <artifacts_dir>
163
190
  ```
164
191
 
192
+ Generated packet prompts may pass run ids, packet ids, task ids, and artifact paths through
193
+ base64url flags such as `--run-id-b64`, `--packet-id-b64`, `--task-id-b64`, and
194
+ `--artifacts-dir-b64` when raw values could contain shell-sensitive characters.
195
+
165
196
  The validator checks the result against the assigned task set and enforces the
166
197
  mechanical constraints that matter for ingestion:
167
198
 
@@ -171,7 +202,7 @@ mechanical constraints that matter for ingestion:
171
202
  - line spans do not exceed known `total_lines`
172
203
  - result fields conform to the shipped schemas
173
204
 
174
- Workers should retry invalid JSON up to the bounded retry count in the prompt.
205
+ Workers should retry rejected submissions up to the bounded retry count in the prompt.
175
206
 
176
207
  ## `merge-and-ingest` Output
177
208
 
@@ -183,7 +214,9 @@ audit-code merge-and-ingest --run-id <run_id> --artifacts-dir <artifacts_dir>
183
214
 
184
215
  Merge behavior:
185
216
 
186
- - validates every JSON file under `task-results/`
217
+ - validates every backend-assigned result-map path
218
+ - rejects unexpected JSON files under `task-results/`
219
+ - rejects task IDs that appear in the wrong assigned result path
187
220
  - rejects duplicate task results
188
221
  - rejects unknown task IDs
189
222
  - rejects missing assigned task results
package/docs/run-flow.md CHANGED
@@ -14,11 +14,13 @@ This document describes the backend execution flow that supports that conversati
14
14
  4. Build `review_packets.json` and `audit_plan_metrics.json` from those tasks.
15
15
  5. Stop at semantic review with an active run handoff.
16
16
  6. `prepare-dispatch` writes a small run-scoped `dispatch-plan.json` and one
17
- prompt per review packet.
17
+ prompt per review packet, plus a backend-owned result map.
18
+ Isolated large-file packets also get mechanical anchor summaries for
19
+ targeted review.
18
20
  7. The active conversation orchestrator launches one bounded subagent per
19
21
  packet when the host supports subagents.
20
- 8. Each subagent writes one validated `AuditResult` JSON object per underlying
21
- task.
22
+ 8. Each subagent pipes `AuditResult[]` to the packet's `submit-packet` command;
23
+ the backend validates and writes assigned result files.
22
24
  9. `merge-and-ingest` validates the full assigned task set and ingests the
23
25
  existing `AuditResult[]` shape.
24
26
  10. Result ingestion updates coverage, requeue, runtime-validation state, and