auditor-lambda 0.3.41 → 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 (74) hide show
  1. package/dist/cli/dispatch.js +5 -1
  2. package/dist/cli/prompts.d.ts +19 -0
  3. package/dist/cli/prompts.js +95 -0
  4. package/dist/cli/steps.d.ts +1 -1
  5. package/dist/cli.js +287 -7
  6. package/dist/extractors/analyzers/css.d.ts +2 -0
  7. package/dist/extractors/analyzers/css.js +101 -0
  8. package/dist/extractors/analyzers/html.d.ts +2 -0
  9. package/dist/extractors/analyzers/html.js +92 -0
  10. package/dist/extractors/analyzers/merge.d.ts +14 -0
  11. package/dist/extractors/analyzers/merge.js +85 -0
  12. package/dist/extractors/analyzers/python.d.ts +2 -0
  13. package/dist/extractors/analyzers/python.js +104 -0
  14. package/dist/extractors/analyzers/registry.d.ts +33 -0
  15. package/dist/extractors/analyzers/registry.js +100 -0
  16. package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
  17. package/dist/extractors/analyzers/resourceUrl.js +25 -0
  18. package/dist/extractors/analyzers/sql.d.ts +2 -0
  19. package/dist/extractors/analyzers/sql.js +19 -0
  20. package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
  21. package/dist/extractors/analyzers/treeSitter.js +111 -0
  22. package/dist/extractors/analyzers/types.d.ts +53 -0
  23. package/dist/extractors/analyzers/types.js +1 -0
  24. package/dist/extractors/analyzers/typescript.d.ts +2 -0
  25. package/dist/extractors/analyzers/typescript.js +257 -0
  26. package/dist/extractors/disposition.js +8 -1
  27. package/dist/extractors/graph.d.ts +1 -0
  28. package/dist/extractors/graph.js +167 -1
  29. package/dist/extractors/graphPythonImports.d.ts +15 -0
  30. package/dist/extractors/graphPythonImports.js +36 -0
  31. package/dist/extractors/pathPatterns.d.ts +6 -0
  32. package/dist/extractors/pathPatterns.js +8 -0
  33. package/dist/io/artifacts.d.ts +12 -1
  34. package/dist/io/artifacts.js +12 -0
  35. package/dist/orchestrator/advance.d.ts +20 -0
  36. package/dist/orchestrator/advance.js +61 -2
  37. package/dist/orchestrator/dependencyMap.js +27 -0
  38. package/dist/orchestrator/edgeReasoning.d.ts +39 -0
  39. package/dist/orchestrator/edgeReasoning.js +125 -0
  40. package/dist/orchestrator/executors.js +11 -1
  41. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
  42. package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
  43. package/dist/orchestrator/internalExecutors.d.ts +10 -1
  44. package/dist/orchestrator/internalExecutors.js +89 -11
  45. package/dist/orchestrator/localCommands.js +6 -25
  46. package/dist/orchestrator/nextStep.js +2 -0
  47. package/dist/orchestrator/reviewPackets.d.ts +37 -4
  48. package/dist/orchestrator/reviewPackets.js +93 -46
  49. package/dist/orchestrator/runtimeValidation.js +4 -31
  50. package/dist/orchestrator/scope.d.ts +62 -0
  51. package/dist/orchestrator/scope.js +227 -0
  52. package/dist/orchestrator/state.js +2 -0
  53. package/dist/reporting/synthesis.d.ts +37 -2
  54. package/dist/reporting/synthesis.js +95 -16
  55. package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
  56. package/dist/reporting/synthesisNarrativePrompt.js +60 -0
  57. package/dist/reporting/workBlocks.d.ts +2 -10
  58. package/dist/supervisor/sessionConfig.d.ts +8 -1
  59. package/dist/supervisor/sessionConfig.js +22 -1
  60. package/dist/types/analyzerCapability.d.ts +16 -0
  61. package/dist/types/analyzerCapability.js +1 -0
  62. package/dist/types/auditScope.d.ts +43 -0
  63. package/dist/types/auditScope.js +14 -0
  64. package/dist/types/synthesisNarrative.d.ts +7 -0
  65. package/dist/types/synthesisNarrative.js +5 -0
  66. package/dist/types.d.ts +2 -19
  67. package/dist/validation/artifacts.js +9 -0
  68. package/dist/validation/sessionConfig.js +24 -1
  69. package/package.json +4 -2
  70. package/schemas/analyzer_capability.schema.json +47 -0
  71. package/schemas/audit_findings.schema.json +141 -0
  72. package/schemas/finding.schema.json +2 -1
  73. package/schemas/graph_bundle.schema.json +5 -0
  74. package/schemas/scope.schema.json +46 -0
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { isFileMissingError, readJsonFile, writeJsonFile } from "@audit-tools/shared";
5
5
  import { loadArtifactBundle } from "../io/artifacts.js";
6
- import { orderTasksForPacketReview, buildReviewPackets } from "../orchestrator/reviewPackets.js";
6
+ import { orderTasksForPacketReview, buildReviewPackets, sizeIndexFromManifest, } from "../orchestrator/reviewPackets.js";
7
7
  import { buildFileAnchorSummary } from "../orchestrator/fileAnchors.js";
8
8
  import { resolveFreshSessionProviderName } from "../providers/index.js";
9
9
  import { loadSessionConfig } from "../supervisor/sessionConfig.js";
@@ -166,6 +166,7 @@ export function buildPendingAuditTasks(bundle) {
166
166
  return orderTasksForPacketReview(pendingTasks, {
167
167
  graphBundle: bundle.graph_bundle,
168
168
  lineIndex,
169
+ sizeIndex: sizeIndexFromManifest(bundle.repo_manifest),
169
170
  });
170
171
  }
171
172
  export async function prepareDispatchArtifacts(params) {
@@ -205,13 +206,16 @@ export async function prepareDispatchArtifacts(params) {
205
206
  ? tasks.filter((task) => !priorResultTaskIds.has(task.task_id))
206
207
  : tasks;
207
208
  const lineIndex = Object.fromEntries(dispatchTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
209
+ const sizeIndex = sizeIndexFromManifest(bundle.repo_manifest);
208
210
  const orderedTasks = orderTasksForPacketReview(dispatchTasks, {
209
211
  graphBundle: bundle.graph_bundle,
210
212
  lineIndex,
213
+ sizeIndex,
211
214
  });
212
215
  const packets = buildReviewPackets(orderedTasks, {
213
216
  graphBundle: bundle.graph_bundle,
214
217
  lineIndex,
218
+ sizeIndex,
215
219
  });
216
220
  const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
217
221
  const resultPathByTaskId = new Map(orderedTasks.map((task) => [
@@ -1,4 +1,5 @@
1
1
  import type { ActiveReviewRun } from "../supervisor/operatorHandoff.js";
2
+ import type { AnalyzerPlanEntry } from "../extractors/analyzers/types.js";
2
3
  export declare function nextStepCommand(root: string, artifactsDir: string): string;
3
4
  export declare function mergeAndIngestCommand(artifactsDir: string, runId: string): string;
4
5
  export declare function renderDispatchReviewPrompt(params: {
@@ -14,5 +15,23 @@ export declare function renderSingleTaskFallbackStepPrompt(params: {
14
15
  singleTaskPromptPath: string;
15
16
  activeReviewRun: ActiveReviewRun;
16
17
  }): string;
18
+ export declare function renderEdgeReasoningStepPrompt(params: {
19
+ basePrompt: string;
20
+ resultsPath: string;
21
+ continueCommand: string;
22
+ contentHash: string;
23
+ }): string;
24
+ export declare function renderEdgeReasoningDispatchPrompt(params: {
25
+ promptPath: string;
26
+ resultsPath: string;
27
+ continueCommand: string;
28
+ contentHash: string;
29
+ candidateCount: number;
30
+ }): string;
17
31
  export declare function renderPresentReportPrompt(finalReportPath: string): string;
32
+ export declare function renderAnalyzerInstallPrompt(params: {
33
+ unresolved: AnalyzerPlanEntry[];
34
+ decisionsPath: string;
35
+ continueCommand: string;
36
+ }): string;
18
37
  export declare function renderBlockedStepPrompt(reason: string): string;
@@ -102,6 +102,62 @@ export function renderSingleTaskFallbackStepPrompt(params) {
102
102
  "",
103
103
  ].join("\n");
104
104
  }
105
+ export function renderEdgeReasoningStepPrompt(params) {
106
+ return [
107
+ params.basePrompt,
108
+ "",
109
+ "## Results path",
110
+ "",
111
+ 'Write the JSON object ({"rewrites":[{"from":"...","to":"...","kind":"...","reason":"..."}]}) to:',
112
+ "",
113
+ ` ${params.resultsPath}`,
114
+ "",
115
+ `Cache key (edge-set content hash): ${params.contentHash}.`,
116
+ "If you already produced rewrites for this exact key, you may reuse them instead of regenerating.",
117
+ "",
118
+ `Then run: ${params.continueCommand}`,
119
+ "",
120
+ "Read and follow only the new step prompt returned by that command.",
121
+ "",
122
+ ].join("\n");
123
+ }
124
+ export function renderEdgeReasoningDispatchPrompt(params) {
125
+ return [
126
+ "# audit-code edge reasoning (subagent dispatch)",
127
+ "",
128
+ `The dependency graph has ${params.candidateCount} low-confidence edge(s) whose`,
129
+ "machine-generated `reason` text can be clarified. This is a single, bounded,",
130
+ "optional pass: it only rewrites the `reason` string of those edges — it never",
131
+ "adds, removes, re-targets, or re-weights an edge.",
132
+ "",
133
+ "Dispatch exactly ONE subagent (via the `task` tool or equivalent). Hand it this",
134
+ "prompt file path; do not load the file into this orchestrator context:",
135
+ "",
136
+ ` ${params.promptPath}`,
137
+ "",
138
+ "Subagent prompt shape:",
139
+ "",
140
+ " Read and follow the edge-reasoning instructions in: <prompt path above>",
141
+ "",
142
+ 'The subagent must write its JSON result ({"rewrites":[...]}) to:',
143
+ "",
144
+ ` ${params.resultsPath}`,
145
+ "",
146
+ `Cache key (edge-set content hash): ${params.contentHash}.`,
147
+ "If you hold a cached result for this exact key from a previous run, you may write",
148
+ "it to the results path directly instead of dispatching a subagent.",
149
+ "",
150
+ "**File access pre-approval:** if your host supports per-subagent file access",
151
+ `restrictions, allow the subagent to read ${params.promptPath} and write ${params.resultsPath}.`,
152
+ "",
153
+ "After the subagent writes the result, run exactly:",
154
+ "",
155
+ ` ${params.continueCommand}`,
156
+ "",
157
+ "Read and follow only the new step prompt returned by that command.",
158
+ "",
159
+ ].join("\n");
160
+ }
105
161
  export function renderPresentReportPrompt(finalReportPath) {
106
162
  return [
107
163
  "# audit-code present report",
@@ -116,6 +172,45 @@ export function renderPresentReportPrompt(finalReportPath) {
116
172
  "",
117
173
  ].join("\n");
118
174
  }
175
+ export function renderAnalyzerInstallPrompt(params) {
176
+ const analyzerLines = params.unresolved.flatMap((entry) => [
177
+ `- **${entry.id}** — needs \`${entry.dependency ?? entry.id}\`; ${entry.supportedCount} in-scope file(s) would be analyzed.`,
178
+ ]);
179
+ const exampleObject = `{ ${params.unresolved
180
+ .map((entry) => `"${entry.id}": "ephemeral"`)
181
+ .join(", ")} }`;
182
+ return [
183
+ "# audit-code analyzer install",
184
+ "",
185
+ "The deterministic regex graph is built. These optional language analyzers can",
186
+ "produce a richer graph (real module resolution, inheritance, and a call graph),",
187
+ "but their compiler dependency is not installed in the audited repo:",
188
+ "",
189
+ ...analyzerLines,
190
+ "",
191
+ "Choose how to resolve each one and write a JSON object of `{ \"<analyzer-id>\": <setting> }`",
192
+ "to the decisions path below. Valid settings:",
193
+ "",
194
+ "- `ephemeral` — install into a shared, version-keyed cache (never touches this project); compile once, reuse across audits.",
195
+ "- `permanent` — same as `ephemeral` but a durable opt-in recorded in session config.",
196
+ "- `skip` — do not run this analyzer; keep the regex floor.",
197
+ "",
198
+ "Default if you are unsure or cannot install: choose `skip`. The audit proceeds either way.",
199
+ "",
200
+ "## Decisions path",
201
+ "",
202
+ "Write your choices to:",
203
+ "",
204
+ ` ${params.decisionsPath}`,
205
+ "",
206
+ `Example: ${exampleObject}`,
207
+ "",
208
+ `Then run: ${params.continueCommand}`,
209
+ "",
210
+ "Read and follow only the new step prompt returned by that command.",
211
+ "",
212
+ ].join("\n");
213
+ }
119
214
  export function renderBlockedStepPrompt(reason) {
120
215
  return [
121
216
  "# audit-code blocked",
@@ -1,7 +1,7 @@
1
1
  import type { StepStatus } from "@audit-tools/shared";
2
2
  import type { AccessDeclaration } from "../types/workerSession.js";
3
3
  export declare const STEP_CONTRACT_VERSION = "audit-code-step/v1alpha1";
4
- export type StepKind = "dispatch_review" | "single_task_fallback" | "design_review" | "present_report" | "blocked";
4
+ export type StepKind = "dispatch_review" | "single_task_fallback" | "design_review" | "analyzer_install" | "edge_reasoning" | "edge_reasoning_dispatch" | "synthesis_narrative" | "present_report" | "blocked";
5
5
  export interface StepArtifact {
6
6
  contract_version: typeof STEP_CONTRACT_VERSION;
7
7
  step_kind: StepKind;
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, readdir, rename, rm, unlink } from "node:fs/promises";
1
+ import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -11,7 +11,7 @@ import { buildFlowCoverage } from "./orchestrator/flowCoverage.js";
11
11
  import { buildRuntimeValidationTasks, } from "./orchestrator/runtimeValidation.js";
12
12
  import { initializeCoverageFromPlan } from "./orchestrator/planning.js";
13
13
  import { loadArtifactBundle, writeCoreArtifacts, promoteFinalAuditReport, } from "./io/artifacts.js";
14
- import { isFileMissingError, readJsonFile, writeJsonFile, prefixValidationIssues } from "@audit-tools/shared";
14
+ import { isFileMissingError, readJsonFile, writeJsonFile, prefixValidationIssues, RunLogger } from "@audit-tools/shared";
15
15
  import { validateArtifactBundle } from "./validation/artifacts.js";
16
16
  import { validateAuditResults, formatAuditResultIssues, } from "./validation/auditResults.js";
17
17
  import { validateConfiguredProviderEnvironment, validateSessionConfig, } from "./validation/sessionConfig.js";
@@ -20,21 +20,26 @@ import { deriveAuditState } from "./orchestrator/state.js";
20
20
  import { advanceAudit } from "./orchestrator/advance.js";
21
21
  import { checkFileIntegrity } from "./orchestrator/fileIntegrity.js";
22
22
  import { decideNextStep } from "./orchestrator/nextStep.js";
23
+ import { collectLowConfidenceEdges, buildEdgeReasoningPrompt, edgeReasoningContentHash, } from "./orchestrator/edgeReasoning.js";
23
24
  import { renderDesignReviewPrompt } from "./orchestrator/designReviewPrompt.js";
25
+ import { renderSynthesisNarrativePrompt } from "./reporting/synthesisNarrativePrompt.js";
24
26
  import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
25
27
  import { appendRunLedgerEntry, loadRunLedger } from "./supervisor/runLedger.js";
26
28
  import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./supervisor/operatorHandoff.js";
27
- 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";
28
33
  import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
29
34
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
30
- import { estimateTaskGroupTokens, } from "./orchestrator/reviewPackets.js";
35
+ import { estimateTaskGroupTokens, sizeIndexFromManifest, } from "./orchestrator/reviewPackets.js";
31
36
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
32
37
  import { runAuditCodeMcpServer } from "./mcp/server.js";
33
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";
34
39
  // Re-exports from extracted modules
35
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";
36
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";
37
- import { nextStepCommand, mergeAndIngestCommand, renderDispatchReviewPrompt, renderSingleTaskFallbackStepPrompt, renderPresentReportPrompt, renderBlockedStepPrompt, } from "./cli/prompts.js";
42
+ import { nextStepCommand, mergeAndIngestCommand, renderDispatchReviewPrompt, renderSingleTaskFallbackStepPrompt, renderPresentReportPrompt, renderAnalyzerInstallPrompt, renderEdgeReasoningStepPrompt, renderEdgeReasoningDispatchPrompt, renderBlockedStepPrompt, } from "./cli/prompts.js";
38
43
  import { writeCurrentStep, } from "./cli/steps.js";
39
44
  import { WORKER_RESULT_CONTRACT_VERSION, buildWorkerResult, persistWorkerRunArtifacts, isWorkerResult, buildWorkerFailureBlocker, formatAuditResultValidationError, } from "./cli/workerResult.js";
40
45
  import { DISPATCH_RESULT_MAP_FILENAME, ACTIVE_DISPATCH_FILENAME, resolveRunScopedArg, loadDispatchResultMap, entriesByTaskId, buildPendingAuditTasks, prepareDispatchArtifacts, } from "./cli/dispatch.js";
@@ -313,9 +318,15 @@ async function maybeArchiveLegacyPendingResults(auditResultsPath) {
313
318
  }
314
319
  async function runAuditStep(options) {
315
320
  const bundle = await loadArtifactBundle(options.artifactsDir);
321
+ const runLogger = new RunLogger(join(options.artifactsDir, "run.log.jsonl"), {
322
+ enabled: options.runLog ?? true,
323
+ });
316
324
  const lineIndex = bundle.repo_manifest
317
325
  ? await buildLineIndex(options.root, bundle.repo_manifest)
318
326
  : undefined;
327
+ const sizeIndex = bundle.repo_manifest
328
+ ? sizeIndexFromManifest(bundle.repo_manifest)
329
+ : undefined;
319
330
  if (looksLikeCliFlag(options.auditResultsPath)) {
320
331
  throw new Error(`Invalid audit results path '${options.auditResultsPath}'. This looks like a CLI flag rather than a file path.`);
321
332
  }
@@ -343,14 +354,27 @@ async function runAuditStep(options) {
343
354
  const externalAnalyzerResults = options.externalAnalyzerPath
344
355
  ? await readJsonFile(options.externalAnalyzerPath)
345
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;
346
363
  const result = await advanceAudit(bundle, {
347
364
  root: options.root,
348
365
  lineIndex,
366
+ sizeIndex,
349
367
  auditResults: auditResults,
350
368
  runtimeValidationUpdates,
351
369
  externalAnalyzerResults,
370
+ narrativeResults,
371
+ edgeReasoningResults,
372
+ analyzers: options.analyzers,
373
+ graphLlmEdgeReasoning: options.graphLlmEdgeReasoning,
374
+ since: options.since,
352
375
  preferredExecutor: options.preferredExecutor,
353
376
  opentoken: options.opentoken,
377
+ runLogger,
354
378
  });
355
379
  await writeCoreArtifacts(options.artifactsDir, result.updated_bundle);
356
380
  const archivedPendingResults = await maybeArchiveLegacyPendingResults(options.auditResultsPath);
@@ -555,7 +579,11 @@ async function cmdAdvanceAudit(argv) {
555
579
  auditResultsPath: getFlag(argv, "--results"),
556
580
  runtimeUpdatesPath: getFlag(argv, "--updates"),
557
581
  externalAnalyzerPath,
582
+ analyzers: sessionConfig.analyzers,
583
+ graphLlmEdgeReasoning: sessionConfig.graph?.llm_edge_reasoning,
584
+ since: getFlag(argv, "--since"),
558
585
  opentoken: sessionConfig.opentoken?.enabled,
586
+ runLog: sessionConfig.observability?.run_log,
559
587
  });
560
588
  if (result.selected_executor !== "agent") {
561
589
  await clearDispatchFiles(artifactsDir);
@@ -579,6 +607,7 @@ async function cmdAdvanceAudit(argv) {
579
607
  }
580
608
  async function runDeterministicForNextStep(params) {
581
609
  let lastSummary = "";
610
+ let analyzers = params.analyzers;
582
611
  for (let index = 0; index < params.maxRuns; index++) {
583
612
  const bundle = await loadArtifactBundle(params.artifactsDir);
584
613
  const decision = decideNextStep(bundle);
@@ -621,6 +650,89 @@ async function runDeterministicForNextStep(params) {
621
650
  }
622
651
  }
623
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
+ }
624
736
  if (decision.selected_executor === "design_review") {
625
737
  const findingsPath = join(params.artifactsDir, "incoming", "design-review-findings.json");
626
738
  let reviewFindings;
@@ -647,6 +759,36 @@ async function runDeterministicForNextStep(params) {
647
759
  bundle,
648
760
  };
649
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
+ }
650
792
  if (decision.selected_executor === "agent") {
651
793
  return {
652
794
  kind: "semantic_review",
@@ -682,6 +824,9 @@ async function runDeterministicForNextStep(params) {
682
824
  result = await runAuditStep({
683
825
  root: params.root,
684
826
  artifactsDir: params.artifactsDir,
827
+ analyzers,
828
+ graphLlmEdgeReasoning: params.graphLlmEdgeReasoning,
829
+ since: params.since,
685
830
  opentoken: params.opentoken,
686
831
  });
687
832
  }
@@ -783,6 +928,10 @@ async function cmdNextStep(argv) {
783
928
  timeoutMs: getTimeoutMs(argv, sessionConfig),
784
929
  maxRuns: getMaxRuns(argv),
785
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"),
786
935
  });
787
936
  if (result.kind === "complete") {
788
937
  const step = await writeCurrentStep({
@@ -850,6 +999,130 @@ async function cmdNextStep(argv) {
850
999
  console.log(JSON.stringify(step, null, 2));
851
1000
  return;
852
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
+ }
853
1126
  if (!hostCanDispatch) {
854
1127
  const singleTaskPromptPath = join(artifactsDir, "dispatch", "current-single-task-prompt.md");
855
1128
  const workerCommand = renderCommand(result.activeReviewRun.worker_command);
@@ -1151,7 +1424,8 @@ async function cmdRunToCompletion(argv) {
1151
1424
  const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
1152
1425
  const allCandidateTasks = buildPendingAuditTasks(bundle);
1153
1426
  const candidateGroups = chunkArray(allCandidateTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
1154
- const slotTokenEstimates = candidateGroups.map((g) => estimateTaskGroupTokens(g));
1427
+ const candidateSizeIndex = sizeIndexFromManifest(bundle.repo_manifest);
1428
+ const slotTokenEstimates = candidateGroups.map((g) => estimateTaskGroupTokens(g, candidateSizeIndex));
1155
1429
  const providerLimits = await provider.queryLimits?.(hostModel)
1156
1430
  .then((r) => r ? { ...r, source: "provider_query" } : null)
1157
1431
  .catch(() => null)
@@ -1457,6 +1731,8 @@ async function cmdRunToCompletion(argv) {
1457
1731
  auditResultsPath,
1458
1732
  runtimeUpdatesPath,
1459
1733
  externalAnalyzerPath,
1734
+ analyzers: sessionConfig.analyzers,
1735
+ since: getFlag(argv, "--since"),
1460
1736
  });
1461
1737
  workerResult = {
1462
1738
  contract_version: WORKER_RESULT_CONTRACT_VERSION,
@@ -2290,7 +2566,11 @@ async function cmdIntake(argv) {
2290
2566
  }
2291
2567
  async function cmdPlan(argv) {
2292
2568
  const artifactsDir = getArtifactsDir(argv);
2293
- 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
+ });
2294
2574
  console.log(JSON.stringify({
2295
2575
  artifacts_dir: artifactsDir,
2296
2576
  selected_executor: result.selected_executor,
@@ -0,0 +1,2 @@
1
+ import type { LanguageAnalyzer } from "./types.js";
2
+ export declare const cssAnalyzer: LanguageAnalyzer;