auditor-lambda 0.3.40 → 0.5.0

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 (193) hide show
  1. package/audit-code-wrapper-lib.mjs +20 -2
  2. package/dist/cli/args.d.ts +59 -0
  3. package/dist/cli/args.js +244 -0
  4. package/dist/cli/dispatch.d.ts +80 -0
  5. package/dist/cli/dispatch.js +532 -0
  6. package/dist/cli/prompts.d.ts +37 -0
  7. package/dist/cli/prompts.js +225 -0
  8. package/dist/cli/steps.d.ts +29 -0
  9. package/dist/cli/steps.js +30 -0
  10. package/dist/cli/waveManifest.d.ts +40 -0
  11. package/dist/cli/waveManifest.js +41 -0
  12. package/dist/cli/workerResult.d.ts +18 -0
  13. package/dist/cli/workerResult.js +42 -0
  14. package/dist/cli.d.ts +2 -22
  15. package/dist/cli.js +442 -975
  16. package/dist/extractors/analyzers/css.d.ts +2 -0
  17. package/dist/extractors/analyzers/css.js +101 -0
  18. package/dist/extractors/analyzers/html.d.ts +2 -0
  19. package/dist/extractors/analyzers/html.js +92 -0
  20. package/dist/extractors/analyzers/merge.d.ts +14 -0
  21. package/dist/extractors/analyzers/merge.js +85 -0
  22. package/dist/extractors/analyzers/python.d.ts +2 -0
  23. package/dist/extractors/analyzers/python.js +104 -0
  24. package/dist/extractors/analyzers/registry.d.ts +33 -0
  25. package/dist/extractors/analyzers/registry.js +100 -0
  26. package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
  27. package/dist/extractors/analyzers/resourceUrl.js +25 -0
  28. package/dist/extractors/analyzers/sql.d.ts +2 -0
  29. package/dist/extractors/analyzers/sql.js +19 -0
  30. package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
  31. package/dist/extractors/analyzers/treeSitter.js +111 -0
  32. package/dist/extractors/analyzers/types.d.ts +53 -0
  33. package/dist/extractors/analyzers/typescript.d.ts +2 -0
  34. package/dist/extractors/analyzers/typescript.js +257 -0
  35. package/dist/extractors/browserExtension.d.ts +1 -3
  36. package/dist/extractors/browserExtension.js +2 -2
  37. package/dist/extractors/designAssessment.d.ts +1 -3
  38. package/dist/extractors/disposition.d.ts +2 -1
  39. package/dist/extractors/disposition.js +11 -1
  40. package/dist/extractors/flows.d.ts +1 -3
  41. package/dist/extractors/flows.js +2 -2
  42. package/dist/extractors/graph.d.ts +2 -2
  43. package/dist/extractors/graph.js +171 -327
  44. package/dist/extractors/graphManifestEdges.d.ts +1 -1
  45. package/dist/extractors/graphPathUtils.d.ts +1 -1
  46. package/dist/extractors/graphPythonImports.d.ts +18 -0
  47. package/dist/extractors/graphPythonImports.js +362 -0
  48. package/dist/extractors/pathPatterns.d.ts +6 -0
  49. package/dist/extractors/pathPatterns.js +8 -0
  50. package/dist/extractors/risk.d.ts +1 -2
  51. package/dist/extractors/surfaces.d.ts +1 -3
  52. package/dist/extractors/surfaces.js +2 -2
  53. package/dist/io/artifacts.d.ts +12 -5
  54. package/dist/io/artifacts.js +13 -1
  55. package/dist/io/runArtifacts.js +1 -1
  56. package/dist/mcp/server.js +1 -1
  57. package/dist/orchestrator/advance.d.ts +21 -0
  58. package/dist/orchestrator/advance.js +69 -7
  59. package/dist/orchestrator/auditTaskUtils.d.ts +4 -0
  60. package/dist/orchestrator/auditTaskUtils.js +27 -0
  61. package/dist/orchestrator/dependencyMap.js +27 -0
  62. package/dist/orchestrator/edgeReasoning.d.ts +39 -0
  63. package/dist/orchestrator/edgeReasoning.js +125 -0
  64. package/dist/orchestrator/executors.js +11 -1
  65. package/dist/orchestrator/fileAnchors.d.ts +1 -1
  66. package/dist/orchestrator/fileIntegrity.d.ts +7 -0
  67. package/dist/orchestrator/fileIntegrity.js +41 -0
  68. package/dist/orchestrator/flowCoverage.d.ts +1 -1
  69. package/dist/orchestrator/flowPlanning.d.ts +1 -1
  70. package/dist/orchestrator/flowRequeue.d.ts +1 -1
  71. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
  72. package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
  73. package/dist/orchestrator/internalExecutors.d.ts +13 -2
  74. package/dist/orchestrator/internalExecutors.js +112 -16
  75. package/dist/orchestrator/localCommands.js +6 -25
  76. package/dist/orchestrator/nextStep.d.ts +2 -1
  77. package/dist/orchestrator/nextStep.js +3 -1
  78. package/dist/orchestrator/planning.d.ts +1 -1
  79. package/dist/orchestrator/requeueCommand.d.ts +1 -1
  80. package/dist/orchestrator/reviewPackets.d.ts +37 -4
  81. package/dist/orchestrator/reviewPackets.js +113 -158
  82. package/dist/orchestrator/runtimeValidation.d.ts +1 -1
  83. package/dist/orchestrator/runtimeValidation.js +4 -31
  84. package/dist/orchestrator/scope.d.ts +62 -0
  85. package/dist/orchestrator/scope.js +227 -0
  86. package/dist/orchestrator/state.js +2 -0
  87. package/dist/orchestrator/taskBuilder.d.ts +1 -1
  88. package/dist/orchestrator/taskBuilder.js +1 -12
  89. package/dist/orchestrator/unionFind.d.ts +7 -0
  90. package/dist/orchestrator/unionFind.js +32 -0
  91. package/dist/orchestrator/unitBuilder.d.ts +2 -2
  92. package/dist/orchestrator/unitBuilder.js +4 -18
  93. package/dist/prompts/renderWorkerPrompt.js +18 -1
  94. package/dist/providers/claudeCodeProvider.d.ts +4 -4
  95. package/dist/providers/claudeCodeProvider.js +9 -3
  96. package/dist/providers/constants.d.ts +1 -1
  97. package/dist/providers/constants.js +1 -1
  98. package/dist/providers/index.d.ts +1 -2
  99. package/dist/providers/index.js +5 -4
  100. package/dist/providers/localSubprocessProvider.d.ts +2 -2
  101. package/dist/providers/localSubprocessProvider.js +1 -1
  102. package/dist/providers/opencodeProvider.d.ts +4 -4
  103. package/dist/providers/opencodeProvider.js +7 -2
  104. package/dist/providers/spawnLoggedCommand.d.ts +3 -1
  105. package/dist/providers/spawnLoggedCommand.js +21 -0
  106. package/dist/providers/subprocessTemplateProvider.d.ts +4 -4
  107. package/dist/providers/subprocessTemplateProvider.js +8 -3
  108. package/dist/providers/vscodeTaskProvider.d.ts +3 -4
  109. package/dist/providers/vscodeTaskProvider.js +2 -2
  110. package/dist/quota/discoveredLimits.js +1 -1
  111. package/dist/quota/hostLimits.d.ts +1 -2
  112. package/dist/quota/hostLimits.js +4 -46
  113. package/dist/quota/index.d.ts +18 -15
  114. package/dist/quota/index.js +4 -9
  115. package/dist/quota/scheduler.d.ts +1 -3
  116. package/dist/quota/scheduler.js +1 -2
  117. package/dist/reporting/synthesis.d.ts +37 -3
  118. package/dist/reporting/synthesis.js +97 -16
  119. package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
  120. package/dist/reporting/synthesisNarrativePrompt.js +60 -0
  121. package/dist/reporting/workBlocks.d.ts +2 -11
  122. package/dist/supervisor/operatorHandoff.js +1 -1
  123. package/dist/supervisor/runLedger.d.ts +1 -1
  124. package/dist/supervisor/runLedger.js +2 -2
  125. package/dist/supervisor/sessionConfig.d.ts +8 -1
  126. package/dist/supervisor/sessionConfig.js +22 -3
  127. package/dist/types/analyzerCapability.d.ts +16 -0
  128. package/dist/types/auditScope.d.ts +43 -0
  129. package/dist/types/auditScope.js +14 -0
  130. package/dist/types/reviewPlanning.d.ts +1 -1
  131. package/dist/types/synthesisNarrative.d.ts +7 -0
  132. package/dist/types/synthesisNarrative.js +5 -0
  133. package/dist/types/workerSession.d.ts +6 -0
  134. package/dist/types.d.ts +2 -19
  135. package/dist/validation/artifacts.d.ts +1 -1
  136. package/dist/validation/artifacts.js +10 -1
  137. package/dist/validation/auditResults.d.ts +1 -1
  138. package/dist/validation/auditResults.js +1 -1
  139. package/dist/validation/sessionConfig.d.ts +2 -3
  140. package/dist/validation/sessionConfig.js +25 -3
  141. package/package.json +7 -3
  142. package/schemas/analyzer_capability.schema.json +47 -0
  143. package/schemas/audit_findings.schema.json +141 -0
  144. package/schemas/finding.schema.json +2 -1
  145. package/schemas/graph_bundle.schema.json +5 -0
  146. package/schemas/scope.schema.json +46 -0
  147. package/scripts/postinstall.mjs +0 -1
  148. package/dist/io/json.d.ts +0 -10
  149. package/dist/io/json.js +0 -142
  150. package/dist/providers/types.d.ts +0 -33
  151. package/dist/quota/compositeQuotaSource.d.ts +0 -7
  152. package/dist/quota/compositeQuotaSource.js +0 -20
  153. package/dist/quota/errorParsers/claudeCodeErrorParser.d.ts +0 -6
  154. package/dist/quota/errorParsers/claudeCodeErrorParser.js +0 -39
  155. package/dist/quota/errorParsers/genericErrorParser.d.ts +0 -9
  156. package/dist/quota/errorParsers/genericErrorParser.js +0 -7
  157. package/dist/quota/errorParsers/index.d.ts +0 -5
  158. package/dist/quota/errorParsers/index.js +0 -12
  159. package/dist/quota/errorParsing.d.ts +0 -7
  160. package/dist/quota/errorParsing.js +0 -69
  161. package/dist/quota/fileLock.d.ts +0 -6
  162. package/dist/quota/fileLock.js +0 -64
  163. package/dist/quota/learnedQuotaSource.d.ts +0 -7
  164. package/dist/quota/learnedQuotaSource.js +0 -25
  165. package/dist/quota/limits.d.ts +0 -16
  166. package/dist/quota/limits.js +0 -77
  167. package/dist/quota/quotaSource.d.ts +0 -12
  168. package/dist/quota/slidingWindow.d.ts +0 -4
  169. package/dist/quota/slidingWindow.js +0 -28
  170. package/dist/quota/state.d.ts +0 -15
  171. package/dist/quota/state.js +0 -148
  172. package/dist/quota/types.d.ts +0 -67
  173. package/dist/quota/types.js +0 -1
  174. package/dist/reporting/rootCause.d.ts +0 -10
  175. package/dist/reporting/rootCause.js +0 -146
  176. package/dist/types/disposition.d.ts +0 -9
  177. package/dist/types/disposition.js +0 -1
  178. package/dist/types/flows.d.ts +0 -17
  179. package/dist/types/flows.js +0 -1
  180. package/dist/types/graph.d.ts +0 -22
  181. package/dist/types/graph.js +0 -1
  182. package/dist/types/risk.d.ts +0 -9
  183. package/dist/types/risk.js +0 -1
  184. package/dist/types/runLedger.d.ts +0 -17
  185. package/dist/types/runLedger.js +0 -6
  186. package/dist/types/sessionConfig.d.ts +0 -79
  187. package/dist/types/sessionConfig.js +0 -15
  188. package/dist/types/surfaces.d.ts +0 -15
  189. package/dist/types/surfaces.js +0 -1
  190. package/dist/validation/basic.d.ts +0 -13
  191. package/dist/validation/basic.js +0 -46
  192. /package/dist/{providers → extractors/analyzers}/types.js +0 -0
  193. /package/dist/{quota/quotaSource.js → types/analyzerCapability.js} +0 -0
package/dist/cli.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
2
- import { createReadStream, existsSync } from "node:fs";
3
- import { Buffer } from "node:buffer";
4
- import { createHash } from "node:crypto";
5
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
6
4
  import { fileURLToPath } from "node:url";
7
5
  import { buildRepoManifest } from "./extractors/fileInventory.js";
8
6
  import { buildFileDisposition } from "./extractors/disposition.js";
@@ -13,244 +11,47 @@ import { buildFlowCoverage } from "./orchestrator/flowCoverage.js";
13
11
  import { buildRuntimeValidationTasks, } from "./orchestrator/runtimeValidation.js";
14
12
  import { initializeCoverageFromPlan } from "./orchestrator/planning.js";
15
13
  import { loadArtifactBundle, writeCoreArtifacts, promoteFinalAuditReport, } from "./io/artifacts.js";
16
- import { isFileMissingError, readJsonFile, writeJsonFile } from "./io/json.js";
14
+ import { isFileMissingError, readJsonFile, writeJsonFile, prefixValidationIssues, RunLogger } from "@audit-tools/shared";
17
15
  import { validateArtifactBundle } from "./validation/artifacts.js";
18
16
  import { validateAuditResults, formatAuditResultIssues, } from "./validation/auditResults.js";
19
- import { prefixValidationIssues } from "./validation/basic.js";
20
17
  import { validateConfiguredProviderEnvironment, validateSessionConfig, } from "./validation/sessionConfig.js";
21
18
  import { buildAuditReportModel, renderAuditReportMarkdown, } from "./reporting/synthesis.js";
22
19
  import { deriveAuditState } from "./orchestrator/state.js";
23
20
  import { advanceAudit } from "./orchestrator/advance.js";
21
+ import { checkFileIntegrity } from "./orchestrator/fileIntegrity.js";
24
22
  import { decideNextStep } from "./orchestrator/nextStep.js";
23
+ import { collectLowConfidenceEdges, buildEdgeReasoningPrompt, edgeReasoningContentHash, } from "./orchestrator/edgeReasoning.js";
25
24
  import { renderDesignReviewPrompt } from "./orchestrator/designReviewPrompt.js";
25
+ import { renderSynthesisNarrativePrompt } from "./reporting/synthesisNarrativePrompt.js";
26
26
  import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
27
27
  import { appendRunLedgerEntry, loadRunLedger } from "./supervisor/runLedger.js";
28
28
  import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./supervisor/operatorHandoff.js";
29
- import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
29
+ import { getSessionConfigPath, loadSessionConfig, persistAnalyzerSettings, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
30
+ import { resolveAnalyzerPlan, needsInstallDecision, } from "./extractors/analyzers/registry.js";
31
+ import { buildPathLookup } from "./extractors/graph.js";
32
+ import { buildDispositionMap } from "./extractors/disposition.js";
30
33
  import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
31
34
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
32
- import { buildReviewPackets, orderTasksForPacketReview, estimateTaskGroupTokens, } from "./orchestrator/reviewPackets.js";
33
- import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
35
+ import { estimateTaskGroupTokens, sizeIndexFromManifest, } from "./orchestrator/reviewPackets.js";
34
36
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
35
37
  import { runAuditCodeMcpServer } from "./mcp/server.js";
36
- import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, getHeaderExtractorForProvider, } from "./quota/index.js";
38
+ import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, getHeaderExtractorForProvider, setQuotaStateDir, } from "./quota/index.js";
39
+ // Re-exports from extracted modules
40
+ export { resolveHostDispatchCapability, DIRECT_CLI_DEFAULTS, getFlag, hasFlag, getOptionalBooleanFlag, getArtifactsDir, getRootDir, getBatchResultsDir, getMaxRuns, getAgentBatchSize, getParallelWorkers, getTimeoutMs, chunkArray, getUiMode, looksLikeCliFlag, countLines, warnIfNotGitRepo, } from "./cli/args.js";
41
+ import { DIRECT_CLI_DEFAULTS, getFlag, hasFlag, getOptionalBooleanFlag, fromBase64Url, renderCommand, summarizeLaunchExit, taskResultPath, readStdinText, getArtifactsDir, getRootDir, warnIfNotGitRepo, getBatchResultsDir, getMaxRuns, getAgentBatchSize, getParallelWorkers, getTimeoutMs, getExplicitProvider, getHostModel, getHostMaxActiveSubagents, getQuotaProbeMode, resolveRunProviderName, chunkArray, getUiMode, looksLikeCliFlag, resolveHostDispatchCapability, countLines, listBatchResultFiles, } from "./cli/args.js";
42
+ import { nextStepCommand, mergeAndIngestCommand, renderDispatchReviewPrompt, renderSingleTaskFallbackStepPrompt, renderPresentReportPrompt, renderAnalyzerInstallPrompt, renderEdgeReasoningStepPrompt, renderEdgeReasoningDispatchPrompt, renderBlockedStepPrompt, } from "./cli/prompts.js";
43
+ import { writeCurrentStep, } from "./cli/steps.js";
44
+ import { WORKER_RESULT_CONTRACT_VERSION, buildWorkerResult, persistWorkerRunArtifacts, isWorkerResult, buildWorkerFailureBlocker, formatAuditResultValidationError, } from "./cli/workerResult.js";
45
+ import { DISPATCH_RESULT_MAP_FILENAME, ACTIVE_DISPATCH_FILENAME, resolveRunScopedArg, loadDispatchResultMap, entriesByTaskId, buildPendingAuditTasks, prepareDispatchArtifacts, } from "./cli/dispatch.js";
46
+ import { readWaveManifest, writeWaveManifest, removeWaveManifest, buildWaveSlotEntry, } from "./cli/waveManifest.js";
37
47
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
38
48
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
39
- const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
40
- const STEP_CONTRACT_VERSION = "audit-code-step/v1alpha1";
41
- const LARGE_FILE_PACKET_TARGET_LINES = 2500;
42
- const SMALL_MODEL_HINT_MAX_LINES = 500;
43
- const SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS = 3000;
44
- const DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS = 9000;
45
- const DIRECT_CLI_DEFAULTS = {
46
- rootDir: ".",
47
- artifactsDir: ".artifacts",
48
- maxRuns: 1000,
49
- agentBatchSize: 6,
50
- parallelWorkers: 1,
51
- timeoutMs: 30 * 60 * 1000, // 30 minutes
52
- uiMode: "headless",
53
- };
54
- // Keep the sample-run payload explicit so the demo command is deterministic.
55
49
  const SAMPLE_REPO_FILES = [
56
50
  { path: "src/api/auth.ts", size_bytes: 1240, hash: "abc123" },
57
51
  { path: "src/lib/session.ts", size_bytes: 980, hash: "def456" },
58
52
  { path: "infra/deploy.yml", size_bytes: 420, hash: "ghi789" },
59
53
  { path: "docs/notes.md", size_bytes: 300, hash: "doc111" },
60
54
  ];
61
- function isLongFlagToken(value) {
62
- return typeof value === "string" && value.startsWith("--");
63
- }
64
- // Read a long-form CLI flag while treating a following flag token as "missing".
65
- function getFlag(argv, name, fallback) {
66
- const index = argv.indexOf(name);
67
- if (index < 0)
68
- return fallback;
69
- const candidate = argv[index + 1];
70
- if (!candidate || isLongFlagToken(candidate))
71
- return fallback;
72
- return candidate;
73
- }
74
- // Boolean flags only care whether the token is present at all.
75
- function hasFlag(argv, name) {
76
- return argv.includes(name);
77
- }
78
- function getOptionalBooleanFlag(argv, name) {
79
- const raw = getFlag(argv, name);
80
- if (raw === undefined) {
81
- return undefined;
82
- }
83
- if (raw === "true") {
84
- return true;
85
- }
86
- if (raw === "false") {
87
- return false;
88
- }
89
- throw new Error(`${name} must be either true or false.`);
90
- }
91
- function optionalBooleanEnv(value) {
92
- if (value === "true")
93
- return true;
94
- if (value === "false")
95
- return false;
96
- return undefined;
97
- }
98
- export function resolveHostDispatchCapability(options) {
99
- if (options.explicit !== undefined) {
100
- return options.explicit;
101
- }
102
- if (options.sessionConfig.host_can_dispatch_subagents !== undefined) {
103
- return options.sessionConfig.host_can_dispatch_subagents;
104
- }
105
- return optionalBooleanEnv((options.env ?? process.env).AUDIT_CODE_HOST_CAN_DISPATCH) ?? true;
106
- }
107
- function toBase64Url(value) {
108
- return Buffer.from(value, "utf8").toString("base64url");
109
- }
110
- function fromBase64Url(value) {
111
- return Buffer.from(value, "base64url").toString("utf8");
112
- }
113
- function digestId(value) {
114
- return createHash("sha256").update(value).digest("hex").slice(0, 12);
115
- }
116
- function safeArtifactStem(value) {
117
- const sanitized = value
118
- .replace(/[^a-zA-Z0-9_-]+/g, "_")
119
- .replace(/^_+|_+$/g, "")
120
- .slice(0, 80);
121
- return sanitized.length > 0 ? sanitized : "artifact";
122
- }
123
- function artifactNameForId(value, extension) {
124
- return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
125
- }
126
- function quoteCommandArg(value) {
127
- return /[\s"]/u.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
128
- }
129
- function renderCommand(argv) {
130
- return argv.map((item) => quoteCommandArg(item)).join(" ");
131
- }
132
- function summarizeLaunchExit(result) {
133
- if (result.accepted !== false && !result.error) {
134
- return null;
135
- }
136
- const parts = [
137
- result.signal
138
- ? `signal ${result.signal}`
139
- : `exit code ${result.exitCode ?? "unknown"}`,
140
- result.command ? `command: ${result.command}` : null,
141
- result.stdoutPath ? `stdout: ${result.stdoutPath}` : null,
142
- result.stderrPath ? `stderr: ${result.stderrPath}` : null,
143
- result.error ?? null,
144
- ].filter((part) => Boolean(part));
145
- return parts.join("; ");
146
- }
147
- function taskResultPath(taskResultsDir, taskId) {
148
- return join(taskResultsDir, artifactNameForId(taskId, "json"));
149
- }
150
- function packetPromptPath(taskResultsDir, packetId) {
151
- return join(taskResultsDir, artifactNameForId(packetId, "prompt.md"));
152
- }
153
- async function readStdinText() {
154
- if (process.stdin.isTTY) {
155
- return "";
156
- }
157
- return await new Promise((resolveInput, reject) => {
158
- let input = "";
159
- process.stdin.setEncoding("utf8");
160
- process.stdin.on("data", (chunk) => {
161
- input += chunk;
162
- });
163
- process.stdin.on("end", () => resolveInput(input));
164
- process.stdin.on("error", reject);
165
- });
166
- }
167
- function resolveFlagPath(argv, name, fallback) {
168
- return resolve(getFlag(argv, name, fallback));
169
- }
170
- function normalizePositiveInteger(value) {
171
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
172
- return undefined;
173
- }
174
- return Math.floor(value);
175
- }
176
- function parsePositiveIntegerFlag(argv, name) {
177
- const raw = getFlag(argv, name);
178
- if (raw === undefined) {
179
- return undefined;
180
- }
181
- return normalizePositiveInteger(Number(raw));
182
- }
183
- function getArtifactsDir(argv) {
184
- return resolveFlagPath(argv, "--artifacts-dir", DIRECT_CLI_DEFAULTS.artifactsDir);
185
- }
186
- function getRootDir(argv) {
187
- return resolveFlagPath(argv, "--root", DIRECT_CLI_DEFAULTS.rootDir);
188
- }
189
- function warnIfNotGitRepo(root) {
190
- const gitEntry = join(root, ".git");
191
- if (!existsSync(gitEntry)) {
192
- console.warn(`Warning: target directory '${root}' does not appear to be a git repository. Diff-based signals will be unavailable.`);
193
- }
194
- }
195
- function getBatchResultsDir(argv) {
196
- const value = getFlag(argv, "--batch-results");
197
- return value ? resolve(value) : undefined;
198
- }
199
- function getMaxRuns(argv) {
200
- return parsePositiveIntegerFlag(argv, "--max-runs") ?? DIRECT_CLI_DEFAULTS.maxRuns;
201
- }
202
- function getAgentBatchSize(argv, sessionConfig) {
203
- return (parsePositiveIntegerFlag(argv, "--agent-batch-size") ??
204
- normalizePositiveInteger(sessionConfig.agent_task_batch_size) ??
205
- DIRECT_CLI_DEFAULTS.agentBatchSize);
206
- }
207
- function getParallelWorkers(argv, sessionConfig) {
208
- return (parsePositiveIntegerFlag(argv, "--parallel") ??
209
- normalizePositiveInteger(sessionConfig.parallel_workers) ??
210
- DIRECT_CLI_DEFAULTS.parallelWorkers);
211
- }
212
- function getTimeoutMs(argv, sessionConfig) {
213
- return (parsePositiveIntegerFlag(argv, "--timeout") ??
214
- normalizePositiveInteger(sessionConfig.timeout_ms) ??
215
- DIRECT_CLI_DEFAULTS.timeoutMs);
216
- }
217
- function getExplicitProvider(argv) {
218
- return getFlag(argv, "--provider");
219
- }
220
- function getHostModel(argv) {
221
- return getFlag(argv, "--host-model") ?? null;
222
- }
223
- function getHostMaxActiveSubagents(argv) {
224
- return parsePositiveIntegerFlag(argv, "--host-max-active-subagents") ?? null;
225
- }
226
- function getQuotaProbeMode(argv, sessionConfig) {
227
- const raw = getFlag(argv, "--quota-probe") ?? sessionConfig.quota?.probe ?? "auto";
228
- if (raw === "auto" || raw === "never" || raw === "force")
229
- return raw;
230
- return "auto";
231
- }
232
- function resolveRunProviderName(argv, sessionConfig) {
233
- return resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig);
234
- }
235
- function chunkArray(arr, size) {
236
- const chunkSize = normalizePositiveInteger(size);
237
- if (chunkSize === undefined) {
238
- throw new Error("chunkArray size must be a positive integer.");
239
- }
240
- const chunks = [];
241
- for (let i = 0; i < arr.length; i += chunkSize) {
242
- chunks.push(arr.slice(i, i + chunkSize));
243
- }
244
- return chunks;
245
- }
246
- function getUiMode(argv, fallback = DIRECT_CLI_DEFAULTS.uiMode) {
247
- const raw = getFlag(argv, "--ui");
248
- if (raw === "visible")
249
- return "visible";
250
- if (raw === "headless")
251
- return "headless";
252
- return fallback;
253
- }
254
55
  function buildEnvelope(params) {
255
56
  return {
256
57
  contract_version: ADVANCE_AUDIT_CONTRACT_VERSION,
@@ -312,30 +113,6 @@ function buildBlockedAuditState(params) {
312
113
  : item),
313
114
  };
314
115
  }
315
- async function countLines(path) {
316
- return new Promise((resolve, reject) => {
317
- let lines = 0;
318
- let byteCount = 0;
319
- let lastByte = -1;
320
- const stream = createReadStream(path);
321
- stream.on("data", (chunk) => {
322
- const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
323
- byteCount += buffer.length;
324
- for (let i = 0; i < buffer.length; ++i) {
325
- if (buffer[i] === 10)
326
- lines++;
327
- lastByte = buffer[i];
328
- }
329
- });
330
- stream.on("end", () => {
331
- if (byteCount === 0)
332
- return resolve(0);
333
- // Files not ending with \n have one final line not counted above
334
- resolve(lastByte !== 10 ? lines + 1 : lines);
335
- });
336
- stream.on("error", reject);
337
- });
338
- }
339
116
  async function buildLineIndex(root, repoManifest) {
340
117
  const entries = [];
341
118
  const batchSize = 25;
@@ -368,26 +145,6 @@ async function buildLineIndexForPaths(root, paths) {
368
145
  }));
369
146
  return Object.fromEntries(entries);
370
147
  }
371
- async function listBatchResultFiles(batchDir) {
372
- const entries = await readdir(batchDir, { withFileTypes: true });
373
- const files = entries
374
- .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
375
- .map((entry) => join(batchDir, entry.name))
376
- .sort((a, b) => a.localeCompare(b));
377
- if (files.length === 0) {
378
- throw new Error(`No JSON audit result files found in ${batchDir}.`);
379
- }
380
- return files;
381
- }
382
- function buildPendingAuditTasks(bundle) {
383
- const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
384
- const pendingTasks = (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
385
- const lineIndex = Object.fromEntries(pendingTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
386
- return orderTasksForPacketReview(pendingTasks, {
387
- graphBundle: bundle.graph_bundle,
388
- lineIndex,
389
- });
390
- }
391
148
  async function addFileLineCountHints(root, tasks) {
392
149
  const lineIndex = await buildLineIndexForPaths(root, tasks.flatMap((task) => task.file_paths));
393
150
  return tasks.map((task) => ({
@@ -477,6 +234,11 @@ async function ensureSemanticReviewRun(params) {
477
234
  const pendingTasks = await addFileLineCountHints(params.root, buildPendingAuditTasks(params.bundle));
478
235
  const pendingTasksPath = join(paths.runDir, "pending-audit-tasks.json");
479
236
  const auditResultsPath = join(paths.runDir, "audit-results.json");
237
+ const taskReadPaths = new Set();
238
+ for (const pt of pendingTasks) {
239
+ for (const fp of pt.file_paths)
240
+ taskReadPaths.add(fp);
241
+ }
480
242
  const task = {
481
243
  contract_version: "audit-code-worker/v1alpha1",
482
244
  run_id: runId,
@@ -496,6 +258,10 @@ async function ensureSemanticReviewRun(params) {
496
258
  pending_audit_tasks_path: pendingTasksPath,
497
259
  timeout_ms: params.timeoutMs,
498
260
  max_retries: 0,
261
+ access: {
262
+ read_paths: [...taskReadPaths],
263
+ write_paths: [auditResultsPath, paths.resultPath],
264
+ },
499
265
  };
500
266
  const prompt = renderWorkerPrompt(task);
501
267
  await writeWorkerTaskFiles(task, prompt, paths, params.artifactsDir, pendingTasks);
@@ -519,174 +285,6 @@ async function ensureSemanticReviewRun(params) {
519
285
  });
520
286
  return { state: blockedState, bundle: blockedBundle, activeReviewRun };
521
287
  }
522
- function nextStepCommand(root, artifactsDir, extraArgs = []) {
523
- return renderCommand([
524
- "audit-code",
525
- "next-step",
526
- "--root",
527
- root,
528
- "--artifacts-dir",
529
- artifactsDir,
530
- ...extraArgs,
531
- ]);
532
- }
533
- function mergeAndIngestCommand(artifactsDir, runId) {
534
- return renderCommand([
535
- "audit-code",
536
- "merge-and-ingest",
537
- "--run-id",
538
- runId,
539
- "--artifacts-dir",
540
- artifactsDir,
541
- ]);
542
- }
543
- function renderDispatchReviewPrompt(params) {
544
- const mergeCommand = mergeAndIngestCommand(params.artifactsDir, params.activeReviewRun.run_id);
545
- const continueCommand = nextStepCommand(params.root, params.artifactsDir);
546
- const modelLine = params.hostCanSelectSubagentModel
547
- ? "When launching each subagent, map `entry.model_hint.tier` (`small`, `standard`, `deep`) to an available host model without asking the user for model names."
548
- : "Ignore `entry.model_hint`; this host did not report per-subagent model selection.";
549
- const toolsLine = params.hostCanRestrictSubagentTools
550
- ? "Restrict review subagents to read/search plus the packet submit command named in their prompt. Do not give them source edit/write tools."
551
- : "Do not ask the user about per-subagent tool restrictions; this host did not report a callable restriction facility.";
552
- const dispatchDataLines = params.dispatchQuotaPath
553
- ? [
554
- "Read these generated files unless the current tool response already included equivalent `dispatch_plan_entries` and `dispatch_quota` fields:",
555
- "",
556
- ` Dispatch plan: ${params.dispatchPlanPath}`,
557
- ` Dispatch quota: ${params.dispatchQuotaPath}`,
558
- "",
559
- "Use the `wave_size` from the quota data. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
560
- "",
561
- "`host_concurrency_limit` records any detected hard host cap that contributed to `wave_size`.",
562
- "",
563
- "For each wave: use the `task` tool (or equivalent subagent dispatch) to launch up to `wave_size` subagents in parallel (one per entry), wait for all to finish, then start the next wave.",
564
- ]
565
- : [
566
- "Read this generated dispatch plan unless the current tool response already included equivalent `dispatch_plan_entries`:",
567
- "",
568
- ` ${params.dispatchPlanPath}`,
569
- "",
570
- "Launch one subagent for each entry in the plan.",
571
- ];
572
- return [
573
- "# audit-code dispatch review",
574
- "",
575
- ...dispatchDataLines,
576
- "",
577
- "Pass each `entry.prompt_path` literally to its subagent; do not load packet prompt files into this orchestrator context.",
578
- "",
579
- "Subagent prompt shape:",
580
- "",
581
- ' Read and follow the audit instructions in: <entry.prompt_path>',
582
- "",
583
- modelLine,
584
- toolsLine,
585
- "",
586
- "Each subagent must submit its packet through the submit command printed in its packet prompt and stop after successful submission.",
587
- "",
588
- "**After all waves complete:**",
589
- "",
590
- "Run exactly:",
591
- "",
592
- ` ${mergeCommand}`,
593
- "",
594
- "If merge-and-ingest fails, stop and report the exact command and error output. Do not manually merge results or edit audit state.",
595
- "",
596
- "If merge-and-ingest succeeds, run:",
597
- "",
598
- ` ${continueCommand}`,
599
- "",
600
- "Read and follow only the new step prompt path returned by that command.",
601
- "",
602
- ].join("\n");
603
- }
604
- function renderSingleTaskFallbackStepPrompt(params) {
605
- return [
606
- "# audit-code single-task fallback step",
607
- "",
608
- "Use this step only because the host reported no callable subagent facility.",
609
- "",
610
- "Read and follow exactly this generated single-task prompt:",
611
- "",
612
- ` ${params.singleTaskPromptPath}`,
613
- "",
614
- "Complete exactly one AuditResult for the task named there, write the JSON array to the prompt's audit_results_path, run the exact worker_command from that prompt, then stop.",
615
- "",
616
- "Do not run dispatch commands, do not prepare packets, do not run next-step again in this turn, and do not read a report after the worker command.",
617
- "",
618
- "The only backend command allowed after writing the result is:",
619
- "",
620
- ` ${renderCommand(params.activeReviewRun.worker_command)}`,
621
- "",
622
- ].join("\n");
623
- }
624
- function renderPresentReportPrompt(finalReportPath) {
625
- return [
626
- "# audit-code present report",
627
- "",
628
- "The deterministic audit is complete.",
629
- "",
630
- "Read the final audit report from the `audit-code://report/current` MCP resource (or from the file at:",
631
- ` ${finalReportPath}`,
632
- "if a Read tool is available).",
633
- "",
634
- "Present the completed audit with work blocks first.",
635
- "",
636
- "Do not run the orchestrator again for this completed audit.",
637
- "",
638
- ].join("\n");
639
- }
640
- function renderBlockedStepPrompt(reason) {
641
- return [
642
- "# audit-code blocked",
643
- "",
644
- "The audit cannot continue automatically from this step.",
645
- "",
646
- "Report this blocker verbatim and stop:",
647
- "",
648
- reason,
649
- "",
650
- ].join("\n");
651
- }
652
- async function writeCurrentStep(params) {
653
- const stepsDir = join(params.artifactsDir, "steps");
654
- await mkdir(stepsDir, { recursive: true });
655
- const promptPath = join(stepsDir, "current-prompt.md");
656
- const stepPath = join(stepsDir, "current-step.json");
657
- await writeFile(promptPath, params.prompt, "utf8");
658
- const step = {
659
- contract_version: STEP_CONTRACT_VERSION,
660
- step_kind: params.stepKind,
661
- prompt_path: promptPath,
662
- status: params.status,
663
- run_id: params.runId,
664
- allowed_commands: params.allowedCommands,
665
- stop_condition: params.stopCondition,
666
- repo_root: params.repoRoot,
667
- artifacts_dir: params.artifactsDir,
668
- artifact_paths: {
669
- current_step: stepPath,
670
- current_prompt: promptPath,
671
- ...params.artifactPaths,
672
- },
673
- };
674
- await writeJsonFile(stepPath, step);
675
- return step;
676
- }
677
- function formatAuditResultValidationError(issues) {
678
- return (`audit-results validation failed with ${issues.length} error(s):\n` +
679
- formatAuditResultIssues(issues));
680
- }
681
- function buildWorkerFailureBlocker(workerResult) {
682
- const details = workerResult.errors.filter((error) => error.trim().length > 0);
683
- return details.length > 0
684
- ? `${workerResult.summary} ${details.join(" ")}`
685
- : workerResult.summary;
686
- }
687
- function looksLikeCliFlag(value) {
688
- return isLongFlagToken(value);
689
- }
690
288
  export const cliTestUtils = {
691
289
  defaults: DIRECT_CLI_DEFAULTS,
692
290
  getFlag,
@@ -720,9 +318,15 @@ async function maybeArchiveLegacyPendingResults(auditResultsPath) {
720
318
  }
721
319
  async function runAuditStep(options) {
722
320
  const bundle = await loadArtifactBundle(options.artifactsDir);
321
+ const runLogger = new RunLogger(join(options.artifactsDir, "run.log.jsonl"), {
322
+ enabled: options.runLog ?? true,
323
+ });
723
324
  const lineIndex = bundle.repo_manifest
724
325
  ? await buildLineIndex(options.root, bundle.repo_manifest)
725
326
  : undefined;
327
+ const sizeIndex = bundle.repo_manifest
328
+ ? sizeIndexFromManifest(bundle.repo_manifest)
329
+ : undefined;
726
330
  if (looksLikeCliFlag(options.auditResultsPath)) {
727
331
  throw new Error(`Invalid audit results path '${options.auditResultsPath}'. This looks like a CLI flag rather than a file path.`);
728
332
  }
@@ -750,13 +354,27 @@ async function runAuditStep(options) {
750
354
  const externalAnalyzerResults = options.externalAnalyzerPath
751
355
  ? await readJsonFile(options.externalAnalyzerPath)
752
356
  : undefined;
357
+ const narrativeResults = options.narrativeResultsPath
358
+ ? await readJsonFile(options.narrativeResultsPath)
359
+ : undefined;
360
+ const edgeReasoningResults = options.edgeReasoningResultsPath
361
+ ? await readJsonFile(options.edgeReasoningResultsPath)
362
+ : undefined;
753
363
  const result = await advanceAudit(bundle, {
754
364
  root: options.root,
755
365
  lineIndex,
366
+ sizeIndex,
756
367
  auditResults: auditResults,
757
368
  runtimeValidationUpdates,
758
369
  externalAnalyzerResults,
370
+ narrativeResults,
371
+ edgeReasoningResults,
372
+ analyzers: options.analyzers,
373
+ graphLlmEdgeReasoning: options.graphLlmEdgeReasoning,
374
+ since: options.since,
759
375
  preferredExecutor: options.preferredExecutor,
376
+ opentoken: options.opentoken,
377
+ runLogger,
760
378
  });
761
379
  await writeCoreArtifacts(options.artifactsDir, result.updated_bundle);
762
380
  const archivedPendingResults = await maybeArchiveLegacyPendingResults(options.auditResultsPath);
@@ -805,29 +423,6 @@ async function ingestBatchAuditResults(options) {
805
423
  next_likely_step: state.status === "complete" ? null : decision.selected_obligation,
806
424
  };
807
425
  }
808
- function buildWorkerResult(params) {
809
- return {
810
- contract_version: WORKER_RESULT_CONTRACT_VERSION,
811
- run_id: params.runId,
812
- obligation_id: params.obligationId,
813
- status: params.status,
814
- progress_made: params.progressMade,
815
- selected_executor: params.selectedExecutor,
816
- artifacts_written: params.artifactsWritten,
817
- summary: params.summary,
818
- next_likely_step: params.nextLikelyStep,
819
- errors: params.errors,
820
- };
821
- }
822
- async function persistWorkerRunArtifacts(paths, workerResult, executionMode) {
823
- await writeJsonFile(paths.resultPath, workerResult);
824
- await writeJsonFile(paths.statusPath, {
825
- run_id: workerResult.run_id,
826
- status: workerResult.status,
827
- execution_mode: executionMode,
828
- result_path: paths.resultPath,
829
- });
830
- }
831
426
  async function persistConfigErrorHandoff(params) {
832
427
  const bundle = await loadArtifactBundle(params.artifactsDir);
833
428
  const blockedState = buildBlockedAuditState({
@@ -850,12 +445,6 @@ async function persistConfigErrorHandoff(params) {
850
445
  });
851
446
  await writeAuditCodeHandoffArtifacts(handoff);
852
447
  }
853
- function isWorkerResult(value) {
854
- return (typeof value === "object" &&
855
- value !== null &&
856
- value.contract_version ===
857
- WORKER_RESULT_CONTRACT_VERSION);
858
- }
859
448
  export async function runSample(argv = process.argv) {
860
449
  const repoManifest = buildRepoManifest("sample-repo", SAMPLE_REPO_FILES);
861
450
  const disposition = buildFileDisposition(repoManifest);
@@ -990,6 +579,11 @@ async function cmdAdvanceAudit(argv) {
990
579
  auditResultsPath: getFlag(argv, "--results"),
991
580
  runtimeUpdatesPath: getFlag(argv, "--updates"),
992
581
  externalAnalyzerPath,
582
+ analyzers: sessionConfig.analyzers,
583
+ graphLlmEdgeReasoning: sessionConfig.graph?.llm_edge_reasoning,
584
+ since: getFlag(argv, "--since"),
585
+ opentoken: sessionConfig.opentoken?.enabled,
586
+ runLog: sessionConfig.observability?.run_log,
993
587
  });
994
588
  if (result.selected_executor !== "agent") {
995
589
  await clearDispatchFiles(artifactsDir);
@@ -1013,6 +607,7 @@ async function cmdAdvanceAudit(argv) {
1013
607
  }
1014
608
  async function runDeterministicForNextStep(params) {
1015
609
  let lastSummary = "";
610
+ let analyzers = params.analyzers;
1016
611
  for (let index = 0; index < params.maxRuns; index++) {
1017
612
  const bundle = await loadArtifactBundle(params.artifactsDir);
1018
613
  const decision = decideNextStep(bundle);
@@ -1039,6 +634,105 @@ async function runDeterministicForNextStep(params) {
1039
634
  : join(params.artifactsDir, "audit-report.md"),
1040
635
  };
1041
636
  }
637
+ if (index === 0 && bundle.repo_manifest) {
638
+ const pendingTasks = buildPendingAuditTasks(bundle);
639
+ const taskFiles = new Set();
640
+ for (const task of pendingTasks) {
641
+ for (const fp of Object.keys(task.file_line_counts ?? {}))
642
+ taskFiles.add(fp);
643
+ }
644
+ if (taskFiles.size > 0) {
645
+ const integrity = await checkFileIntegrity(params.root, bundle.repo_manifest, [...taskFiles]);
646
+ if (!integrity.is_clean) {
647
+ console.log(`File integrity check: ${integrity.changed_files.length} changed, ${integrity.missing_files.length} missing — re-running intake.`);
648
+ await advanceAudit(bundle, { root: params.root, preferredExecutor: "intake_executor", opentoken: params.opentoken });
649
+ continue;
650
+ }
651
+ }
652
+ }
653
+ if (decision.selected_executor === "graph_enrichment_executor") {
654
+ const includedFiles = bundle.repo_manifest
655
+ ? [
656
+ ...new Set(buildPathLookup(bundle.repo_manifest, buildDispositionMap(bundle.file_disposition)).values()),
657
+ ]
658
+ : [];
659
+ const plan = resolveAnalyzerPlan(params.root, analyzers, includedFiles);
660
+ const unresolved = plan.filter(needsInstallDecision);
661
+ if (unresolved.length > 0) {
662
+ const decisionsPath = join(params.artifactsDir, "incoming", "analyzer-decisions.json");
663
+ let decisions;
664
+ try {
665
+ decisions = await readJsonFile(decisionsPath);
666
+ }
667
+ catch (error) {
668
+ if (!isFileMissingError(error))
669
+ throw error;
670
+ }
671
+ if (decisions && typeof decisions === "object") {
672
+ const settings = {};
673
+ for (const [id, value] of Object.entries(decisions)) {
674
+ if (value === "ephemeral" ||
675
+ value === "permanent" ||
676
+ value === "skip" ||
677
+ value === "repo" ||
678
+ value === "auto") {
679
+ settings[id] = value;
680
+ }
681
+ }
682
+ if (Object.keys(settings).length > 0) {
683
+ const merged = await persistAnalyzerSettings(params.artifactsDir, settings);
684
+ analyzers = merged.analyzers;
685
+ }
686
+ await unlink(decisionsPath).catch(() => { });
687
+ continue;
688
+ }
689
+ return {
690
+ kind: "analyzer_install",
691
+ state,
692
+ bundle,
693
+ unresolved,
694
+ };
695
+ }
696
+ // Phase 4B — optional edge-reasoning producing turn. Once analyzer installs
697
+ // are resolved, if the flag is on and the floor carries low-confidence
698
+ // (< 0.65) edges, emit one bounded host turn (subagent dispatch or a single
699
+ // host step) to produce reason rewrites, then re-run. The enrichment
700
+ // executor applies the host-supplied rewrites in the SAME advanceAudit call
701
+ // that merges analyzer edges and writes analyzer_capability, so graph_bundle
702
+ // and its marker stay revision-consistent (no staleness loop). Flag off or
703
+ // no candidates → fall through and run the executor with no rewrites.
704
+ if (params.graphLlmEdgeReasoning === true && bundle.graph_bundle) {
705
+ const candidates = collectLowConfidenceEdges(bundle.graph_bundle);
706
+ if (candidates.length > 0) {
707
+ const edgeReasoningResultsPath = join(params.artifactsDir, "incoming", "edge-reasoning.json");
708
+ let edgeReasoningResults;
709
+ try {
710
+ edgeReasoningResults = await readJsonFile(edgeReasoningResultsPath);
711
+ }
712
+ catch (error) {
713
+ if (!isFileMissingError(error))
714
+ throw error;
715
+ }
716
+ if (edgeReasoningResults) {
717
+ await runAuditStep({
718
+ root: params.root,
719
+ artifactsDir: params.artifactsDir,
720
+ analyzers,
721
+ graphLlmEdgeReasoning: true,
722
+ edgeReasoningResultsPath,
723
+ since: params.since,
724
+ opentoken: params.opentoken,
725
+ });
726
+ await unlink(edgeReasoningResultsPath).catch(() => { });
727
+ continue;
728
+ }
729
+ return { kind: "edge_reasoning", state, bundle, candidates };
730
+ }
731
+ }
732
+ // No undecided installs (and no pending edge reasoning): fall through to run
733
+ // the executor below (it installs for ephemeral/permanent, uses repo/cache,
734
+ // skips the rest).
735
+ }
1042
736
  if (decision.selected_executor === "design_review") {
1043
737
  const findingsPath = join(params.artifactsDir, "incoming", "design-review-findings.json");
1044
738
  let reviewFindings;
@@ -1065,6 +759,36 @@ async function runDeterministicForNextStep(params) {
1065
759
  bundle,
1066
760
  };
1067
761
  }
762
+ if (decision.selected_executor === "synthesis_narrative_executor") {
763
+ const narrativePath = join(params.artifactsDir, "incoming", "synthesis-narrative.json");
764
+ let narrativeResults;
765
+ try {
766
+ narrativeResults = await readJsonFile(narrativePath);
767
+ }
768
+ catch (error) {
769
+ if (!isFileMissingError(error))
770
+ throw error;
771
+ }
772
+ if (narrativeResults) {
773
+ await runAuditStep({
774
+ root: params.root,
775
+ artifactsDir: params.artifactsDir,
776
+ preferredExecutor: "synthesis_narrative_executor",
777
+ narrativeResultsPath: narrativePath,
778
+ opentoken: params.opentoken,
779
+ });
780
+ await unlink(narrativePath).catch(() => { });
781
+ continue;
782
+ }
783
+ if (params.narrativeEnabled) {
784
+ return {
785
+ kind: "synthesis_narrative",
786
+ state,
787
+ bundle,
788
+ };
789
+ }
790
+ // Narrative disabled: fall through so the deterministic omit runs below.
791
+ }
1068
792
  if (decision.selected_executor === "agent") {
1069
793
  return {
1070
794
  kind: "semantic_review",
@@ -1095,11 +819,45 @@ async function runDeterministicForNextStep(params) {
1095
819
  reason: lastSummary || decision.reason,
1096
820
  };
1097
821
  }
1098
- const result = await runAuditStep({
1099
- root: params.root,
1100
- artifactsDir: params.artifactsDir,
1101
- });
822
+ let result;
823
+ try {
824
+ result = await runAuditStep({
825
+ root: params.root,
826
+ artifactsDir: params.artifactsDir,
827
+ analyzers,
828
+ graphLlmEdgeReasoning: params.graphLlmEdgeReasoning,
829
+ since: params.since,
830
+ opentoken: params.opentoken,
831
+ });
832
+ }
833
+ catch (error) {
834
+ const current = await loadArtifactBundle(params.artifactsDir);
835
+ const currentState = deriveAuditState(current);
836
+ currentState.last_executor = decision.selected_executor ?? undefined;
837
+ currentState.last_obligation = decision.selected_obligation ?? undefined;
838
+ await writeCoreArtifacts(params.artifactsDir, { ...current, audit_state: currentState });
839
+ await writeJsonFile(join(params.artifactsDir, "steps", "deterministic-progress.json"), {
840
+ iteration: index + 1,
841
+ max_runs: params.maxRuns,
842
+ last_executor: decision.selected_executor,
843
+ last_obligation: decision.selected_obligation,
844
+ prior_summary: lastSummary || null,
845
+ error: error instanceof Error ? error.message : String(error),
846
+ timestamp: new Date().toISOString(),
847
+ });
848
+ const detail = error instanceof Error ? error.message : String(error);
849
+ throw new Error(`Deterministic executor ${decision.selected_executor} failed on obligation ${decision.selected_obligation} (iteration ${index + 1}/${params.maxRuns}, prior progress: ${lastSummary || "none"}): ${detail}`, { cause: error instanceof Error ? error : undefined });
850
+ }
1102
851
  lastSummary = result.progress_summary;
852
+ await writeJsonFile(join(params.artifactsDir, "steps", "deterministic-progress.json"), {
853
+ iteration: index + 1,
854
+ max_runs: params.maxRuns,
855
+ last_executor: result.selected_executor,
856
+ last_obligation: decision.selected_obligation,
857
+ progress_made: result.progress_made,
858
+ summary: result.progress_summary,
859
+ timestamp: new Date().toISOString(),
860
+ });
1103
861
  if (result.selected_executor !== "agent") {
1104
862
  await clearDispatchFiles(params.artifactsDir);
1105
863
  }
@@ -1169,6 +927,11 @@ async function cmdNextStep(argv) {
1169
927
  selfCliPath: resolve(argv[1] ?? process.argv[1] ?? ""),
1170
928
  timeoutMs: getTimeoutMs(argv, sessionConfig),
1171
929
  maxRuns: getMaxRuns(argv),
930
+ opentoken: sessionConfig.opentoken?.enabled,
931
+ narrativeEnabled: sessionConfig.synthesis?.narrative !== false,
932
+ analyzers: sessionConfig.analyzers,
933
+ graphLlmEdgeReasoning: sessionConfig.graph?.llm_edge_reasoning,
934
+ since: getFlag(argv, "--since"),
1172
935
  });
1173
936
  if (result.kind === "complete") {
1174
937
  const step = await writeCurrentStep({
@@ -1236,6 +999,130 @@ async function cmdNextStep(argv) {
1236
999
  console.log(JSON.stringify(step, null, 2));
1237
1000
  return;
1238
1001
  }
1002
+ if (result.kind === "analyzer_install") {
1003
+ const decisionsPath = join(artifactsDir, "incoming", "analyzer-decisions.json");
1004
+ await mkdir(join(artifactsDir, "incoming"), { recursive: true });
1005
+ const continueCommand = nextStepCommand(root, artifactsDir);
1006
+ const step = await writeCurrentStep({
1007
+ artifactsDir,
1008
+ stepKind: "analyzer_install",
1009
+ status: "ready",
1010
+ runId: null,
1011
+ allowedCommands: [continueCommand],
1012
+ stopCondition: "Write analyzer install decisions to the results path, then run next-step.",
1013
+ repoRoot: root,
1014
+ artifactPaths: {
1015
+ analyzer_decisions: decisionsPath,
1016
+ },
1017
+ prompt: renderAnalyzerInstallPrompt({
1018
+ unresolved: result.unresolved,
1019
+ decisionsPath,
1020
+ continueCommand,
1021
+ }),
1022
+ });
1023
+ console.log(JSON.stringify(step, null, 2));
1024
+ return;
1025
+ }
1026
+ if (result.kind === "edge_reasoning") {
1027
+ await mkdir(join(artifactsDir, "incoming"), { recursive: true });
1028
+ const edgeReasoningResultsPath = join(artifactsDir, "incoming", "edge-reasoning.json");
1029
+ const continueCommand = nextStepCommand(root, artifactsDir);
1030
+ const basePrompt = buildEdgeReasoningPrompt(result.candidates);
1031
+ const contentHash = edgeReasoningContentHash(result.candidates);
1032
+ if (hostCanDispatch) {
1033
+ // Dispatch path: isolate the (potentially large) edge-list prompt in a file
1034
+ // and have the host fan it out to one subagent, mirroring the packet review
1035
+ // dispatch contract. The subagent writes the rewrites file; next-step applies.
1036
+ const edgeReasoningPromptPath = join(artifactsDir, "incoming", "edge-reasoning-prompt.md");
1037
+ await writeFile(edgeReasoningPromptPath, basePrompt, "utf8");
1038
+ const step = await writeCurrentStep({
1039
+ artifactsDir,
1040
+ stepKind: "edge_reasoning_dispatch",
1041
+ status: "ready",
1042
+ runId: null,
1043
+ allowedCommands: [continueCommand],
1044
+ stopCondition: "Dispatch one subagent to write the edge-reasoning rewrites, then run next-step.",
1045
+ repoRoot: root,
1046
+ artifactPaths: {
1047
+ edge_reasoning_prompt: edgeReasoningPromptPath,
1048
+ edge_reasoning_results: edgeReasoningResultsPath,
1049
+ },
1050
+ prompt: renderEdgeReasoningDispatchPrompt({
1051
+ promptPath: edgeReasoningPromptPath,
1052
+ resultsPath: edgeReasoningResultsPath,
1053
+ continueCommand,
1054
+ contentHash,
1055
+ candidateCount: result.candidates.length,
1056
+ }),
1057
+ access: {
1058
+ read_paths: [edgeReasoningPromptPath],
1059
+ write_paths: [edgeReasoningResultsPath],
1060
+ },
1061
+ });
1062
+ console.log(JSON.stringify(step, null, 2));
1063
+ return;
1064
+ }
1065
+ // One-step fallback (no callable subagent facility): the host produces the
1066
+ // rewrites itself in a single bounded turn, mirroring the narrative step.
1067
+ const step = await writeCurrentStep({
1068
+ artifactsDir,
1069
+ stepKind: "edge_reasoning",
1070
+ status: "ready",
1071
+ runId: null,
1072
+ allowedCommands: [continueCommand],
1073
+ stopCondition: "Write the edge-reasoning rewrites to the results path, then run next-step.",
1074
+ repoRoot: root,
1075
+ artifactPaths: {
1076
+ edge_reasoning_results: edgeReasoningResultsPath,
1077
+ },
1078
+ prompt: renderEdgeReasoningStepPrompt({
1079
+ basePrompt,
1080
+ resultsPath: edgeReasoningResultsPath,
1081
+ continueCommand,
1082
+ contentHash,
1083
+ }),
1084
+ access: {
1085
+ read_paths: [],
1086
+ write_paths: [edgeReasoningResultsPath],
1087
+ },
1088
+ });
1089
+ console.log(JSON.stringify(step, null, 2));
1090
+ return;
1091
+ }
1092
+ if (result.kind === "synthesis_narrative") {
1093
+ const narrativeResultsPath = join(artifactsDir, "incoming", "synthesis-narrative.json");
1094
+ await mkdir(join(artifactsDir, "incoming"), { recursive: true });
1095
+ const continueCommand = nextStepCommand(root, artifactsDir);
1096
+ const basePrompt = result.bundle.audit_findings
1097
+ ? renderSynthesisNarrativePrompt(result.bundle.audit_findings)
1098
+ : "# Synthesis narrative\n\nNo findings report is available; write an empty themes array.";
1099
+ const fullPrompt = [
1100
+ basePrompt,
1101
+ "## Results path",
1102
+ "",
1103
+ "Write the SynthesisNarrative JSON object to:",
1104
+ "",
1105
+ ` ${narrativeResultsPath}`,
1106
+ "",
1107
+ `Then run: ${continueCommand}`,
1108
+ "",
1109
+ ].join("\n");
1110
+ const step = await writeCurrentStep({
1111
+ artifactsDir,
1112
+ stepKind: "synthesis_narrative",
1113
+ status: "ready",
1114
+ runId: null,
1115
+ allowedCommands: [continueCommand],
1116
+ stopCondition: "Write the synthesis narrative to the results path, then run next-step.",
1117
+ repoRoot: root,
1118
+ artifactPaths: {
1119
+ synthesis_narrative_results: narrativeResultsPath,
1120
+ },
1121
+ prompt: fullPrompt,
1122
+ });
1123
+ console.log(JSON.stringify(step, null, 2));
1124
+ return;
1125
+ }
1239
1126
  if (!hostCanDispatch) {
1240
1127
  const singleTaskPromptPath = join(artifactsDir, "dispatch", "current-single-task-prompt.md");
1241
1128
  const workerCommand = renderCommand(result.activeReviewRun.worker_command);
@@ -1258,11 +1145,16 @@ async function cmdNextStep(argv) {
1258
1145
  singleTaskPromptPath,
1259
1146
  activeReviewRun: result.activeReviewRun,
1260
1147
  }),
1148
+ access: {
1149
+ read_paths: [singleTaskPromptPath],
1150
+ write_paths: [result.activeReviewRun.audit_results_path],
1151
+ },
1261
1152
  });
1262
1153
  console.log(JSON.stringify(step, null, 2));
1263
1154
  return;
1264
1155
  }
1265
1156
  const dispatch = await prepareDispatchArtifacts({
1157
+ packageRoot,
1266
1158
  runId: result.activeReviewRun.run_id,
1267
1159
  artifactsDir,
1268
1160
  root,
@@ -1299,6 +1191,13 @@ async function cmdNextStep(argv) {
1299
1191
  hostCanRestrictSubagentTools,
1300
1192
  hostCanSelectSubagentModel,
1301
1193
  }),
1194
+ access: {
1195
+ read_paths: [
1196
+ dispatch.dispatch_plan_path,
1197
+ ...(dispatch.dispatch_quota_path ? [dispatch.dispatch_quota_path] : []),
1198
+ ],
1199
+ write_paths: [],
1200
+ },
1302
1201
  });
1303
1202
  console.log(JSON.stringify(step, null, 2));
1304
1203
  }
@@ -1349,6 +1248,38 @@ async function cmdRunToCompletion(argv) {
1349
1248
  while (runCount < maxRuns) {
1350
1249
  const bundle = await loadArtifactBundle(artifactsDir);
1351
1250
  const decision = decideNextStep(bundle);
1251
+ // Resume interrupted parallel wave: ingest any results that workers
1252
+ // wrote before the previous process exited.
1253
+ const priorWave = await readWaveManifest(artifactsDir);
1254
+ if (priorWave) {
1255
+ process.stderr.write(`[audit-code] Recovering interrupted wave (${priorWave.slots.length} slot(s), obligation ${priorWave.obligation_id}).\n`);
1256
+ let recoveredProgress = false;
1257
+ for (const entry of priorWave.slots) {
1258
+ try {
1259
+ const results = await readJsonFile(entry.audit_results_path);
1260
+ if (!results || results.length === 0)
1261
+ continue;
1262
+ const stepResult = await runAuditStep({
1263
+ root,
1264
+ artifactsDir,
1265
+ preferredExecutor: "result_ingestion_executor",
1266
+ auditResultsPath: entry.audit_results_path,
1267
+ });
1268
+ if (stepResult.progress_made) {
1269
+ recoveredProgress = true;
1270
+ anyProgress = true;
1271
+ for (const a of stepResult.artifacts_written)
1272
+ artifactsWritten.add(a);
1273
+ }
1274
+ }
1275
+ catch {
1276
+ process.stderr.write(`[audit-code] Skipping unreadable results for ${entry.run_id}.\n`);
1277
+ }
1278
+ }
1279
+ await removeWaveManifest(artifactsDir);
1280
+ if (recoveredProgress)
1281
+ continue;
1282
+ }
1352
1283
  if (decision.selected_executor === "agent" &&
1353
1284
  bundle.audit_tasks?.some((t) => t.tags?.includes("selective_deepening") &&
1354
1285
  t.status !== "complete") &&
@@ -1402,6 +1333,11 @@ async function cmdRunToCompletion(argv) {
1402
1333
  const blockPendingTasks = await addFileLineCountHints(root, buildPendingAuditTasks(bundle));
1403
1334
  const blockPendingTasksPath = join(blockPaths.runDir, "pending-audit-tasks.json");
1404
1335
  const blockAuditResultsPath = join(blockPaths.runDir, "audit-results.json");
1336
+ const blockReadPaths = new Set();
1337
+ for (const pt of blockPendingTasks) {
1338
+ for (const fp of pt.file_paths)
1339
+ blockReadPaths.add(fp);
1340
+ }
1405
1341
  const blockTask = {
1406
1342
  contract_version: "audit-code-worker/v1alpha1",
1407
1343
  run_id: blockRunId,
@@ -1421,6 +1357,10 @@ async function cmdRunToCompletion(argv) {
1421
1357
  pending_audit_tasks_path: blockPendingTasksPath,
1422
1358
  timeout_ms: timeoutMs,
1423
1359
  max_retries: 0,
1360
+ access: {
1361
+ read_paths: [...blockReadPaths],
1362
+ write_paths: [blockAuditResultsPath, blockPaths.resultPath],
1363
+ },
1424
1364
  };
1425
1365
  const blockPrompt = renderWorkerPrompt(blockTask);
1426
1366
  await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir, blockPendingTasks);
@@ -1484,7 +1424,8 @@ async function cmdRunToCompletion(argv) {
1484
1424
  const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
1485
1425
  const allCandidateTasks = buildPendingAuditTasks(bundle);
1486
1426
  const candidateGroups = chunkArray(allCandidateTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
1487
- const slotTokenEstimates = candidateGroups.map((g) => estimateTaskGroupTokens(g));
1427
+ const candidateSizeIndex = sizeIndexFromManifest(bundle.repo_manifest);
1428
+ const slotTokenEstimates = candidateGroups.map((g) => estimateTaskGroupTokens(g, candidateSizeIndex));
1488
1429
  const providerLimits = await provider.queryLimits?.(hostModel)
1489
1430
  .then((r) => r ? { ...r, source: "provider_query" } : null)
1490
1431
  .catch(() => null)
@@ -1526,6 +1467,11 @@ async function cmdRunToCompletion(argv) {
1526
1467
  const slotPaths = getRunPaths(artifactsDir, slotRunId);
1527
1468
  const slotAuditResultsPath = join(slotPaths.runDir, "audit-results.json");
1528
1469
  const slotPendingTasksPath = join(slotPaths.runDir, "pending-audit-tasks.json");
1470
+ const slotReadPaths = new Set();
1471
+ for (const t of group) {
1472
+ for (const fp of t.file_paths)
1473
+ slotReadPaths.add(fp);
1474
+ }
1529
1475
  const slotTask = {
1530
1476
  contract_version: "audit-code-worker/v1alpha1",
1531
1477
  run_id: slotRunId,
@@ -1540,6 +1486,10 @@ async function cmdRunToCompletion(argv) {
1540
1486
  worker_command_mode: "deferred",
1541
1487
  timeout_ms: timeoutMs,
1542
1488
  max_retries: 0,
1489
+ access: {
1490
+ read_paths: [...slotReadPaths],
1491
+ write_paths: [slotAuditResultsPath, slotPaths.resultPath],
1492
+ },
1543
1493
  };
1544
1494
  const slotPrompt = renderWorkerPrompt(slotTask);
1545
1495
  await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir, group, { updateDispatch: false });
@@ -1556,6 +1506,12 @@ async function cmdRunToCompletion(argv) {
1556
1506
  pending_audit_tasks_path: slot.pendingTasksPath,
1557
1507
  })), workerSlots.flatMap((slot) => slot.group));
1558
1508
  const parallelStartedAt = new Date().toISOString();
1509
+ await writeWaveManifest(artifactsDir, {
1510
+ obligation_id: obligationId ?? "unknown",
1511
+ started_at: parallelStartedAt,
1512
+ pid: process.pid,
1513
+ slots: workerSlots.map(buildWaveSlotEntry),
1514
+ });
1559
1515
  const { results: launchResults } = await runSlidingWindow(workerSlots.map((slot) => () => provider.launch({
1560
1516
  repoRoot: root,
1561
1517
  runId: slot.runId,
@@ -1710,6 +1666,7 @@ async function cmdRunToCompletion(argv) {
1710
1666
  }
1711
1667
  }
1712
1668
  }
1669
+ await removeWaveManifest(artifactsDir);
1713
1670
  if (batchErrors.length > 0) {
1714
1671
  const bundleAfter = await loadArtifactBundle(artifactsDir);
1715
1672
  const blockedState = buildBlockedAuditState({
@@ -1774,6 +1731,8 @@ async function cmdRunToCompletion(argv) {
1774
1731
  auditResultsPath,
1775
1732
  runtimeUpdatesPath,
1776
1733
  externalAnalyzerPath,
1734
+ analyzers: sessionConfig.analyzers,
1735
+ since: getFlag(argv, "--since"),
1777
1736
  });
1778
1737
  workerResult = {
1779
1738
  contract_version: WORKER_RESULT_CONTRACT_VERSION,
@@ -1884,6 +1843,13 @@ async function cmdRunToCompletion(argv) {
1884
1843
  const providerAuditResultsPath = preferredExecutor === "agent"
1885
1844
  ? join(paths.runDir, "audit-results.json")
1886
1845
  : auditResultsPath;
1846
+ const providerReadPaths = new Set();
1847
+ if (pendingAuditTasks) {
1848
+ for (const pt of pendingAuditTasks) {
1849
+ for (const fp of pt.file_paths)
1850
+ providerReadPaths.add(fp);
1851
+ }
1852
+ }
1887
1853
  const task = {
1888
1854
  contract_version: "audit-code-worker/v1alpha1",
1889
1855
  run_id: runId,
@@ -1905,6 +1871,10 @@ async function cmdRunToCompletion(argv) {
1905
1871
  external_analyzer_results_path: externalAnalyzerPath,
1906
1872
  timeout_ms: timeoutMs,
1907
1873
  max_retries: 0,
1874
+ access: providerReadPaths.size > 0 ? {
1875
+ read_paths: [...providerReadPaths],
1876
+ write_paths: [providerAuditResultsPath ?? paths.resultPath, paths.resultPath],
1877
+ } : undefined,
1908
1878
  };
1909
1879
  const prompt = renderWorkerPrompt(task);
1910
1880
  await writeWorkerTaskFiles(task, prompt, paths, artifactsDir, pendingAuditTasks);
@@ -2141,520 +2111,12 @@ async function cmdWorkerRun(argv) {
2141
2111
  process.exitCode = 1;
2142
2112
  }
2143
2113
  }
2144
- const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
2145
- const ACTIVE_DISPATCH_FILENAME = "active-dispatch.json";
2146
- function dispatchResultMapPath(runDir) {
2147
- return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
2148
- }
2149
- function resolveRunScopedArg(argv, rawFlag, b64Flag) {
2150
- const raw = getFlag(argv, rawFlag);
2151
- const encoded = getFlag(argv, b64Flag);
2152
- return raw ?? (encoded ? fromBase64Url(encoded) : undefined);
2153
- }
2154
- async function loadDispatchResultMap(runDir) {
2155
- try {
2156
- return await readJsonFile(dispatchResultMapPath(runDir));
2157
- }
2158
- catch (error) {
2159
- if (!isFileMissingError(error)) {
2160
- throw error;
2161
- }
2162
- return null;
2163
- }
2164
- }
2165
- function entriesByTaskId(entries) {
2166
- return new Map(entries.map((entry) => [entry.task_id, entry]));
2167
- }
2168
- function isIsolatedLargeFilePacket(packet) {
2169
- return (packet.file_paths.length === 1 &&
2170
- packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES);
2171
- }
2172
- function buildDispatchComplexity(packet, largeFileMode) {
2173
- return {
2174
- priority: packet.priority,
2175
- task_count: packet.task_ids.length,
2176
- file_count: packet.file_paths.length,
2177
- total_lines: packet.total_lines,
2178
- estimated_tokens: packet.estimated_tokens,
2179
- lenses: packet.lenses,
2180
- tags: packet.tags ?? [],
2181
- large_file_mode: largeFileMode,
2182
- };
2183
- }
2184
- function buildDispatchModelHint(complexity) {
2185
- const deepReasons = [];
2186
- if (complexity.priority === "high")
2187
- deepReasons.push("high_priority");
2188
- if (complexity.large_file_mode)
2189
- deepReasons.push("isolated_large_file");
2190
- if (complexity.estimated_tokens >= DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS) {
2191
- deepReasons.push("high_estimated_tokens");
2192
- }
2193
- if (complexity.tags.some((tag) => tag === "critical_flow" || tag.startsWith("critical_flow:"))) {
2194
- deepReasons.push("critical_flow");
2195
- }
2196
- if (complexity.tags.some((tag) => tag === "external_analyzer_signal" || tag.startsWith("external_tool:"))) {
2197
- deepReasons.push("external_analyzer_signal");
2198
- }
2199
- if (complexity.tags.includes("lens_verification")) {
2200
- deepReasons.push("lens_verification");
2201
- }
2202
- if (deepReasons.length > 0) {
2203
- return { tier: "deep", reasons: deepReasons };
2204
- }
2205
- const sensitiveLenses = new Set(["security", "data_integrity", "reliability"]);
2206
- const hasSensitiveLens = complexity.lenses.some((lens) => sensitiveLenses.has(lens));
2207
- if (complexity.priority === "low" &&
2208
- complexity.total_lines <= SMALL_MODEL_HINT_MAX_LINES &&
2209
- complexity.estimated_tokens <= SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS &&
2210
- !hasSensitiveLens &&
2211
- complexity.tags.length === 0) {
2212
- return { tier: "small", reasons: ["small_low_priority_packet"] };
2213
- }
2214
- const reasons = [];
2215
- if (complexity.priority === "medium")
2216
- reasons.push("medium_priority");
2217
- if (hasSensitiveLens)
2218
- reasons.push("sensitive_lens");
2219
- if (complexity.total_lines > SMALL_MODEL_HINT_MAX_LINES) {
2220
- reasons.push("moderate_size");
2221
- }
2222
- return {
2223
- tier: "standard",
2224
- reasons: reasons.length > 0 ? reasons : ["default_review_packet"],
2225
- };
2226
- }
2227
- function withinRoot(root, path) {
2228
- const rootPath = resolve(root);
2229
- const absolutePath = resolve(rootPath, path);
2230
- const relativePath = relative(rootPath, absolutePath);
2231
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
2232
- throw new Error(`Path '${path}' escapes repository root '${rootPath}'.`);
2233
- }
2234
- return absolutePath;
2235
- }
2236
- function renderAnchorPreview(summary, anchorPath) {
2237
- const preview = summary.anchors.slice(0, 24).map((anchor) => {
2238
- const location = anchor.line ? `${summary.path}:${anchor.line}` : summary.path;
2239
- const detail = anchor.detail ? ` - ${anchor.detail}` : "";
2240
- return `- ${location} [${anchor.kind}] ${anchor.name}${detail}`;
2241
- });
2242
- return [
2243
- "## Large File Review Mode",
2244
- "This packet is intentionally isolated because it covers one large file.",
2245
- "Use targeted reads/searches within this file, guided by the mechanical anchors.",
2246
- "Do not read unrelated files unless a finding cannot be evidenced without a direct boundary check.",
2247
- `Anchor file: ${anchorPath}`,
2248
- `Anchor counts: symbols=${summary.counts.symbols}, routes=${summary.counts.routes}, keywords=${summary.counts.keywords}, graph_edges=${summary.counts.graph_edges}, analyzer_signals=${summary.counts.analyzer_signals}, omitted=${summary.omitted_anchor_count}`,
2249
- "Anchor preview:",
2250
- ...(preview.length > 0 ? preview : ["- no anchors extracted beyond file boundaries"]),
2251
- "",
2252
- ];
2253
- }
2254
- function formatPacketConfidence(value) {
2255
- return typeof value === "number" && Number.isFinite(value)
2256
- ? value.toFixed(2)
2257
- : "n/a";
2258
- }
2259
- function renderPacketGraphContext(packet) {
2260
- const hasContext = (packet.entrypoints?.length ?? 0) > 0 ||
2261
- (packet.key_edges?.length ?? 0) > 0 ||
2262
- (packet.boundary_files?.length ?? 0) > 0 ||
2263
- packet.quality !== undefined;
2264
- if (!hasContext) {
2265
- return [];
2266
- }
2267
- const lines = ["## Packet graph context"];
2268
- if (packet.entrypoints?.length) {
2269
- lines.push("Entrypoints:");
2270
- lines.push(...packet.entrypoints.map((entrypoint) => `- ${entrypoint}`));
2271
- }
2272
- if (packet.key_edges?.length) {
2273
- lines.push("Key internal edges:");
2274
- lines.push(...packet.key_edges.map((edge) => {
2275
- const kind = edge.kind ? ` [${edge.kind}]` : "";
2276
- const reason = edge.reason ? ` - ${edge.reason}` : "";
2277
- return `- ${edge.from} -> ${edge.to}${kind} confidence=${formatPacketConfidence(edge.confidence)}${reason}`;
2278
- }));
2279
- }
2280
- if (packet.boundary_files?.length) {
2281
- lines.push("Boundary files to check only when evidence crosses the packet:");
2282
- lines.push(...packet.boundary_files.map((path) => `- ${path}`));
2283
- }
2284
- if (packet.quality) {
2285
- lines.push(`Quality: cohesion=${packet.quality.cohesion_score}, internal_edges=${packet.quality.internal_edge_count}, boundary_edges=${packet.quality.boundary_edge_count}, unexplained_files=${packet.quality.unexplained_file_count}`);
2286
- }
2287
- lines.push("");
2288
- return lines;
2289
- }
2290
- async function prepareDispatchArtifacts(params) {
2291
- const runId = params.runId;
2292
- const artifactsDir = params.artifactsDir;
2293
- const runDir = join(artifactsDir, "runs", runId);
2294
- const taskResultsDir = join(runDir, "task-results");
2295
- const dispatchPlanPath = join(runDir, "dispatch-plan.json");
2296
- let reviewRoot = params.root;
2297
- try {
2298
- const workerTask = await readJsonFile(join(runDir, "task.json"));
2299
- reviewRoot ??= workerTask.repo_root;
2300
- }
2301
- catch (error) {
2302
- if (!isFileMissingError(error)) {
2303
- throw error;
2304
- }
2305
- }
2306
- const bundle = await loadArtifactBundle(artifactsDir);
2307
- const tasksPath = join(runDir, "pending-audit-tasks.json");
2308
- const tasks = await readJsonFile(tasksPath).catch((error) => {
2309
- if (isFileMissingError(error))
2310
- return buildPendingAuditTasks(bundle);
2311
- throw error;
2312
- });
2313
- const sessionConfig = params.sessionConfig ?? (await loadSessionConfig(artifactsDir).catch(() => ({})));
2314
- const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
2315
- const lensDefs = await readJsonFile(lensDefsPath);
2316
- await mkdir(taskResultsDir, { recursive: true });
2317
- // On resume: skip tasks whose result files already exist from a prior dispatch.
2318
- const priorResultTaskIds = new Set();
2319
- for (const task of tasks) {
2320
- if (existsSync(taskResultPath(taskResultsDir, task.task_id))) {
2321
- priorResultTaskIds.add(task.task_id);
2322
- }
2323
- }
2324
- const dispatchTasks = priorResultTaskIds.size > 0
2325
- ? tasks.filter((task) => !priorResultTaskIds.has(task.task_id))
2326
- : tasks;
2327
- const lineIndex = Object.fromEntries(dispatchTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
2328
- const orderedTasks = orderTasksForPacketReview(dispatchTasks, {
2329
- graphBundle: bundle.graph_bundle,
2330
- lineIndex,
2331
- });
2332
- const packets = buildReviewPackets(orderedTasks, {
2333
- graphBundle: bundle.graph_bundle,
2334
- lineIndex,
2335
- });
2336
- const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
2337
- const resultPathByTaskId = new Map(orderedTasks.map((task) => [
2338
- task.task_id,
2339
- taskResultPath(taskResultsDir, task.task_id),
2340
- ]));
2341
- const resultPathSet = new Set(resultPathByTaskId.values());
2342
- if (resultPathSet.size !== resultPathByTaskId.size) {
2343
- throw new Error("prepare-dispatch generated duplicate result paths; task ids must be uniquely addressable.");
2344
- }
2345
- const plan = [];
2346
- const resultMapEntries = [];
2347
- for (const task of tasks) {
2348
- if (priorResultTaskIds.has(task.task_id)) {
2349
- resultMapEntries.push({
2350
- packet_id: "__prior_dispatch__",
2351
- task_id: task.task_id,
2352
- result_path: taskResultPath(taskResultsDir, task.task_id),
2353
- });
2354
- }
2355
- }
2356
- let largestPacketId = null;
2357
- let largestLines = 0;
2358
- let largestEstimatedTokens = 0;
2359
- const warnings = [];
2360
- for (const packet of packets) {
2361
- const promptPath = packetPromptPath(taskResultsDir, packet.packet_id);
2362
- const packetTasks = packet.task_ids
2363
- .map((taskId) => tasksById.get(taskId))
2364
- .filter((task) => task !== undefined);
2365
- if (packet.total_lines > largestLines) {
2366
- largestLines = packet.total_lines;
2367
- largestEstimatedTokens = packet.estimated_tokens;
2368
- largestPacketId = packet.packet_id;
2369
- }
2370
- const largeFileMode = isIsolatedLargeFilePacket(packet);
2371
- if (packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES && !largeFileMode) {
2372
- warnings.push({
2373
- code: "large_packet",
2374
- message: `large packet ${packet.packet_id} (~${packet.total_lines} lines) may hit quota limits`,
2375
- });
2376
- }
2377
- for (const task of packetTasks) {
2378
- if (!lensDefs[task.lens]) {
2379
- warnings.push({
2380
- code: "missing_lens_definition",
2381
- message: `no lens definition for '${task.lens}' (task ${task.task_id})`,
2382
- });
2383
- }
2384
- }
2385
- const fileList = packet.file_paths.map((path) => {
2386
- const lines = packet.file_line_counts[path] ?? 0;
2387
- return `- ${path} (${lines} lines)`;
2388
- }).join("\n");
2389
- let anchorPath = null;
2390
- let anchorSummary = null;
2391
- if (largeFileMode) {
2392
- const filePath = packet.file_paths[0];
2393
- if (!reviewRoot) {
2394
- warnings.push({
2395
- code: "large_file_anchor_unavailable",
2396
- message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
2397
- });
2398
- }
2399
- else {
2400
- try {
2401
- const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
2402
- const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
2403
- anchorSummary = buildFileAnchorSummary({
2404
- path: filePath,
2405
- content,
2406
- totalLines,
2407
- graphBundle: bundle.graph_bundle,
2408
- externalAnalyzerResults: bundle.external_analyzer_results,
2409
- });
2410
- anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
2411
- await writeJsonFile(anchorPath, anchorSummary);
2412
- }
2413
- catch (error) {
2414
- warnings.push({
2415
- code: "large_file_anchor_failed",
2416
- message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
2417
- (error instanceof Error ? error.message : String(error)),
2418
- });
2419
- }
2420
- }
2421
- }
2422
- const largeFileSection = anchorSummary && anchorPath
2423
- ? renderAnchorPreview(anchorSummary, anchorPath)
2424
- : largeFileMode
2425
- ? [
2426
- "## Large File Review Mode",
2427
- "This packet is intentionally isolated because it covers one large file.",
2428
- "Use targeted reads/searches within this file only.",
2429
- "No mechanical anchor file was available, so rely on targeted symbol and keyword searches before reading broad ranges.",
2430
- "",
2431
- ]
2432
- : [];
2433
- const taskSections = packetTasks.flatMap((task) => {
2434
- const lensDef = lensDefs[task.lens];
2435
- const inputLines = task.inputs
2436
- ? Object.entries(task.inputs)
2437
- .sort(([a], [b]) => a.localeCompare(b))
2438
- .map(([key, value]) => `input.${key}: ${value}`)
2439
- : [];
2440
- const isLensVerification = task.tags?.includes("lens_verification") ?? false;
2441
- const coverageTemplate = task.file_paths.map((path) => ({
2442
- path,
2443
- total_lines: task.file_line_counts?.[path] ?? lineIndex[path] ?? 0,
2444
- }));
2445
- return [
2446
- `### ${task.task_id}`,
2447
- `unit_id: ${task.unit_id}`,
2448
- `pass_id: ${task.pass_id}`,
2449
- `lens: ${task.lens}`,
2450
- ...(task.tags?.length ? [`tags: ${task.tags.join(", ")}`] : []),
2451
- ...inputLines,
2452
- `rationale: ${task.rationale}`,
2453
- "",
2454
- `Lens guidance: ${lensDef?.description ?? task.lens}`,
2455
- `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
2456
- ...(isLensVerification
2457
- ? [
2458
- "",
2459
- "Lens verification mode: review the prior result summary in the rationale and use only targeted source checks.",
2460
- "Do not redo every packet and do not write direct findings for this task.",
2461
- "Return findings: [] plus verification metadata. Include followup_tasks only for bounded, specific re-review packets.",
2462
- ]
2463
- : []),
2464
- "",
2465
- "file_coverage (copy exactly into your AuditResult for this task):",
2466
- "```json",
2467
- JSON.stringify(coverageTemplate),
2468
- "```",
2469
- "",
2470
- ];
2471
- });
2472
- const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
2473
- `--run-id-b64 ${toBase64Url(runId)} ` +
2474
- `--packet-id-b64 ${toBase64Url(packet.packet_id)} ` +
2475
- `--artifacts-dir-b64 ${toBase64Url(artifactsDir)}`;
2476
- const complexity = buildDispatchComplexity(packet, largeFileMode);
2477
- for (const task of packetTasks) {
2478
- resultMapEntries.push({
2479
- packet_id: packet.packet_id,
2480
- task_id: task.task_id,
2481
- result_path: resultPathByTaskId.get(task.task_id),
2482
- });
2483
- }
2484
- const prompt = [
2485
- "You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
2486
- "",
2487
- "## Packet",
2488
- `packet_id: ${packet.packet_id}`,
2489
- `task_count: ${packet.task_ids.length}`,
2490
- `lenses: ${packet.lenses.join(", ")}`,
2491
- `estimated_tokens: ${packet.estimated_tokens}`,
2492
- "",
2493
- "## Files to read",
2494
- largeFileMode
2495
- ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
2496
- : "Use your Read tool. Paths are repo-relative from the current working directory.",
2497
- "Prefer host Read/Grep tools. On native Windows, do not use Unix pipelines like `grep ... | head`; if shell search is unavoidable, use `Select-String` as a fallback.",
2498
- fileList,
2499
- "",
2500
- ...renderPacketGraphContext(packet),
2501
- ...largeFileSection,
2502
- "## Tasks",
2503
- ...taskSections,
2504
- "## Output",
2505
- "Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
2506
- "remediate findings, create extra task results, or run unrelated audits.",
2507
- "Produce one JSON array containing exactly one AuditResult object for each listed task.",
2508
- "",
2509
- "Required AuditResult fields:",
2510
- " task_id copy from the task metadata",
2511
- " unit_id copy from the task metadata",
2512
- " pass_id copy from the task metadata",
2513
- " lens copy from the task metadata",
2514
- " file_coverage [{path, total_lines}] - copy the exact template from each task section above",
2515
- " findings [] or array of finding objects",
2516
- "",
2517
- "Lens verification tasks:",
2518
- " tasks tagged lens_verification must use findings: [] and include verification:",
2519
- " {verified: boolean, needs_followup: boolean, concerns?: string[],",
2520
- " coverage_concerns?: string[], confidence_concerns?: string[],",
2521
- " followup_tasks?: AuditTask[]}.",
2522
- " Follow-up AuditTask suggestions must stay bounded to files in this packet and use the same lens.",
2523
- "",
2524
- "Each finding object:",
2525
- " id unique ID, e.g. \"COR-001\"",
2526
- " title short title",
2527
- " category specific finding category, such as missing-validation or command-execution",
2528
- " severity critical|high|medium|low|info",
2529
- " confidence high|medium|low",
2530
- " lens must match the task lens exactly",
2531
- " summary 1-2 sentence description",
2532
- " affected_files [{path, line_start?, line_end?, symbol?}] - objects, not strings; min 1 entry",
2533
- " evidence [\"path/to/file.ts:42 - description of what you see there\"] - min 1 entry",
2534
- "",
2535
- "Constraints:",
2536
- "1. line_end must not exceed the file's actual line count.",
2537
- "2. affected_files entries are objects with a path key, not plain strings.",
2538
- "3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
2539
- "4. findings: [] is correct when you find nothing genuine.",
2540
- "",
2541
- "## Submit",
2542
- "Pipe the JSON array on stdin to this command:",
2543
- ` ${submitCommand}`,
2544
- "",
2545
- "The command validates and writes the packet-owned result files. Exit 0 means accepted.",
2546
- "Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
2547
- "",
2548
- "## Final response",
2549
- `After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
2550
- ].join("\n");
2551
- await writeFile(promptPath, prompt, "utf8");
2552
- plan.push({
2553
- packet_id: packet.packet_id,
2554
- description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)` +
2555
- (largeFileMode ? " [isolated large-file mode]" : ""),
2556
- prompt_path: promptPath,
2557
- complexity,
2558
- model_hint: buildDispatchModelHint(complexity),
2559
- });
2560
- }
2561
- await writeJsonFile(dispatchPlanPath, plan);
2562
- await writeJsonFile(dispatchResultMapPath(runDir), {
2563
- contract_version: "audit-code-dispatch-results/v1alpha1",
2564
- run_id: runId,
2565
- entries: resultMapEntries,
2566
- });
2567
- // Compute and write dispatch-quota.json
2568
- const hostModel = params.hostModel ?? null;
2569
- const perPacketTokens = plan.map((p) => p.complexity.estimated_tokens);
2570
- const quotaProviderName = resolveFreshSessionProviderName(undefined, sessionConfig);
2571
- const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
2572
- const quotaState = await readQuotaState().catch(() => ({ version: 2, entries: {} }));
2573
- const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
2574
- const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
2575
- explicitLimit: params.hostActiveSubagentLimit,
2576
- sessionConfig,
2577
- });
2578
- const dispatchCachedLimits = await lookupDiscoveredLimits(quotaProviderKey).catch(() => null);
2579
- const waveSchedule = scheduleWave({
2580
- providerName: quotaProviderName,
2581
- sessionConfig,
2582
- hostModel,
2583
- requestedConcurrency: sessionConfig.parallel_workers ?? plan.length,
2584
- estimatedSlotTokens: perPacketTokens,
2585
- quotaStateEntry,
2586
- hostConcurrencyLimit,
2587
- discoveredLimits: dispatchCachedLimits,
2588
- });
2589
- const dispatchQuota = {
2590
- contract_version: "audit-code-dispatch-quota/v1alpha2",
2591
- run_id: runId,
2592
- model: hostModel,
2593
- resolved_limits: waveSchedule.resolved_limits,
2594
- confidence: waveSchedule.confidence,
2595
- source: waveSchedule.source,
2596
- host_concurrency_limit: waveSchedule.host_concurrency_limit,
2597
- wave_size: waveSchedule.wave_size,
2598
- estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
2599
- cooldown_until: waveSchedule.cooldown_until,
2600
- quota_source_snapshot: waveSchedule.quota_source_snapshot ?? null,
2601
- backoff_state: null,
2602
- };
2603
- const dispatchQuotaPath = join(runDir, "dispatch-quota.json");
2604
- await writeJsonFile(dispatchQuotaPath, dispatchQuota);
2605
- // Warn about packets that exceed the context budget only when we have reliable limit
2606
- // information (confidence medium/high). Low-confidence limits are conservative defaults
2607
- // and would produce misleading warnings since the real context window is unknown.
2608
- if (waveSchedule.confidence !== "low") {
2609
- const contextBudget = waveSchedule.resolved_limits.context_tokens - waveSchedule.resolved_limits.output_tokens;
2610
- for (const p of plan) {
2611
- if (p.complexity.estimated_tokens > contextBudget) {
2612
- warnings.push({
2613
- code: "oversized_packet",
2614
- message: `Packet ${p.packet_id} estimated tokens (${p.complexity.estimated_tokens}) exceed ` +
2615
- `context budget (${contextBudget}). This packet may fail at dispatch. ` +
2616
- `Set quota.default_context_tokens or quota.models in session-config.json to override.`,
2617
- });
2618
- }
2619
- }
2620
- }
2621
- const warningsPath = warnings.length > 0
2622
- ? join(runDir, "dispatch-warnings.json")
2623
- : null;
2624
- if (warningsPath) {
2625
- await writeJsonFile(warningsPath, warnings);
2626
- }
2627
- const activeDispatch = {
2628
- run_id: runId,
2629
- created_at: new Date().toISOString(),
2630
- packet_count: plan.length,
2631
- task_count: orderedTasks.length,
2632
- status: "active",
2633
- };
2634
- await writeJsonFile(join(artifactsDir, ACTIVE_DISPATCH_FILENAME), activeDispatch);
2635
- return {
2636
- run_id: runId,
2637
- dispatch_plan_path: dispatchPlanPath,
2638
- dispatch_quota_path: dispatchQuotaPath,
2639
- packet_count: plan.length,
2640
- task_count: orderedTasks.length,
2641
- skipped_task_count: priorResultTaskIds.size,
2642
- largest_packet: largestPacketId
2643
- ? {
2644
- packet_id: largestPacketId,
2645
- total_lines: largestLines,
2646
- estimated_tokens: largestEstimatedTokens,
2647
- }
2648
- : null,
2649
- warning_count: warnings.length,
2650
- dispatch_warnings_path: warningsPath,
2651
- };
2652
- }
2653
2114
  async function cmdPrepareDispatch(argv) {
2654
2115
  const runId = getFlag(argv, "--run-id");
2655
2116
  if (!runId)
2656
2117
  throw new Error("prepare-dispatch requires --run-id <run_id>");
2657
2118
  const result = await prepareDispatchArtifacts({
2119
+ packageRoot,
2658
2120
  runId,
2659
2121
  artifactsDir: getArtifactsDir(argv),
2660
2122
  root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
@@ -3104,7 +2566,11 @@ async function cmdIntake(argv) {
3104
2566
  }
3105
2567
  async function cmdPlan(argv) {
3106
2568
  const artifactsDir = getArtifactsDir(argv);
3107
- const result = await runAuditStep({ root: getRootDir(argv), artifactsDir });
2569
+ const result = await runAuditStep({
2570
+ root: getRootDir(argv),
2571
+ artifactsDir,
2572
+ since: getFlag(argv, "--since"),
2573
+ });
3108
2574
  console.log(JSON.stringify({
3109
2575
  artifacts_dir: artifactsDir,
3110
2576
  selected_executor: result.selected_executor,
@@ -3551,6 +3017,7 @@ async function cmdDispatchStatus(argv) {
3551
3017
  }, null, 2));
3552
3018
  }
3553
3019
  async function main(argv) {
3020
+ setQuotaStateDir(join(homedir(), ".audit-code"));
3554
3021
  const command = argv[2] ?? "sample-run";
3555
3022
  switch (command) {
3556
3023
  case "sample-run":