auditor-lambda 0.10.3 → 0.10.7

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 (183) hide show
  1. package/audit-code-wrapper-build.mjs +198 -0
  2. package/audit-code-wrapper-install-hosts.mjs +1140 -0
  3. package/audit-code-wrapper-io.mjs +155 -0
  4. package/audit-code-wrapper-legacy.mjs +125 -0
  5. package/audit-code-wrapper-lib.mjs +17 -1801
  6. package/audit-code-wrapper-opencode.mjs +256 -0
  7. package/dispatch/merge-results.mjs +5 -3
  8. package/dispatch/validate-result.mjs +2 -2
  9. package/dist/adapters/coverageSummary.js +6 -2
  10. package/dist/adapters/normalizeExternal.js +16 -1
  11. package/dist/adapters/npmAudit.js +20 -9
  12. package/dist/adapters/semgrep.js +26 -1
  13. package/dist/cli/advanceAuditCommand.d.ts +1 -0
  14. package/dist/cli/advanceAuditCommand.js +95 -0
  15. package/dist/cli/args.js +1 -2
  16. package/dist/cli/auditStep.js +2 -2
  17. package/dist/cli/cleanup.d.ts +11 -1
  18. package/dist/cli/cleanup.js +25 -5
  19. package/dist/cli/cleanupCommand.d.ts +1 -0
  20. package/dist/cli/cleanupCommand.js +24 -0
  21. package/dist/cli/dispatch.d.ts +55 -31
  22. package/dist/cli/dispatch.js +298 -241
  23. package/dist/cli/dispatchStatusCommand.d.ts +1 -0
  24. package/dist/cli/dispatchStatusCommand.js +68 -0
  25. package/dist/cli/explainTaskCommand.d.ts +1 -0
  26. package/dist/cli/explainTaskCommand.js +33 -0
  27. package/dist/cli/importExternalAnalyzerCommand.d.ts +1 -0
  28. package/dist/cli/importExternalAnalyzerCommand.js +20 -0
  29. package/dist/cli/ingestResultsCommand.d.ts +1 -0
  30. package/dist/cli/ingestResultsCommand.js +34 -0
  31. package/dist/cli/intakeCommand.d.ts +1 -0
  32. package/dist/cli/intakeCommand.js +17 -0
  33. package/dist/cli/lineIndex.js +19 -12
  34. package/dist/cli/nextStepCommand.d.ts +139 -0
  35. package/dist/cli/nextStepCommand.js +281 -232
  36. package/dist/cli/planCommand.d.ts +1 -0
  37. package/dist/cli/planCommand.js +16 -0
  38. package/dist/cli/prepareDispatchCommand.d.ts +1 -0
  39. package/dist/cli/prepareDispatchCommand.js +25 -0
  40. package/dist/cli/quotaCommand.d.ts +1 -0
  41. package/dist/cli/quotaCommand.js +56 -0
  42. package/dist/cli/requeueCommand.d.ts +1 -0
  43. package/dist/cli/requeueCommand.js +10 -0
  44. package/dist/cli/runToCompletion.js +451 -412
  45. package/dist/cli/sampleRunCommand.d.ts +1 -0
  46. package/dist/cli/sampleRunCommand.js +93 -0
  47. package/dist/cli/statusCommand.js +1 -1
  48. package/dist/cli/steps.js +4 -1
  49. package/dist/cli/submitPacketCommand.js +16 -15
  50. package/dist/cli/synthesizeCommand.d.ts +1 -0
  51. package/dist/cli/synthesizeCommand.js +15 -0
  52. package/dist/cli/updateRuntimeValidationCommand.d.ts +1 -0
  53. package/dist/cli/updateRuntimeValidationCommand.js +16 -0
  54. package/dist/cli/validateCommand.d.ts +1 -0
  55. package/dist/cli/validateCommand.js +41 -0
  56. package/dist/cli/validateResultCommand.d.ts +1 -0
  57. package/dist/cli/validateResultCommand.js +63 -0
  58. package/dist/cli/validateResultsCommand.d.ts +1 -0
  59. package/dist/cli/validateResultsCommand.js +31 -0
  60. package/dist/cli/workerRunCommand.d.ts +15 -1
  61. package/dist/cli/workerRunCommand.js +40 -4
  62. package/dist/cli.d.ts +3 -2
  63. package/dist/cli.js +21 -628
  64. package/dist/coverage.js +7 -3
  65. package/dist/extractors/analyzers/css.js +2 -2
  66. package/dist/extractors/analyzers/html.js +2 -2
  67. package/dist/extractors/analyzers/python.js +2 -2
  68. package/dist/extractors/analyzers/registry.js +17 -36
  69. package/dist/extractors/analyzers/treeSitter.d.ts +10 -1
  70. package/dist/extractors/analyzers/treeSitter.js +28 -6
  71. package/dist/extractors/analyzers/typescript.js +104 -85
  72. package/dist/extractors/browserExtension.js +4 -1
  73. package/dist/extractors/designAssessment.js +21 -21
  74. package/dist/extractors/fsIntake.js +34 -10
  75. package/dist/extractors/graph.js +17 -7
  76. package/dist/extractors/graphManifestEdges/cargo.d.ts +4 -0
  77. package/dist/extractors/graphManifestEdges/cargo.js +107 -0
  78. package/dist/extractors/graphManifestEdges/go.d.ts +5 -0
  79. package/dist/extractors/graphManifestEdges/go.js +151 -0
  80. package/dist/extractors/graphManifestEdges/index.d.ts +8 -0
  81. package/dist/extractors/graphManifestEdges/index.js +11 -0
  82. package/dist/extractors/graphManifestEdges/jsonc.d.ts +3 -0
  83. package/dist/extractors/graphManifestEdges/jsonc.js +97 -0
  84. package/dist/extractors/graphManifestEdges/maven.d.ts +3 -0
  85. package/dist/extractors/graphManifestEdges/maven.js +73 -0
  86. package/dist/extractors/graphManifestEdges/packageJson.d.ts +19 -0
  87. package/dist/extractors/graphManifestEdges/packageJson.js +204 -0
  88. package/dist/extractors/graphManifestEdges/pnpm.d.ts +2 -0
  89. package/dist/extractors/graphManifestEdges/pnpm.js +42 -0
  90. package/dist/extractors/graphManifestEdges/pyproject.d.ts +3 -0
  91. package/dist/extractors/graphManifestEdges/pyproject.js +83 -0
  92. package/dist/extractors/graphManifestEdges/toml.d.ts +4 -0
  93. package/dist/extractors/graphManifestEdges/toml.js +68 -0
  94. package/dist/extractors/graphManifestEdges/typescript.d.ts +3 -0
  95. package/dist/extractors/graphManifestEdges/typescript.js +56 -0
  96. package/dist/extractors/graphManifestEdges/workspace.d.ts +10 -0
  97. package/dist/extractors/graphManifestEdges/workspace.js +72 -0
  98. package/dist/extractors/graphManifestEdges/yaml.d.ts +3 -0
  99. package/dist/extractors/graphManifestEdges/yaml.js +59 -0
  100. package/dist/extractors/graphManifestEdges/yamlPaths.d.ts +4 -0
  101. package/dist/extractors/graphManifestEdges/yamlPaths.js +89 -0
  102. package/dist/extractors/graphPythonImports.js +4 -20
  103. package/dist/extractors/pathPatterns.js +3 -13
  104. package/dist/io/artifacts.d.ts +1 -1
  105. package/dist/io/artifacts.js +4 -1
  106. package/dist/io/runArtifacts.d.ts +8 -2
  107. package/dist/io/runArtifacts.js +103 -69
  108. package/dist/io/toolingManifest.js +2 -1
  109. package/dist/orchestrator/advance.js +36 -0
  110. package/dist/orchestrator/artifactFreshness.d.ts +1 -1
  111. package/dist/orchestrator/artifactFreshness.js +1 -1
  112. package/dist/orchestrator/artifactMetadata.js +5 -5
  113. package/dist/orchestrator/auditTaskUtils.d.ts +4 -0
  114. package/dist/orchestrator/auditTaskUtils.js +8 -12
  115. package/dist/orchestrator/autoFixExecutor.js +40 -26
  116. package/dist/orchestrator/dependencyMap.js +1 -1
  117. package/dist/orchestrator/executorResult.d.ts +33 -0
  118. package/dist/orchestrator/executors.d.ts +7 -0
  119. package/dist/orchestrator/executors.js +24 -0
  120. package/dist/orchestrator/fileAnchors.js +42 -29
  121. package/dist/orchestrator/fileIntegrity.js +6 -1
  122. package/dist/orchestrator/flowCoverage.js +1 -2
  123. package/dist/orchestrator/flowPlanning.js +8 -4
  124. package/dist/orchestrator/graphEnrichmentExecutor.js +67 -45
  125. package/dist/orchestrator/ingestionExecutors.js +9 -1
  126. package/dist/orchestrator/intakeExecutors.d.ts +0 -4
  127. package/dist/orchestrator/intakeExecutors.js +24 -14
  128. package/dist/orchestrator/localCommands.d.ts +1 -0
  129. package/dist/orchestrator/localCommands.js +10 -17
  130. package/dist/orchestrator/nextStep.js +3 -1
  131. package/dist/orchestrator/requeueCommand.js +4 -0
  132. package/dist/orchestrator/reviewPacketGraph.js +50 -18
  133. package/dist/orchestrator/reviewPackets.js +10 -8
  134. package/dist/orchestrator/runtimeCommand.js +35 -7
  135. package/dist/orchestrator/runtimeValidationUpdate.js +6 -0
  136. package/dist/orchestrator/selectiveDeepening/highRiskClean.js +3 -2
  137. package/dist/orchestrator/selectiveDeepening/lensVerification.js +44 -18
  138. package/dist/orchestrator/staleness.js +3 -3
  139. package/dist/orchestrator/state.js +1 -1
  140. package/dist/orchestrator/syntaxResolutionExecutor.js +17 -24
  141. package/dist/orchestrator/synthesisExecutors.js +1 -0
  142. package/dist/orchestrator/taskBuilder.js +5 -4
  143. package/dist/providers/claudeCodeProvider.js +4 -1
  144. package/dist/providers/opencodeProvider.js +4 -1
  145. package/dist/quota/discoveredLimits.js +3 -3
  146. package/dist/quota/headerExtraction.js +5 -2
  147. package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.js +3 -0
  148. package/dist/quota/headerExtractors/index.js +3 -3
  149. package/dist/quota/index.d.ts +3 -1
  150. package/dist/quota/index.js +3 -0
  151. package/dist/reporting/findingRanks.d.ts +3 -0
  152. package/dist/reporting/findingRanks.js +24 -0
  153. package/dist/reporting/mergeFindings.js +1 -24
  154. package/dist/reporting/synthesis.d.ts +3 -1
  155. package/dist/reporting/synthesis.js +30 -6
  156. package/dist/reporting/synthesisNarrativePrompt.js +3 -0
  157. package/dist/reporting/workBlocks.js +1 -14
  158. package/dist/supervisor/operatorHandoff.js +2 -6
  159. package/dist/supervisor/runLedger.js +30 -41
  160. package/dist/types/activeDispatch.d.ts +31 -0
  161. package/dist/types/activeDispatch.js +2 -0
  162. package/dist/types.d.ts +21 -4
  163. package/dist/types.js +24 -16
  164. package/dist/validation/artifacts.js +3 -0
  165. package/dist/validation/auditResults.js +8 -2
  166. package/package.json +2 -2
  167. package/schemas/audit_findings.schema.json +5 -1
  168. package/schemas/audit_plan_metrics.schema.json +1 -1
  169. package/schemas/audit_result.schema.json +5 -6
  170. package/schemas/audit_task.schema.json +1 -4
  171. package/schemas/blind_spot_register.schema.json +1 -1
  172. package/schemas/coverage_matrix.schema.json +2 -8
  173. package/schemas/finding.schema.json +1 -16
  174. package/schemas/flow_coverage.schema.json +2 -8
  175. package/schemas/graph_bundle.schema.json +31 -0
  176. package/schemas/lens.schema.json +7 -0
  177. package/schemas/review_packets.schema.json +6 -17
  178. package/schemas/step_contract.schema.json +8 -2
  179. package/schemas/unit_manifest.schema.json +1 -4
  180. package/scripts/postinstall.mjs +3 -1
  181. package/skills/audit-code/audit-code.prompt.md +2 -3
  182. package/dist/extractors/graphManifestEdges.d.ts +0 -12
  183. package/dist/extractors/graphManifestEdges.js +0 -1135
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { isFileMissingError, readJsonFile, writeJsonFile, DEFAULT_EMPIRICAL_HALF_LIFE_HOURS, } from "@audit-tools/shared";
5
5
  import { buildQuotaSource } from "@audit-tools/shared/quota/compositeQuotaSource";
6
+ import { DISPATCH_RESULT_MAP_FILENAME, ACTIVE_DISPATCH_FILENAME, } from "../types/activeDispatch.js";
6
7
  import { loadArtifactBundle } from "../io/artifacts.js";
7
8
  import { writePacketSchemaFiles } from "../io/runArtifacts.js";
8
9
  import { orderTasksForPacketReview, buildReviewPackets, sizeIndexFromManifest, } from "../orchestrator/reviewPackets.js";
@@ -15,8 +16,7 @@ export const LARGE_FILE_PACKET_TARGET_LINES = 2500;
15
16
  export const SMALL_MODEL_HINT_MAX_LINES = 500;
16
17
  export const SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS = 3000;
17
18
  export const DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS = 9000;
18
- export const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
19
- export const ACTIVE_DISPATCH_FILENAME = "active-dispatch.json";
19
+ export { DISPATCH_RESULT_MAP_FILENAME, ACTIVE_DISPATCH_FILENAME, } from "../types/activeDispatch.js";
20
20
  export const DEFAULT_DISPATCH_CONFIRM_THRESHOLD = 10;
21
21
  /**
22
22
  * FINDING-012: pure-arithmetic fan-out summary the loader can gate on. Given the
@@ -194,6 +194,279 @@ export function buildPendingAuditTasks(bundle) {
194
194
  sizeIndex: sizeIndexFromManifest(bundle.repo_manifest),
195
195
  });
196
196
  }
197
+ /**
198
+ * Encapsulates the canary and budget-cap filtering logic.
199
+ * Returns the subset of packets to emit this round plus deferred packets and
200
+ * phase metadata.
201
+ */
202
+ export function filterPackets(packets, priorDispatchThisRun, sessionConfig) {
203
+ const firstContact = !priorDispatchThisRun;
204
+ const canaryEnabled = sessionConfig.dispatch?.canary !== false; // default on
205
+ const doCanary = firstContact && canaryEnabled && packets.length > 1;
206
+ const canaryPacketId = doCanary ? packets[0].packet_id : null;
207
+ const phase = doCanary ? "canary" : "fan_out";
208
+ const postCanaryPackets = doCanary ? packets.slice(0, 1) : packets;
209
+ const maxPackets = sessionConfig.dispatch?.max_packets;
210
+ const budgetCapped = typeof maxPackets === "number" &&
211
+ maxPackets >= 0 &&
212
+ maxPackets < postCanaryPackets.length;
213
+ const emitPackets = budgetCapped
214
+ ? postCanaryPackets.slice(0, maxPackets)
215
+ : postCanaryPackets;
216
+ const deferredPackets = budgetCapped
217
+ ? postCanaryPackets.slice(maxPackets)
218
+ : [];
219
+ return { emitPackets, deferredPackets, phase, canaryPacketId, doCanary, postCanaryCount: postCanaryPackets.length };
220
+ }
221
+ /**
222
+ * Encapsulates large-file anchor extraction for a single packet.
223
+ * Appends to the provided warnings array on unavailability or failure.
224
+ */
225
+ async function extractPacketAnchor(params) {
226
+ const { packet, reviewRoot, bundle, taskResultsDir, warnings } = params;
227
+ if (!reviewRoot) {
228
+ warnings.push({
229
+ code: "large_file_anchor_unavailable",
230
+ message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
231
+ });
232
+ return { anchorPath: null, anchorSummary: null };
233
+ }
234
+ try {
235
+ const filePath = packet.file_paths[0];
236
+ const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
237
+ const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
238
+ const anchorSummary = buildFileAnchorSummary({
239
+ path: filePath,
240
+ content,
241
+ totalLines,
242
+ graphBundle: bundle.graph_bundle,
243
+ externalAnalyzerResults: bundle.external_analyzer_results,
244
+ });
245
+ const anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
246
+ await writeJsonFile(anchorPath, anchorSummary);
247
+ return { anchorPath, anchorSummary };
248
+ }
249
+ catch (error) {
250
+ warnings.push({
251
+ code: "large_file_anchor_failed",
252
+ message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
253
+ (error instanceof Error ? error.message : String(error)),
254
+ });
255
+ return { anchorPath: null, anchorSummary: null };
256
+ }
257
+ }
258
+ /**
259
+ * Extracts the per-task flatMap that builds task section lines.
260
+ */
261
+ export function buildTaskSections(packetTasks, lensDefs, lineIndex) {
262
+ return packetTasks.flatMap((task) => {
263
+ const lensDef = lensDefs[task.lens];
264
+ const inputLines = task.inputs
265
+ ? Object.entries(task.inputs)
266
+ .sort(([a], [b]) => a.localeCompare(b))
267
+ .map(([key, value]) => `input.${key}: ${value}`)
268
+ : [];
269
+ const isLensVerification = task.tags?.includes("lens_verification") ?? false;
270
+ const coverageTemplate = task.file_paths.map((path) => ({
271
+ path,
272
+ total_lines: task.file_line_counts?.[path] ?? lineIndex[path] ?? 0,
273
+ }));
274
+ return [
275
+ `### ${task.task_id}`,
276
+ `unit_id: ${task.unit_id}`,
277
+ `pass_id: ${task.pass_id}`,
278
+ `lens: ${task.lens}`,
279
+ ...(task.tags?.length ? [`tags: ${task.tags.join(", ")}`] : []),
280
+ ...inputLines,
281
+ `rationale: ${task.rationale}`,
282
+ "",
283
+ `Lens guidance: ${lensDef?.description ?? task.lens}`,
284
+ `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
285
+ ...(isLensVerification
286
+ ? [
287
+ "",
288
+ "Lens verification mode: review the prior result summary in the rationale and use only targeted source checks.",
289
+ "Do not redo every packet and do not write direct findings for this task.",
290
+ "Return findings: [] plus verification metadata. Include followup_tasks only for bounded, specific re-review packets.",
291
+ ]
292
+ : []),
293
+ "",
294
+ "file_coverage (copy exactly into your AuditResult for this task):",
295
+ "```json",
296
+ JSON.stringify(coverageTemplate),
297
+ "```",
298
+ "",
299
+ ];
300
+ });
301
+ }
302
+ /**
303
+ * Wraps the 75-line array-join block and returns the assembled prompt string.
304
+ */
305
+ export function buildPacketPrompt(params) {
306
+ const { packet, fileList, largeFileSection, taskSections, submitCommand } = params;
307
+ const largeFileMode = isIsolatedLargeFilePacket(packet);
308
+ return [
309
+ "You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
310
+ "",
311
+ "## Packet",
312
+ `packet_id: ${packet.packet_id}`,
313
+ `task_count: ${packet.task_ids.length}`,
314
+ `lenses: ${packet.lenses.join(", ")}`,
315
+ `estimated_tokens: ${packet.estimated_tokens}`,
316
+ "",
317
+ "## Files to read",
318
+ largeFileMode
319
+ ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
320
+ : "Use your Read tool. Paths are repo-relative from the current working directory.",
321
+ "Use host Read and Grep tools for source inspection. Do not use shell search commands.",
322
+ fileList,
323
+ "",
324
+ ...renderPacketGraphContext(packet),
325
+ ...largeFileSection,
326
+ "## Tasks",
327
+ ...taskSections,
328
+ "## Output",
329
+ "Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
330
+ "remediate findings, run unrelated audits, or write any result file yourself (e.g.",
331
+ "packet-*-result.json / audit_result_*.json) — the submit-packet command below is the only",
332
+ "way to record results, and it writes them inside the artifacts directory for you.",
333
+ "Produce one JSON array containing exactly one AuditResult object for each listed task.",
334
+ "",
335
+ "Schema file (resolve relative to this prompt's directory): audit_result.schema.json",
336
+ " $refs resolved from the same directory: finding.schema.json, audit_task.schema.json",
337
+ "You MAY validate your JSON array against the schema before calling submit-packet. This is optional;",
338
+ " the submit command performs the authoritative validation and will report any errors.",
339
+ "",
340
+ "Required AuditResult fields:",
341
+ " task_id copy from the task metadata",
342
+ " unit_id copy from the task metadata",
343
+ " pass_id copy from the task metadata",
344
+ " lens copy from the task metadata",
345
+ " file_coverage [{path, total_lines}] - copy the exact template from each task section above. You MUST include total_lines. Do not omit or zero it out, as this will cause fatal validation errors.",
346
+ " findings [] or array of finding objects",
347
+ "",
348
+ "Lens verification tasks:",
349
+ " tasks tagged lens_verification must use findings: [] and include verification:",
350
+ " {verified: boolean, needs_followup: boolean, concerns?: string[],",
351
+ " coverage_concerns?: string[], confidence_concerns?: string[],",
352
+ " followup_tasks?: AuditTask[]}.",
353
+ " Follow-up AuditTask suggestions must stay bounded to files in this packet and use the same lens.",
354
+ "",
355
+ "Each finding object:",
356
+ " id unique ID, e.g. \"COR-001\"",
357
+ " title short title",
358
+ " category specific finding category, such as missing-validation or command-execution",
359
+ " severity critical|high|medium|low|info",
360
+ " confidence high|medium|low",
361
+ " lens must match the task lens exactly",
362
+ " summary 1-2 sentence description",
363
+ " affected_files [{path, line_start?, line_end?, symbol?}] - objects, not strings; min 1 entry",
364
+ " evidence [\"path/to/file.ts:42 - description of what you see there\"] - min 1 entry",
365
+ "",
366
+ "Constraints:",
367
+ "1. line_end must not exceed the file's actual line count.",
368
+ "2. affected_files entries are objects with a path key, not plain strings.",
369
+ "3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
370
+ "4. findings: [] is correct when you find nothing genuine.",
371
+ "",
372
+ "## Submit",
373
+ "Pipe the JSON array on stdin to this command:",
374
+ ` ${submitCommand}`,
375
+ " (If using Windows PowerShell, you MUST use `Get-Content <file> | & <command>` instead of the `<` operator.)",
376
+ "",
377
+ "The command validates and writes the packet-owned result files. Exit 0 means accepted.",
378
+ "Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
379
+ "",
380
+ "## Final response",
381
+ `After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
382
+ ].join("\n");
383
+ }
384
+ /**
385
+ * Encapsulates host-model resolution, quota state lookup, provider-limits query,
386
+ * capacity computation, and writing the dispatch-quota artifact.
387
+ */
388
+ async function computeDispatchQuota(params) {
389
+ const { runId, runDir, sessionConfig, perPacketTokens, queryLimits, hostActiveSubagentLimit } = params;
390
+ const quotaProviderName = resolveFreshSessionProviderName(undefined, sessionConfig);
391
+ const hostModel = resolveHostModel({
392
+ providerName: quotaProviderName,
393
+ sessionConfig,
394
+ explicitModel: params.hostModel,
395
+ envVar: "AUDIT_CODE_HOST_MODEL",
396
+ });
397
+ const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
398
+ const quotaState = await readQuotaState().catch(() => ({ version: 2, entries: {} }));
399
+ const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
400
+ const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
401
+ explicitLimit: hostActiveSubagentLimit,
402
+ sessionConfig,
403
+ });
404
+ const providerLimits = await queryLimits?.(hostModel)
405
+ .then((r) => r ? { ...r, source: "provider_query" } : null)
406
+ .catch(() => null)
407
+ ?? null;
408
+ const dispatchCachedLimits = await lookupDiscoveredLimits(quotaProviderKey).catch(() => null);
409
+ const discoveredLimits = mergeDiscoveredLimits(providerLimits, dispatchCachedLimits);
410
+ const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ??
411
+ DEFAULT_EMPIRICAL_HALF_LIFE_HOURS;
412
+ const quotaSource = buildQuotaSource({ halfLifeHours });
413
+ const quotaSourceSnapshot = await quotaSource.queryCurrentUsage(quotaProviderKey).catch(() => null);
414
+ const hostPool = {
415
+ id: quotaProviderKey,
416
+ providerName: quotaProviderName,
417
+ hostModel,
418
+ hostConcurrencyLimit,
419
+ quotaStateEntry,
420
+ discoveredLimits,
421
+ quotaSourceSnapshot,
422
+ };
423
+ const dispatchCapacity = computeDispatchCapacity({
424
+ pools: [hostPool],
425
+ sessionConfig,
426
+ pendingItemTokens: perPacketTokens,
427
+ });
428
+ const waveSchedule = dispatchCapacity.primary.schedule;
429
+ const dispatchQuota = {
430
+ contract_version: "audit-code-dispatch-quota/v1alpha2",
431
+ run_id: runId,
432
+ model: hostModel,
433
+ resolved_limits: waveSchedule.resolved_limits,
434
+ confidence: waveSchedule.confidence,
435
+ source: waveSchedule.source,
436
+ host_concurrency_limit: waveSchedule.host_concurrency_limit,
437
+ wave_size: dispatchCapacity.total_slots,
438
+ estimated_wave_tokens: dispatchCapacity.estimated_wave_tokens,
439
+ cooldown_until: dispatchCapacity.cooldown_until,
440
+ quota_source_snapshot: waveSchedule.quota_source_snapshot ?? null,
441
+ backoff_state: null,
442
+ };
443
+ const dispatchQuotaPath = join(runDir, "dispatch-quota.json");
444
+ await writeJsonFile(dispatchQuotaPath, dispatchQuota);
445
+ return { dispatchQuota, dispatchQuotaPath, waveSchedule, dispatchCapacity };
446
+ }
447
+ /**
448
+ * Extracts the context-budget warning loop.
449
+ * Returns warnings for packets whose estimated token count exceeds the context budget.
450
+ * When confidence is 'low', returns an empty array (limits are unreliable).
451
+ */
452
+ export function collectOversizedWarnings(plan, waveSchedule) {
453
+ if (waveSchedule.confidence === "low") {
454
+ return [];
455
+ }
456
+ const contextBudget = waveSchedule.resolved_limits.context_tokens - waveSchedule.resolved_limits.output_tokens;
457
+ const warnings = [];
458
+ for (const p of plan) {
459
+ if (p.complexity.estimated_tokens > contextBudget) {
460
+ warnings.push({
461
+ code: "oversized_packet",
462
+ message: `Packet ${p.packet_id} estimated tokens (${p.complexity.estimated_tokens}) exceed ` +
463
+ `context budget (${contextBudget}). This packet may fail at dispatch. ` +
464
+ `Set quota.default_context_tokens or quota.models in session-config.json to override.`,
465
+ });
466
+ }
467
+ }
468
+ return warnings;
469
+ }
197
470
  export async function prepareDispatchArtifacts(params) {
198
471
  const runId = params.runId;
199
472
  const artifactsDir = params.artifactsDir;
@@ -284,27 +557,11 @@ export async function prepareDispatchArtifacts(params) {
284
557
  // still-pending tasks have no result files, priorResultTaskIds stayed empty, and
285
558
  // the canary re-fired every cycle (1 packet forever, never reaching fan-out).
286
559
  const priorDispatchThisRun = priorActiveDispatch?.run_id === runId;
287
- const firstContact = !priorDispatchThisRun;
288
- const canaryEnabled = sessionConfig.dispatch?.canary !== false; // default on
289
- const doCanary = firstContact && canaryEnabled && packets.length > 1;
290
- const canaryPacketId = doCanary ? packets[0].packet_id : null;
291
- const phase = doCanary ? "canary" : "fan_out";
292
- const postCanaryPackets = doCanary ? packets.slice(0, 1) : packets;
293
- // FINDING-013: top-K coverage budget. Cap the (already priority-ordered)
294
- // packets at max_packets; the remainder are recorded as DEFERRED and excluded
295
- // from the completion check so the run can finish honestly under budget.
296
- // Budget defaults OFF (no cap) so default behavior is unchanged. Canary takes
297
- // precedence: a canary round only emits 1 packet regardless of the budget.
298
- const maxPackets = sessionConfig.dispatch?.max_packets;
299
- const budgetCapped = typeof maxPackets === "number" &&
300
- maxPackets >= 0 &&
301
- maxPackets < postCanaryPackets.length;
302
- const emitPackets = budgetCapped
303
- ? postCanaryPackets.slice(0, maxPackets)
304
- : postCanaryPackets;
305
- const deferredPackets = budgetCapped
306
- ? postCanaryPackets.slice(maxPackets)
307
- : [];
560
+ // FINDING-013: top-K coverage budget. Budget defaults OFF (no cap) so default
561
+ // behavior is unchanged. Canary takes precedence: a canary round only emits 1
562
+ // packet regardless of the budget.
563
+ const { emitPackets, deferredPackets, phase, canaryPacketId, doCanary, postCanaryCount } = filterPackets(packets, priorDispatchThisRun, sessionConfig);
564
+ const budgetCapped = deferredPackets.length > 0;
308
565
  const plan = [];
309
566
  const resultMapEntries = [];
310
567
  for (const task of tasks) {
@@ -349,39 +606,9 @@ export async function prepareDispatchArtifacts(params) {
349
606
  const lines = packet.file_line_counts[path] ?? 0;
350
607
  return `- ${path} (${lines} lines)`;
351
608
  }).join("\n");
352
- let anchorPath = null;
353
- let anchorSummary = null;
354
- if (largeFileMode) {
355
- const filePath = packet.file_paths[0];
356
- if (!reviewRoot) {
357
- warnings.push({
358
- code: "large_file_anchor_unavailable",
359
- message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
360
- });
361
- }
362
- else {
363
- try {
364
- const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
365
- const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
366
- anchorSummary = buildFileAnchorSummary({
367
- path: filePath,
368
- content,
369
- totalLines,
370
- graphBundle: bundle.graph_bundle,
371
- externalAnalyzerResults: bundle.external_analyzer_results,
372
- });
373
- anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
374
- await writeJsonFile(anchorPath, anchorSummary);
375
- }
376
- catch (error) {
377
- warnings.push({
378
- code: "large_file_anchor_failed",
379
- message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
380
- (error instanceof Error ? error.message : String(error)),
381
- });
382
- }
383
- }
384
- }
609
+ const { anchorPath, anchorSummary } = largeFileMode
610
+ ? await extractPacketAnchor({ packet, reviewRoot, bundle, taskResultsDir, warnings })
611
+ : { anchorPath: null, anchorSummary: null };
385
612
  const largeFileSection = anchorSummary && anchorPath
386
613
  ? renderAnchorPreview(anchorSummary, anchorPath)
387
614
  : largeFileMode
@@ -393,45 +620,7 @@ export async function prepareDispatchArtifacts(params) {
393
620
  "",
394
621
  ]
395
622
  : [];
396
- const taskSections = packetTasks.flatMap((task) => {
397
- const lensDef = lensDefs[task.lens];
398
- const inputLines = task.inputs
399
- ? Object.entries(task.inputs)
400
- .sort(([a], [b]) => a.localeCompare(b))
401
- .map(([key, value]) => `input.${key}: ${value}`)
402
- : [];
403
- const isLensVerification = task.tags?.includes("lens_verification") ?? false;
404
- const coverageTemplate = task.file_paths.map((path) => ({
405
- path,
406
- total_lines: task.file_line_counts?.[path] ?? lineIndex[path] ?? 0,
407
- }));
408
- return [
409
- `### ${task.task_id}`,
410
- `unit_id: ${task.unit_id}`,
411
- `pass_id: ${task.pass_id}`,
412
- `lens: ${task.lens}`,
413
- ...(task.tags?.length ? [`tags: ${task.tags.join(", ")}`] : []),
414
- ...inputLines,
415
- `rationale: ${task.rationale}`,
416
- "",
417
- `Lens guidance: ${lensDef?.description ?? task.lens}`,
418
- `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
419
- ...(isLensVerification
420
- ? [
421
- "",
422
- "Lens verification mode: review the prior result summary in the rationale and use only targeted source checks.",
423
- "Do not redo every packet and do not write direct findings for this task.",
424
- "Return findings: [] plus verification metadata. Include followup_tasks only for bounded, specific re-review packets.",
425
- ]
426
- : []),
427
- "",
428
- "file_coverage (copy exactly into your AuditResult for this task):",
429
- "```json",
430
- JSON.stringify(coverageTemplate),
431
- "```",
432
- "",
433
- ];
434
- });
623
+ const taskSections = buildTaskSections(packetTasks, lensDefs, lineIndex);
435
624
  const submitCommand = `node packages/audit-code/audit-code.mjs submit-packet ` +
436
625
  `--run-id-b64 ${toBase64Url(runId)} ` +
437
626
  `--packet-id-b64 ${toBase64Url(packet.packet_id)} ` +
@@ -444,81 +633,7 @@ export async function prepareDispatchArtifacts(params) {
444
633
  result_path: resultPathByTaskId.get(task.task_id),
445
634
  });
446
635
  }
447
- const prompt = [
448
- "You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
449
- "",
450
- "## Packet",
451
- `packet_id: ${packet.packet_id}`,
452
- `task_count: ${packet.task_ids.length}`,
453
- `lenses: ${packet.lenses.join(", ")}`,
454
- `estimated_tokens: ${packet.estimated_tokens}`,
455
- "",
456
- "## Files to read",
457
- largeFileMode
458
- ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
459
- : "Use your Read tool. Paths are repo-relative from the current working directory.",
460
- "Use host Read and Grep tools for source inspection. Do not use shell search commands.",
461
- fileList,
462
- "",
463
- ...renderPacketGraphContext(packet),
464
- ...largeFileSection,
465
- "## Tasks",
466
- ...taskSections,
467
- "## Output",
468
- "Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
469
- "remediate findings, run unrelated audits, or write any result file yourself (e.g.",
470
- "packet-*-result.json / audit_result_*.json) — the submit-packet command below is the only",
471
- "way to record results, and it writes them inside the artifacts directory for you.",
472
- "Produce one JSON array containing exactly one AuditResult object for each listed task.",
473
- "",
474
- "Schema file (resolve relative to this prompt's directory): audit_result.schema.json",
475
- " $refs resolved from the same directory: finding.schema.json, audit_task.schema.json",
476
- "You MAY validate your JSON array against the schema before calling submit-packet. This is optional;",
477
- " the submit command performs the authoritative validation and will report any errors.",
478
- "",
479
- "Required AuditResult fields:",
480
- " task_id copy from the task metadata",
481
- " unit_id copy from the task metadata",
482
- " pass_id copy from the task metadata",
483
- " lens copy from the task metadata",
484
- " file_coverage [{path, total_lines}] - copy the exact template from each task section above. You MUST include total_lines. Do not omit or zero it out, as this will cause fatal validation errors.",
485
- " findings [] or array of finding objects",
486
- "",
487
- "Lens verification tasks:",
488
- " tasks tagged lens_verification must use findings: [] and include verification:",
489
- " {verified: boolean, needs_followup: boolean, concerns?: string[],",
490
- " coverage_concerns?: string[], confidence_concerns?: string[],",
491
- " followup_tasks?: AuditTask[]}.",
492
- " Follow-up AuditTask suggestions must stay bounded to files in this packet and use the same lens.",
493
- "",
494
- "Each finding object:",
495
- " id unique ID, e.g. \"COR-001\"",
496
- " title short title",
497
- " category specific finding category, such as missing-validation or command-execution",
498
- " severity critical|high|medium|low|info",
499
- " confidence high|medium|low",
500
- " lens must match the task lens exactly",
501
- " summary 1-2 sentence description",
502
- " affected_files [{path, line_start?, line_end?, symbol?}] - objects, not strings; min 1 entry",
503
- " evidence [\"path/to/file.ts:42 - description of what you see there\"] - min 1 entry",
504
- "",
505
- "Constraints:",
506
- "1. line_end must not exceed the file's actual line count.",
507
- "2. affected_files entries are objects with a path key, not plain strings.",
508
- "3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
509
- "4. findings: [] is correct when you find nothing genuine.",
510
- "",
511
- "## Submit",
512
- "Pipe the JSON array on stdin to this command:",
513
- ` ${submitCommand}`,
514
- " (If using Windows PowerShell, you MUST use `Get-Content <file> | & <command>` instead of the `<` operator.)",
515
- "",
516
- "The command validates and writes the packet-owned result files. Exit 0 means accepted.",
517
- "Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
518
- "",
519
- "## Final response",
520
- `After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
521
- ].join("\n");
636
+ const prompt = buildPacketPrompt({ packet, packetTasks, fileList, largeFileSection, taskSections, submitCommand });
522
637
  await writeFile(promptPath, prompt, "utf8");
523
638
  plan.push({
524
639
  packet_id: packet.packet_id,
@@ -536,84 +651,26 @@ export async function prepareDispatchArtifacts(params) {
536
651
  entries: resultMapEntries,
537
652
  });
538
653
  const perPacketTokens = plan.map((p) => p.complexity.estimated_tokens);
539
- const quotaProviderName = resolveFreshSessionProviderName(undefined, sessionConfig);
540
- // Resolve the host model (explicit/CLI override → block_quota.host_model → env
541
- // → per-provider default) so per-model quota detection engages with realistic
542
- // limits instead of the conservative unknown-model floor. params.hostModel
543
- // carries any caller/CLI override.
544
- const hostModel = resolveHostModel({
545
- providerName: quotaProviderName,
546
- sessionConfig,
547
- explicitModel: params.hostModel,
548
- envVar: "AUDIT_CODE_HOST_MODEL",
549
- });
550
- const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
551
- const quotaState = await readQuotaState().catch(() => ({ version: 2, entries: {} }));
552
- const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
553
- const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
554
- explicitLimit: params.hostActiveSubagentLimit,
555
- sessionConfig,
556
- });
557
- const providerLimits = await params.queryLimits?.(hostModel)
558
- .then((r) => r ? { ...r, source: "provider_query" } : null)
559
- .catch(() => null)
560
- ?? null;
561
- const dispatchCachedLimits = await lookupDiscoveredLimits(quotaProviderKey).catch(() => null);
562
- const discoveredLimits = mergeDiscoveredLimits(providerLimits, dispatchCachedLimits);
563
- const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ??
564
- DEFAULT_EMPIRICAL_HALF_LIFE_HOURS;
565
- const quotaSource = buildQuotaSource({ halfLifeHours });
566
- const quotaSourceSnapshot = await quotaSource.queryCurrentUsage(quotaProviderKey).catch(() => null);
567
654
  // Size the dispatch just-in-time against the full pending layout (one token
568
655
  // estimate per emitted packet) and the host pool's current limits, rather than
569
656
  // a preset wave size. `parallel_workers` is no longer the ambition — it is
570
657
  // folded into hostConcurrencyLimit as a ceiling (resolveHostActiveSubagentLimit).
571
658
  // Today there is a single pool (the conversation host's subagents); a
572
659
  // heterogeneous provider pool slots in here without changing the call.
573
- const hostPool = {
574
- id: quotaProviderKey,
575
- providerName: quotaProviderName,
576
- hostModel,
577
- hostConcurrencyLimit,
578
- quotaStateEntry,
579
- discoveredLimits,
580
- quotaSourceSnapshot,
581
- };
582
- const dispatchCapacity = computeDispatchCapacity({
583
- pools: [hostPool],
660
+ // Resolve the host model (explicit/CLI override → block_quota.host_model → env
661
+ // → per-provider default) so per-model quota detection engages with realistic
662
+ // limits instead of the conservative unknown-model floor.
663
+ const { dispatchQuotaPath, waveSchedule, dispatchCapacity } = await computeDispatchQuota({
664
+ runId,
665
+ artifactsDir,
666
+ runDir,
584
667
  sessionConfig,
585
- pendingItemTokens: perPacketTokens,
668
+ perPacketTokens,
669
+ hostModel: params.hostModel,
670
+ queryLimits: params.queryLimits,
671
+ hostActiveSubagentLimit: params.hostActiveSubagentLimit,
586
672
  });
587
- const waveSchedule = dispatchCapacity.primary.schedule;
588
- const dispatchQuota = {
589
- contract_version: "audit-code-dispatch-quota/v1alpha2",
590
- run_id: runId,
591
- model: hostModel,
592
- resolved_limits: waveSchedule.resolved_limits,
593
- confidence: waveSchedule.confidence,
594
- source: waveSchedule.source,
595
- host_concurrency_limit: waveSchedule.host_concurrency_limit,
596
- wave_size: dispatchCapacity.total_slots,
597
- estimated_wave_tokens: dispatchCapacity.estimated_wave_tokens,
598
- cooldown_until: dispatchCapacity.cooldown_until,
599
- quota_source_snapshot: waveSchedule.quota_source_snapshot ?? null,
600
- backoff_state: null,
601
- };
602
- const dispatchQuotaPath = join(runDir, "dispatch-quota.json");
603
- await writeJsonFile(dispatchQuotaPath, dispatchQuota);
604
- if (waveSchedule.confidence !== "low") {
605
- const contextBudget = waveSchedule.resolved_limits.context_tokens - waveSchedule.resolved_limits.output_tokens;
606
- for (const p of plan) {
607
- if (p.complexity.estimated_tokens > contextBudget) {
608
- warnings.push({
609
- code: "oversized_packet",
610
- message: `Packet ${p.packet_id} estimated tokens (${p.complexity.estimated_tokens}) exceed ` +
611
- `context budget (${contextBudget}). This packet may fail at dispatch. ` +
612
- `Set quota.default_context_tokens or quota.models in session-config.json to override.`,
613
- });
614
- }
615
- }
616
- }
673
+ warnings.push(...collectOversizedWarnings(plan, waveSchedule));
617
674
  // FINDING-011: when advancing past a canary, warn if it never produced an
618
675
  // accepted result. submit-packet writes the per-task result file ONLY after
619
676
  // validation passes, so presence of that file == ACCEPTED. We map the recorded
@@ -652,7 +709,7 @@ export async function prepareDispatchArtifacts(params) {
652
709
  canary_packet_id: canaryPacketId,
653
710
  ...(budgetCapped
654
711
  ? {
655
- budget_packet_count: postCanaryPackets.length,
712
+ budget_packet_count: postCanaryCount,
656
713
  deferred_packet_ids: deferredPacketIds,
657
714
  deferred_task_ids: deferredTaskIds,
658
715
  }
@@ -0,0 +1 @@
1
+ export declare function cmdDispatchStatus(argv: string[]): Promise<void>;