audit-tools 0.42.1 → 0.44.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 (237) hide show
  1. package/dispatch/merge-results.mjs +229 -140
  2. package/dispatch/validate-result.mjs +98 -40
  3. package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
  4. package/dist/audit/cli/conceptualDispatch.js +35 -3
  5. package/dist/audit/cli/conceptualDispatch.js.map +1 -1
  6. package/dist/audit/cli/dispatch/hostHandoff.d.ts +9 -1
  7. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  8. package/dist/audit/cli/dispatch/hostHandoff.js +94 -38
  9. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  10. package/dist/audit/cli/laneSubmissions.d.ts +2 -2
  11. package/dist/audit/cli/laneSubmissions.js +2 -2
  12. package/dist/audit/cli/nextStepCommand.d.ts +64 -0
  13. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  14. package/dist/audit/cli/nextStepCommand.js +877 -783
  15. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  16. package/dist/audit/cli/nextStepHelpers.d.ts +12 -36
  17. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  18. package/dist/audit/cli/nextStepHelpers.js +34 -77
  19. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  20. package/dist/audit/cli/steps.d.ts +2 -2
  21. package/dist/audit/extractors/graph.d.ts +28 -2
  22. package/dist/audit/extractors/graph.d.ts.map +1 -1
  23. package/dist/audit/extractors/graph.js +122 -11
  24. package/dist/audit/extractors/graph.js.map +1 -1
  25. package/dist/audit/extractors/graphRoutes.d.ts +8 -0
  26. package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
  27. package/dist/audit/extractors/graphRoutes.js +71 -4
  28. package/dist/audit/extractors/graphRoutes.js.map +1 -1
  29. package/dist/audit/io/artifacts.d.ts +55 -0
  30. package/dist/audit/io/artifacts.d.ts.map +1 -1
  31. package/dist/audit/io/artifacts.js +189 -12
  32. package/dist/audit/io/artifacts.js.map +1 -1
  33. package/dist/audit/io/toolingManifest.d.ts +11 -0
  34. package/dist/audit/io/toolingManifest.d.ts.map +1 -1
  35. package/dist/audit/io/toolingManifest.js +13 -3
  36. package/dist/audit/io/toolingManifest.js.map +1 -1
  37. package/dist/audit/orchestrator/advance.d.ts +64 -0
  38. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  39. package/dist/audit/orchestrator/advance.js +191 -57
  40. package/dist/audit/orchestrator/advance.js.map +1 -1
  41. package/dist/audit/orchestrator/charterClarificationExecutor.d.ts.map +1 -1
  42. package/dist/audit/orchestrator/charterClarificationExecutor.js +55 -21
  43. package/dist/audit/orchestrator/charterClarificationExecutor.js.map +1 -1
  44. package/dist/audit/orchestrator/flowCoverage.d.ts.map +1 -1
  45. package/dist/audit/orchestrator/flowCoverage.js +7 -10
  46. package/dist/audit/orchestrator/flowCoverage.js.map +1 -1
  47. package/dist/audit/orchestrator/flowPlanning.d.ts +54 -1
  48. package/dist/audit/orchestrator/flowPlanning.d.ts.map +1 -1
  49. package/dist/audit/orchestrator/flowPlanning.js +77 -18
  50. package/dist/audit/orchestrator/flowPlanning.js.map +1 -1
  51. package/dist/audit/orchestrator/flowRequeue.d.ts.map +1 -1
  52. package/dist/audit/orchestrator/flowRequeue.js +6 -10
  53. package/dist/audit/orchestrator/flowRequeue.js.map +1 -1
  54. package/dist/audit/orchestrator/requeueFold.d.ts.map +1 -1
  55. package/dist/audit/orchestrator/requeueFold.js +18 -5
  56. package/dist/audit/orchestrator/requeueFold.js.map +1 -1
  57. package/dist/audit/orchestrator/staleness.d.ts +24 -1
  58. package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
  59. package/dist/audit/orchestrator/staleness.js +147 -16
  60. package/dist/audit/orchestrator/staleness.js.map +1 -1
  61. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +26 -0
  62. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
  63. package/dist/audit/orchestrator/syntaxResolutionExecutor.js +97 -12
  64. package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
  65. package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
  66. package/dist/audit/orchestrator/taskBuilder.js +111 -21
  67. package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
  68. package/dist/audit/orchestrator/trivialAudit.d.ts +17 -1
  69. package/dist/audit/orchestrator/trivialAudit.d.ts.map +1 -1
  70. package/dist/audit/orchestrator/trivialAudit.js +31 -4
  71. package/dist/audit/orchestrator/trivialAudit.js.map +1 -1
  72. package/dist/audit/types/analyzerCapability.d.ts +6 -6
  73. package/dist/audit/types/charterRegister.d.ts +30 -12
  74. package/dist/audit/types/charterRegister.d.ts.map +1 -1
  75. package/dist/audit/types/charterRegister.js +22 -9
  76. package/dist/audit/types/charterRegister.js.map +1 -1
  77. package/dist/audit/validation/anchorGrounding.d.ts.map +1 -1
  78. package/dist/audit/validation/anchorGrounding.js +17 -0
  79. package/dist/audit/validation/anchorGrounding.js.map +1 -1
  80. package/dist/audit/validation/auditResults.d.ts +17 -0
  81. package/dist/audit/validation/auditResults.d.ts.map +1 -1
  82. package/dist/audit/validation/auditResults.js +50 -17
  83. package/dist/audit/validation/auditResults.js.map +1 -1
  84. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +48 -0
  85. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +1 -1
  86. package/dist/remediate/contractPipeline/cyclicSeamResolution.js +128 -0
  87. package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +1 -1
  88. package/dist/remediate/contractPipeline/derive.d.ts +8 -5
  89. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  90. package/dist/remediate/contractPipeline/derive.js +8 -16
  91. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  92. package/dist/remediate/index.d.ts.map +1 -1
  93. package/dist/remediate/index.js +43 -2
  94. package/dist/remediate/index.js.map +1 -1
  95. package/dist/remediate/intent/intentOrdering.d.ts +13 -5
  96. package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
  97. package/dist/remediate/intent/intentOrdering.js +56 -25
  98. package/dist/remediate/intent/intentOrdering.js.map +1 -1
  99. package/dist/remediate/phases/close.d.ts +57 -2
  100. package/dist/remediate/phases/close.d.ts.map +1 -1
  101. package/dist/remediate/phases/close.js +260 -36
  102. package/dist/remediate/phases/close.js.map +1 -1
  103. package/dist/remediate/phases/triage.d.ts.map +1 -1
  104. package/dist/remediate/phases/triage.js +14 -2
  105. package/dist/remediate/phases/triage.js.map +1 -1
  106. package/dist/remediate/state/disposition.d.ts +26 -4
  107. package/dist/remediate/state/disposition.d.ts.map +1 -1
  108. package/dist/remediate/state/itemStatus.d.ts +33 -6
  109. package/dist/remediate/state/itemStatus.d.ts.map +1 -1
  110. package/dist/remediate/state/itemStatus.js +154 -39
  111. package/dist/remediate/state/itemStatus.js.map +1 -1
  112. package/dist/remediate/state/types.d.ts +41 -2
  113. package/dist/remediate/state/types.d.ts.map +1 -1
  114. package/dist/remediate/state/types.js.map +1 -1
  115. package/dist/remediate/steps/contractPipeline.d.ts +296 -15
  116. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  117. package/dist/remediate/steps/contractPipeline.js +1783 -1001
  118. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  119. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +137 -2
  120. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  121. package/dist/remediate/steps/dispatch/hostHandoff.js +657 -59
  122. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  123. package/dist/remediate/steps/finalGate.d.ts +136 -37
  124. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  125. package/dist/remediate/steps/finalGate.js +120 -86
  126. package/dist/remediate/steps/finalGate.js.map +1 -1
  127. package/dist/remediate/steps/gateCommands.d.ts +8 -0
  128. package/dist/remediate/steps/gateCommands.d.ts.map +1 -1
  129. package/dist/remediate/steps/gateCommands.js +40 -11
  130. package/dist/remediate/steps/gateCommands.js.map +1 -1
  131. package/dist/remediate/steps/nextStep.d.ts +96 -5
  132. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  133. package/dist/remediate/steps/nextStep.js +668 -147
  134. package/dist/remediate/steps/nextStep.js.map +1 -1
  135. package/dist/remediate/steps/types.d.ts +4 -3
  136. package/dist/remediate/steps/types.d.ts.map +1 -1
  137. package/dist/remediate/steps/types.js +10 -2
  138. package/dist/remediate/steps/types.js.map +1 -1
  139. package/dist/remediate/validation/artifacts.d.ts +20 -2
  140. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  141. package/dist/remediate/validation/artifacts.js +224 -130
  142. package/dist/remediate/validation/artifacts.js.map +1 -1
  143. package/dist/remediate/validation/contractPipelineGates.d.ts +58 -0
  144. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  145. package/dist/remediate/validation/contractPipelineGates.js +143 -27
  146. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  147. package/dist/shared/analyzers/acquisitionEngine.d.ts +72 -25
  148. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  149. package/dist/shared/analyzers/acquisitionEngine.js +149 -15
  150. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  151. package/dist/shared/analyzers/binaryAcquisition.d.ts +27 -2
  152. package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
  153. package/dist/shared/analyzers/binaryAcquisition.js +149 -43
  154. package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
  155. package/dist/shared/analyzers/candidates.d.ts +9 -8
  156. package/dist/shared/analyzers/candidates.d.ts.map +1 -1
  157. package/dist/shared/analyzers/candidates.js +96 -64
  158. package/dist/shared/analyzers/candidates.js.map +1 -1
  159. package/dist/shared/analyzers/normalizeExternal.d.ts +40 -12
  160. package/dist/shared/analyzers/normalizeExternal.d.ts.map +1 -1
  161. package/dist/shared/analyzers/normalizeExternal.js +74 -18
  162. package/dist/shared/analyzers/normalizeExternal.js.map +1 -1
  163. package/dist/shared/analyzers/rubocop.d.ts +14 -13
  164. package/dist/shared/analyzers/rubocop.d.ts.map +1 -1
  165. package/dist/shared/analyzers/rubocop.js +29 -6
  166. package/dist/shared/analyzers/rubocop.js.map +1 -1
  167. package/dist/shared/analyzers/types.d.ts +196 -13
  168. package/dist/shared/analyzers/types.d.ts.map +1 -1
  169. package/dist/shared/analyzers/types.js +122 -9
  170. package/dist/shared/analyzers/types.js.map +1 -1
  171. package/dist/shared/decompose/charterClarification.d.ts +12 -1
  172. package/dist/shared/decompose/charterClarification.d.ts.map +1 -1
  173. package/dist/shared/decompose/charterClarification.js +11 -4
  174. package/dist/shared/decompose/charterClarification.js.map +1 -1
  175. package/dist/shared/decompose/charterExtraction.d.ts +20 -10
  176. package/dist/shared/decompose/charterExtraction.d.ts.map +1 -1
  177. package/dist/shared/decompose/charterExtraction.js +52 -23
  178. package/dist/shared/decompose/charterExtraction.js.map +1 -1
  179. package/dist/shared/index.d.ts +1 -0
  180. package/dist/shared/index.d.ts.map +1 -1
  181. package/dist/shared/index.js +5 -0
  182. package/dist/shared/index.js.map +1 -1
  183. package/dist/shared/intent/freeFormIntentInterpreter.d.ts +12 -2
  184. package/dist/shared/intent/freeFormIntentInterpreter.d.ts.map +1 -1
  185. package/dist/shared/intent/freeFormIntentInterpreter.js +27 -22
  186. package/dist/shared/intent/freeFormIntentInterpreter.js.map +1 -1
  187. package/dist/shared/intent/pathScope.d.ts +12 -1
  188. package/dist/shared/intent/pathScope.d.ts.map +1 -1
  189. package/dist/shared/intent/pathScope.js +56 -19
  190. package/dist/shared/intent/pathScope.js.map +1 -1
  191. package/dist/shared/intent/sharedIntentData.d.ts +19 -1
  192. package/dist/shared/intent/sharedIntentData.d.ts.map +1 -1
  193. package/dist/shared/intent/sharedIntentData.js +33 -8
  194. package/dist/shared/intent/sharedIntentData.js.map +1 -1
  195. package/dist/shared/io/fileLock.d.ts +12 -0
  196. package/dist/shared/io/fileLock.d.ts.map +1 -1
  197. package/dist/shared/io/fileLock.js +48 -5
  198. package/dist/shared/io/fileLock.js.map +1 -1
  199. package/dist/shared/io/json.d.ts +13 -0
  200. package/dist/shared/io/json.d.ts.map +1 -1
  201. package/dist/shared/io/json.js +19 -1
  202. package/dist/shared/io/json.js.map +1 -1
  203. package/dist/shared/steps/stepEmissionScaffold.d.ts +69 -0
  204. package/dist/shared/steps/stepEmissionScaffold.d.ts.map +1 -0
  205. package/dist/shared/steps/stepEmissionScaffold.js +83 -0
  206. package/dist/shared/steps/stepEmissionScaffold.js.map +1 -0
  207. package/dist/shared/submission/handRecovery.d.ts.map +1 -1
  208. package/dist/shared/submission/handRecovery.js +69 -4
  209. package/dist/shared/submission/handRecovery.js.map +1 -1
  210. package/dist/shared/submission/submissionIdentity.d.ts +10 -1
  211. package/dist/shared/submission/submissionIdentity.d.ts.map +1 -1
  212. package/dist/shared/submission/submissionIdentity.js +12 -3
  213. package/dist/shared/submission/submissionIdentity.js.map +1 -1
  214. package/dist/shared/submission/submissionLedger.d.ts +43 -2
  215. package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
  216. package/dist/shared/submission/submissionLedger.js +50 -7
  217. package/dist/shared/submission/submissionLedger.js.map +1 -1
  218. package/dist/shared/tooling/allowlistedExec.d.ts +9 -0
  219. package/dist/shared/tooling/allowlistedExec.d.ts.map +1 -1
  220. package/dist/shared/tooling/allowlistedExec.js +19 -2
  221. package/dist/shared/tooling/allowlistedExec.js.map +1 -1
  222. package/dist/shared/tooling/commandShape.d.ts +21 -0
  223. package/dist/shared/tooling/commandShape.d.ts.map +1 -0
  224. package/dist/shared/tooling/commandShape.js +120 -0
  225. package/dist/shared/tooling/commandShape.js.map +1 -0
  226. package/dist/shared/types/charter.d.ts +24 -0
  227. package/dist/shared/types/charter.d.ts.map +1 -1
  228. package/dist/shared/types/charter.js +9 -0
  229. package/dist/shared/types/charter.js.map +1 -1
  230. package/dist/shared/types/remediationOutcome.d.ts +282 -9
  231. package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
  232. package/dist/shared/types/remediationOutcome.js +174 -1
  233. package/dist/shared/types/remediationOutcome.js.map +1 -1
  234. package/package.json +2 -1
  235. package/remediate-code.mjs +15 -0
  236. package/wrapper/remediate-code-wrapper-install-hosts.mjs +45 -4
  237. package/wrapper/remediate-code-wrapper-install-renderers.mjs +12 -2
@@ -1,6 +1,9 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { loadAnalyzerPolicy, loadSessionIntent, applyGuidanceFile, runWithBlockedStepBackstop, writeBlockedStepContract, laneAssetsDir, renderFanoutExecutionLines, writeTextFile, } from "audit-tools/shared";
4
+ // The ONE shared table-driven step-emission scaffold. Imported by relative path
5
+ // like every other cross-area shared module this area reaches for.
6
+ import { createStepEmissionScaffold } from "../../shared/steps/stepEmissionScaffold.js";
4
7
  import { cleanupStaleArtifactsDir } from "./cleanup.js";
5
8
  import { materializeFanoutLanes } from "./fanoutLanes.js";
6
9
  import { AUDIT_GATE_SUBMISSION_SCOPE, GATE_LANES, charterExtractionLane, charterExtractionPacketFilename, laneSubmissionPath, mergeLaneShortfalls, recordExpectedLanes, renderLaneShortfallLines, } from "./laneSubmissions.js";
@@ -182,21 +185,16 @@ async function cmdNextStepBody(argv, root, artifactsDir) {
182
185
  artifactsDir,
183
186
  progressSummary: reason,
184
187
  });
188
+ // The ONE emission that is not a table row, and deliberately so: it runs
189
+ // BEFORE dispatch, so no result kind exists to dispatch on. An unreadable
190
+ // or invalid analyzer policy blocks the step rather than degrading to an
191
+ // empty policy (which would silently drop every recorded consent decision)
192
+ // — and it still goes through the scaffold's single emission site, so the
193
+ // "write the step, log it, exactly once" shape is not re-implemented here.
185
194
  // Shared blocked-step assembly: the step JSON says WHY on its own via
186
195
  // progress.summary — an automated consumer (release smoke, CI) sees only this
187
196
  // contract, not the prompt file.
188
- const step = await writeBlockedStepContract({
189
- tool: "audit-code",
190
- contractVersion: STEP_CONTRACT_VERSION,
191
- artifactsDir,
192
- repoRoot: root,
193
- runId: null,
194
- reason,
195
- artifactPaths: {
196
- operator_handoff: join(artifactsDir, "operator-handoff.json"),
197
- },
198
- });
199
- console.log(JSON.stringify(step, null, 2));
197
+ await NEXT_STEP_EMISSION.emitPlan(blockedStepPlan(operatorHandoffBlock(root, artifactsDir, reason)));
200
198
  return;
201
199
  }
202
200
  const result = await runDeterministicForNextStep({
@@ -210,807 +208,903 @@ async function cmdNextStepBody(argv, root, artifactsDir) {
210
208
  // session config can opt out). The executor builds its own global-`fetch`
211
209
  // adapter when no fetcher is injected. The unit/integration suite never reaches
212
210
  // here, so acquisition stays a hermetic no-op in tests.
213
- externalAcquisition: {
214
- enabled: true,
215
- analyzers: analyzerPolicy.analyzers,
216
- // Item B: recorded consent decisions ride into admission (granted admits
217
- // without a token) and into the consent fold's pending computation.
218
- analyzerConsent: analyzerPolicy.analyzer_consent,
219
- },
211
+ externalAcquisition: buildExternalAcquisitionOptions(analyzerPolicy),
220
212
  since: getFlag(argv, "--since"),
221
213
  });
222
- if (result.kind === "complete") {
223
- const triage = result.triage;
224
- const frictionPending = triage?.action === "dispose";
225
- const step = await writeCurrentStep({
226
- artifactsDir,
227
- stepKind: "present_report",
228
- status: frictionPending ? "ready" : "complete",
229
- runId: null,
230
- // INV-READY-STEP-CONTINUATION (COR-f6a36670): a ready step whose
231
- // stop_condition instructs calling next-step again must carry the
232
- // executable continuation command — never leave the host to reconstruct
233
- // the invocation from prose.
234
- allowedCommands: frictionPending
235
- ? [nextStepCommand(root, artifactsDir)]
236
- : [],
237
- stopCondition: frictionPending
238
- ? "Complete friction triage (write open_observations and any dispositions), then call next-step again."
239
- : "Present the final audit report and stop.",
240
- repoRoot: root,
241
- artifactPaths: {
242
- final_report: result.finalReportPath,
243
- ...(triage ? { friction_record: triage.recordPath } : {}),
244
- },
245
- prompt: renderPresentReportPrompt(result.finalReportPath, triage),
246
- });
247
- console.log(JSON.stringify(step, null, 2));
248
- return;
214
+ // The single dispatch: one table row per result kind, one emission site. A
215
+ // kind the table does not carry is the semantic-review dispatch (the
216
+ // scaffold's fallback), exactly as the old fallthrough branch was.
217
+ await NEXT_STEP_EMISSION.emit(result.kind, {
218
+ argv,
219
+ root,
220
+ artifactsDir,
221
+ analyzerPolicy,
222
+ result,
223
+ });
224
+ }
225
+ function currentStepPlan(params) {
226
+ return { via: "current", params };
227
+ }
228
+ function blockedStepPlan(params) {
229
+ return { via: "blocked", params };
230
+ }
231
+ function semanticReviewPlan(params) {
232
+ return { via: "semantic_review", params };
233
+ }
234
+ /**
235
+ * The blocked-step shape both blocked paths share — the pre-dispatch config
236
+ * failure and the `blocked` result kind. Single-sourced so the two cannot drift
237
+ * into two different operator-handoff contracts.
238
+ */
239
+ function operatorHandoffBlock(root, artifactsDir, reason) {
240
+ return {
241
+ tool: "audit-code",
242
+ contractVersion: STEP_CONTRACT_VERSION,
243
+ artifactsDir,
244
+ repoRoot: root,
245
+ runId: null,
246
+ reason,
247
+ artifactPaths: {
248
+ operator_handoff: join(artifactsDir, "operator-handoff.json"),
249
+ },
250
+ };
251
+ }
252
+ /** The ONE writer dispatch. Each underlying writer is called exactly once here. */
253
+ async function writeAuditStep(plan) {
254
+ switch (plan.via) {
255
+ case "current":
256
+ return await writeCurrentStep(plan.params);
257
+ case "blocked":
258
+ return await writeBlockedStepContract(plan.params);
259
+ case "semantic_review":
260
+ return await renderSemanticReviewStep(plan.params);
249
261
  }
250
- if (result.kind === "blocked") {
251
- // Same diagnosability contract as the config-blocked step above.
252
- const step = await writeBlockedStepContract({
253
- tool: "audit-code",
254
- contractVersion: STEP_CONTRACT_VERSION,
255
- artifactsDir,
256
- repoRoot: root,
257
- runId: null,
258
- reason: result.reason,
259
- artifactPaths: {
260
- operator_handoff: join(artifactsDir, "operator-handoff.json"),
261
- },
262
- });
263
- console.log(JSON.stringify(step, null, 2));
262
+ }
263
+ /**
264
+ * Bind one row to its own result variant. The table is keyed BY the result
265
+ * kind, so the row that runs is by construction the row for `ctx.result.kind`;
266
+ * the narrowing is stated once here rather than re-checked in every body.
267
+ */
268
+ function emissionRow(handle) {
269
+ return (ctx) => handle(ctx, ctx.result);
270
+ }
271
+ /**
272
+ * The caller-side precondition the analyzer-acquisition chokepoint declares:
273
+ * an acquisition-bearing step is only emitted for a run whose durable analyzer
274
+ * policy actually loaded. Refusing here (the terminal backstop turns the throw
275
+ * into a written blocked step) keeps the alternative — emitting a consent or
276
+ * install step against no policy — impossible: those steps collect decisions
277
+ * that have nothing to merge into, and admission would see no recorded
278
+ * decision at all, making an operator's recorded decline unrepresentable
279
+ * rather than merely unenforced.
280
+ */
281
+ function requireLoadedAnalyzerPolicy(policy, stepKind) {
282
+ if (policy)
264
283
  return;
265
- }
266
- if (result.kind === "design_review_parallel") {
267
- // Both passes are unsatisfied — dispatch the contract pass and the
268
- // conceptual pass simultaneously. The conceptual pass is shallow (one agent)
269
- // or deep (N independent perspective subagents + an independent judge),
270
- // resolved JIT from the user-confirmed checkpoint / session config.
271
- const continueCommand = nextStepCommand(root, artifactsDir);
272
- const conceptualSettings = resolveConceptualReviewSettings(result.bundle);
273
- const contract = await prepareContractDispatch({
274
- artifactsDir,
275
- bundle: result.bundle,
276
- maxUnits: conceptualSettings.max_units,
277
- });
278
- const conceptual = await prepareConceptualPass(artifactsDir, result.bundle, conceptualSettings);
279
- const shortfall = mergeLaneShortfalls([
280
- contract.shortfall,
281
- conceptual.shortfall,
282
- ]);
283
- const dispatchPrompt = [
284
- "# Design review — parallel dispatch",
284
+ throw new Error(`audit-code next-step: the ${stepKind} step is acquisition-bearing and requires the durable analyzer policy, which was not loaded. Refusing to emit it rather than proceeding with no recorded consent decisions.`);
285
+ }
286
+ /**
287
+ * The acquisition chokepoint's declared caller obligation, discharged in ONE
288
+ * place: BOTH halves of the loaded policy ride every acquisition call — the
289
+ * per-analyzer settings AND the recorded consent decisions. Dropping the
290
+ * decisions leaves a recorded decline unrepresentable at admission (the
291
+ * chokepoint would see `undefined` and read it as "not yet decided"), and no
292
+ * consent token is synthesized here on the operator's behalf: the field is
293
+ * absent, never an empty string.
294
+ */
295
+ export function buildExternalAcquisitionOptions(policy) {
296
+ return {
297
+ enabled: true,
298
+ analyzers: policy.analyzers,
299
+ // Item B: recorded consent decisions ride into admission (granted admits
300
+ // without a token) and into the consent fold's pending computation.
301
+ analyzerConsent: policy.analyzer_consent,
302
+ };
303
+ }
304
+ const emitComplete = emissionRow(async ({ root, artifactsDir }, result) => {
305
+ const triage = result.triage;
306
+ const frictionPending = triage?.action === "dispose";
307
+ return currentStepPlan({
308
+ artifactsDir,
309
+ stepKind: "present_report",
310
+ status: frictionPending ? "ready" : "complete",
311
+ runId: null,
312
+ // INV-READY-STEP-CONTINUATION (COR-f6a36670): a ready step whose
313
+ // stop_condition instructs calling next-step again must carry the
314
+ // executable continuation command — never leave the host to reconstruct
315
+ // the invocation from prose.
316
+ allowedCommands: frictionPending
317
+ ? [nextStepCommand(root, artifactsDir)]
318
+ : [],
319
+ stopCondition: frictionPending
320
+ ? "Complete friction triage (write open_observations and any dispositions), then call next-step again."
321
+ : "Present the final audit report and stop.",
322
+ repoRoot: root,
323
+ artifactPaths: {
324
+ final_report: result.finalReportPath,
325
+ ...(triage ? { friction_record: triage.recordPath } : {}),
326
+ },
327
+ prompt: renderPresentReportPrompt(result.finalReportPath, triage),
328
+ });
329
+ });
330
+ const emitBlocked = emissionRow(async ({ root, artifactsDir }, result) => {
331
+ // The SAME operator-handoff blocked contract the pre-dispatch config
332
+ // failure emits — built from the one home, never re-typed here.
333
+ return blockedStepPlan(operatorHandoffBlock(root, artifactsDir, result.reason));
334
+ });
335
+ const emitDesignReviewParallel = emissionRow(async ({ root, artifactsDir }, result) => {
336
+ // Both passes are unsatisfied — dispatch the contract pass and the
337
+ // conceptual pass simultaneously. The conceptual pass is shallow (one agent)
338
+ // or deep (N independent perspective subagents + an independent judge),
339
+ // resolved JIT from the user-confirmed checkpoint / session config.
340
+ const continueCommand = nextStepCommand(root, artifactsDir);
341
+ const conceptualSettings = resolveConceptualReviewSettings(result.bundle);
342
+ const contract = await prepareContractDispatch({
343
+ artifactsDir,
344
+ bundle: result.bundle,
345
+ maxUnits: conceptualSettings.max_units,
346
+ });
347
+ const conceptual = await prepareConceptualPass(artifactsDir, result.bundle, conceptualSettings);
348
+ const shortfall = mergeLaneShortfalls([
349
+ contract.shortfall,
350
+ conceptual.shortfall,
351
+ ]);
352
+ const dispatchPrompt = [
353
+ "# Design review — parallel dispatch",
354
+ "",
355
+ ...renderLaneShortfallLines(shortfall),
356
+ "Run the two design-review passes concurrently. Do not wait for one before starting the other.",
357
+ "",
358
+ `1. ${contract.instructionLine}`,
359
+ `2. ${conceptual.instructionLines.join("\n ")}`,
360
+ "",
361
+ "When the contract results and the conceptual results have both been written, run:",
362
+ "",
363
+ ` ${continueCommand}`,
364
+ "",
365
+ ].join("\n");
366
+ return currentStepPlan({
367
+ artifactsDir,
368
+ stepKind: "design_review_parallel",
369
+ status: "ready",
370
+ runId: null,
371
+ allowedCommands: [continueCommand],
372
+ stopCondition: "Dispatch the contract and conceptual review subagents in parallel, then run next-step once both results are written.",
373
+ repoRoot: root,
374
+ artifactPaths: {
375
+ ...contract.artifactPaths,
376
+ ...conceptual.artifactPaths,
377
+ },
378
+ prompt: dispatchPrompt,
379
+ access: {
380
+ read_paths: [...contract.readPaths, ...conceptual.readPaths],
381
+ write_paths: [...contract.writePaths, ...conceptual.writePaths],
382
+ },
383
+ submissionShortfall: shortfall,
384
+ });
385
+ });
386
+ const emitDesignReviewContract = emissionRow(async ({ root, artifactsDir }, result) => {
387
+ // Only the contract pass remains — dispatched exactly as in the parallel
388
+ // branch. This branch is reached whenever the conceptual pass is already
389
+ // done, i.e. late in a run the host itself drove, which is precisely when
390
+ // rendering the adversarial review into the host's own prompt would have it
391
+ // grade its own work (see prepareContractDispatch).
392
+ const continueCommand = nextStepCommand(root, artifactsDir);
393
+ const contract = await prepareContractDispatch({
394
+ artifactsDir,
395
+ bundle: result.bundle,
396
+ maxUnits: resolveConceptualReviewSettings(result.bundle).max_units,
397
+ });
398
+ const dispatchPrompt = [
399
+ "# Design review — contract pass",
400
+ "",
401
+ ...renderLaneShortfallLines(contract.shortfall),
402
+ contract.instructionLine,
403
+ "",
404
+ "When the contract results have been written, run:",
405
+ "",
406
+ ` ${continueCommand}`,
407
+ "",
408
+ ].join("\n");
409
+ return currentStepPlan({
410
+ artifactsDir,
411
+ stepKind: "design_review_contract",
412
+ status: "ready",
413
+ runId: null,
414
+ allowedCommands: [continueCommand],
415
+ stopCondition: "Dispatch the contract review subagent, then run next-step once the contract results are written.",
416
+ repoRoot: root,
417
+ artifactPaths: contract.artifactPaths,
418
+ prompt: dispatchPrompt,
419
+ access: {
420
+ read_paths: contract.readPaths,
421
+ write_paths: contract.writePaths,
422
+ },
423
+ submissionShortfall: contract.shortfall,
424
+ });
425
+ });
426
+ const emitDesignReviewConceptual = emissionRow(async ({ root, artifactsDir }, result) => {
427
+ // Only the conceptual pass remains — shallow (one agent) or deep (N
428
+ // independent perspective subagents + an independent judge), resolved JIT
429
+ // from the user-confirmed checkpoint / session config.
430
+ const continueCommand = nextStepCommand(root, artifactsDir);
431
+ const conceptualSettings = resolveConceptualReviewSettings(result.bundle);
432
+ const conceptual = await prepareConceptualPass(artifactsDir, result.bundle, conceptualSettings);
433
+ const prompt = [
434
+ "# Design review — conceptual pass",
435
+ "",
436
+ ...renderLaneShortfallLines(conceptual.shortfall),
437
+ conceptual.instructionLines.join("\n"),
438
+ "",
439
+ "When the conceptual results have been written, run:",
440
+ "",
441
+ ` ${continueCommand}`,
442
+ "",
443
+ ].join("\n");
444
+ return currentStepPlan({
445
+ artifactsDir,
446
+ stepKind: "design_review_conceptual",
447
+ status: "ready",
448
+ runId: null,
449
+ allowedCommands: [continueCommand],
450
+ stopCondition: conceptual.deep
451
+ ? "Dispatch the conceptual perspective subagents in parallel, then the independent judge, then run next-step once the merged conceptual results are written."
452
+ : "Write conceptual review findings to the results path, then run next-step.",
453
+ repoRoot: root,
454
+ artifactPaths: {
455
+ design_review_conceptual_results: conceptual.conceptualResultsPath,
456
+ ...conceptual.artifactPaths,
457
+ },
458
+ prompt,
459
+ access: {
460
+ read_paths: conceptual.readPaths,
461
+ write_paths: conceptual.writePaths,
462
+ },
463
+ submissionShortfall: conceptual.shortfall,
464
+ });
465
+ });
466
+ const emitCharterExtraction = emissionRow(async ({ root, artifactsDir }, result) => {
467
+ // Phase C charter layer (conceptual, teleological): one blind, materialized
468
+ // LANE per charter kind (design resolution 2 — independence is the shape of
469
+ // the artifacts, not a merge instruction); the tool merges the per-kind
470
+ // submissions and gates + routes them at ingest. Only reached at a deep+
471
+ // ceiling (shallow omits deterministically without a host turn).
472
+ const continueCommand = nextStepCommand(root, artifactsDir);
473
+ const ceiling = resolveCharterCeiling(result.bundle.intent_checkpoint);
474
+ const kinds = charterExtractionKindsForCeiling(ceiling);
475
+ // Channel purity is a property of the INPUT (design resolution 4): each
476
+ // lane gets a tool-materialized evidence packet holding only its channel's
477
+ // material. Packets are (re)written on every emission — they derive from
478
+ // the bundle + disk, so re-materializing is idempotent and keeps a resumed
479
+ // lane's evidence current.
480
+ const packetPaths = [];
481
+ const laneSpecs = await Promise.all(kinds.map(async (kind) => {
482
+ const lane = charterExtractionLane(kind);
483
+ const submissionPath = laneSubmissionPath(artifactsDir, lane);
484
+ const packetPath = join(laneAssetsDir(artifactsDir), charterExtractionPacketFilename(kind));
485
+ await writeTextFile(packetPath, await materializeCharterPacket({ root, bundle: result.bundle, kind }));
486
+ packetPaths.push(packetPath);
487
+ return {
488
+ id: lane,
489
+ label: `Charter ${kind} author (blind lane)`,
490
+ promptFilename: `charter-extraction-${kind}-prompt.md`,
491
+ promptText: renderCharterKindLanePrompt(result.bundle, {
492
+ kind,
493
+ submissionPath,
494
+ packetPath,
495
+ }),
496
+ };
497
+ }));
498
+ const fanout = await materializeFanoutLanes({
499
+ artifactsDir,
500
+ runId: AUDIT_GATE_SUBMISSION_SCOPE,
501
+ lanes: laneSpecs,
502
+ });
503
+ const completedLanes = fanout.lanes.filter((lane) => lane.resultExists);
504
+ return currentStepPlan({
505
+ artifactsDir,
506
+ stepKind: "charter_extraction",
507
+ status: "ready",
508
+ runId: null,
509
+ allowedCommands: [continueCommand],
510
+ stopCondition: "Execute each pending charter lane prompt (one blind agent per kind — subagents if available, else sequentially), write each lane's submission to its results path, then run next-step.",
511
+ repoRoot: root,
512
+ artifactPaths: fanout.artifactPaths,
513
+ prompt: [
514
+ "# audit-code charter extraction (per-kind blind lanes)",
285
515
  "",
286
- ...renderLaneShortfallLines(shortfall),
287
- "Run the two design-review passes concurrently. Do not wait for one before starting the other.",
516
+ ...renderLaneShortfallLines(fanout.shortfall),
517
+ "Each charter kind is authored by its OWN blind lane: a lane must not see another lane's prompt or output, so the later stated↔revealed delta is genuine disagreement rather than one author's self-consistent story. The tool merges the per-kind submissions at ingest.",
288
518
  "",
289
- `1. ${contract.instructionLine}`,
290
- `2. ${conceptual.instructionLines.join("\n ")}`,
519
+ ...renderFanoutExecutionLines({
520
+ lanes: fanout.pendingLanes.map((lane) => ({
521
+ label: lane.label,
522
+ promptPath: lane.promptPath,
523
+ resultPath: lane.resultPath,
524
+ })),
525
+ }),
291
526
  "",
292
- "When the contract results and the conceptual results have both been written, run:",
527
+ ...(completedLanes.length > 0
528
+ ? [
529
+ `Already complete (results on disk — do NOT redo these lanes): ${completedLanes
530
+ .map((lane) => lane.label)
531
+ .join(", ")}.`,
532
+ "",
533
+ ]
534
+ : []),
535
+ "When every pending lane's result file exists, run:",
293
536
  "",
294
537
  ` ${continueCommand}`,
295
538
  "",
296
- ].join("\n");
297
- const step = await writeCurrentStep({
298
- artifactsDir,
299
- stepKind: "design_review_parallel",
300
- status: "ready",
301
- runId: null,
302
- allowedCommands: [continueCommand],
303
- stopCondition: "Dispatch the contract and conceptual review subagents in parallel, then run next-step once both results are written.",
304
- repoRoot: root,
305
- artifactPaths: {
306
- ...contract.artifactPaths,
307
- ...conceptual.artifactPaths,
308
- },
309
- prompt: dispatchPrompt,
310
- access: {
311
- read_paths: [...contract.readPaths, ...conceptual.readPaths],
312
- write_paths: [...contract.writePaths, ...conceptual.writePaths],
539
+ "Read and follow only the new step prompt returned by that command.",
540
+ "",
541
+ ].join("\n"),
542
+ access: {
543
+ // Prompts + packets only: the lanes' whole input is materialized, so no
544
+ // repo/artifact read grant exists to leak another channel's evidence.
545
+ read_paths: [...fanout.readPaths, ...packetPaths],
546
+ write_paths: fanout.writePaths,
547
+ },
548
+ submissionShortfall: fanout.shortfall,
549
+ });
550
+ });
551
+ const emitCharterDelta = emissionRow(async ({ root, artifactsDir }, result) => {
552
+ // Phase C.2 charter delta-mining (conceptual, teleological): an INDEPENDENT
553
+ // delta-miner reads the assembled charters (authored by a different pass, blind
554
+ // to the gaps) and mines the pairwise deltas + the goal DAG; the tool routes +
555
+ // gates them at ingest. Only reached at a deep+ ceiling whose extraction pass
556
+ // produced ≥1 subsystem (charter_register.deltas_pending).
557
+ // Always-materialized (design resolution 2): the miner prompt is a lane FILE.
558
+ const continueCommand = nextStepCommand(root, artifactsDir);
559
+ const submissionPath = laneSubmissionPath(artifactsDir, GATE_LANES.charter_delta);
560
+ const lanePrompt = renderCharterDeltaPrompt(result.bundle, { submissionPath });
561
+ const fanout = await materializeFanoutLanes({
562
+ artifactsDir,
563
+ runId: AUDIT_GATE_SUBMISSION_SCOPE,
564
+ lanes: [
565
+ {
566
+ id: GATE_LANES.charter_delta,
567
+ label: "Independent charter delta-miner",
568
+ promptFilename: "charter-delta-prompt.md",
569
+ promptText: lanePrompt,
313
570
  },
314
- submissionShortfall: shortfall,
315
- });
316
- console.log(JSON.stringify(step, null, 2));
317
- return;
318
- }
319
- if (result.kind === "design_review_contract") {
320
- // Only the contract pass remains — dispatched exactly as in the parallel
321
- // branch. This branch is reached whenever the conceptual pass is already
322
- // done, i.e. late in a run the host itself drove, which is precisely when
323
- // rendering the adversarial review into the host's own prompt would have it
324
- // grade its own work (see prepareContractDispatch).
325
- const continueCommand = nextStepCommand(root, artifactsDir);
326
- const contract = await prepareContractDispatch({
327
- artifactsDir,
328
- bundle: result.bundle,
329
- maxUnits: resolveConceptualReviewSettings(result.bundle).max_units,
330
- });
331
- const dispatchPrompt = [
332
- "# Design review — contract pass",
571
+ ],
572
+ });
573
+ return currentStepPlan({
574
+ artifactsDir,
575
+ stepKind: "charter_delta",
576
+ status: "ready",
577
+ runId: null,
578
+ allowedCommands: [continueCommand],
579
+ stopCondition: "Execute the delta-miner lane prompt (subagent if available, else yourself), write the mined deltas + goal graph to the results path, then run next-step.",
580
+ repoRoot: root,
581
+ artifactPaths: fanout.artifactPaths,
582
+ prompt: [
583
+ "# audit-code charter delta-mining",
333
584
  "",
334
- ...renderLaneShortfallLines(contract.shortfall),
335
- contract.instructionLine,
585
+ ...renderLaneShortfallLines(fanout.shortfall),
586
+ "The assembled charters are ready for the INDEPENDENT delta-miner (it did not author them).",
336
587
  "",
337
- "When the contract results have been written, run:",
588
+ ...renderFanoutExecutionLines({
589
+ lanes: fanout.pendingLanes.map((lane) => ({
590
+ label: lane.label,
591
+ promptPath: lane.promptPath,
592
+ })),
593
+ }),
594
+ "",
595
+ "The executor must write its CharterDeltaSubmission JSON to:",
596
+ "",
597
+ ` ${submissionPath}`,
598
+ "",
599
+ "When the result file exists, run:",
338
600
  "",
339
601
  ` ${continueCommand}`,
340
602
  "",
341
- ].join("\n");
342
- const step = await writeCurrentStep({
343
- artifactsDir,
344
- stepKind: "design_review_contract",
345
- status: "ready",
346
- runId: null,
347
- allowedCommands: [continueCommand],
348
- stopCondition: "Dispatch the contract review subagent, then run next-step once the contract results are written.",
349
- repoRoot: root,
350
- artifactPaths: contract.artifactPaths,
351
- prompt: dispatchPrompt,
352
- access: {
353
- read_paths: contract.readPaths,
354
- write_paths: contract.writePaths,
603
+ "Read and follow only the new step prompt returned by that command.",
604
+ "",
605
+ ].join("\n"),
606
+ access: {
607
+ read_paths: [
608
+ ...fanout.readPaths,
609
+ join(artifactsDir, "charter_register.json"),
610
+ ],
611
+ write_paths: fanout.writePaths,
612
+ },
613
+ submissionShortfall: fanout.shortfall,
614
+ });
615
+ });
616
+ const emitCharterClarification = emissionRow(async ({ root, artifactsDir }, result) => {
617
+ // Phase D triangulation loop: the tool has already run the deterministic loop
618
+ // (partition → VOI-rank → risk-gate → split by attention) and surfaces the
619
+ // interactive queue here. The host relays each SYMMETRIC question and writes the
620
+ // answers back; the executor applies them + re-splits (interruptible: unanswered
621
+ // questions leave-open). Only reached at a deep+ ceiling with attention > 0 and
622
+ // ≥1 open interactive question.
623
+ const continueCommand = nextStepCommand(root, artifactsDir);
624
+ const answersPath = laneSubmissionPath(artifactsDir, GATE_LANES.charter_clarification);
625
+ const ceiling = resolveCharterCeiling(result.bundle.intent_checkpoint);
626
+ const clarificationPrompt = renderCharterClarificationPrompt(result.bundle, {
627
+ answersPath,
628
+ continueCommand,
629
+ ceiling,
630
+ });
631
+ const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.charter_clarification, promptText: clarificationPrompt }]);
632
+ return currentStepPlan({
633
+ artifactsDir,
634
+ stepKind: "charter_clarification",
635
+ status: "ready",
636
+ runId: null,
637
+ allowedCommands: [continueCommand],
638
+ stopCondition: "Relay each charter-alignment question to the user, write the answers to the answers path, then run next-step.",
639
+ repoRoot: root,
640
+ artifactPaths: {
641
+ charter_clarification_answers: answersPath,
642
+ },
643
+ prompt: [
644
+ ...renderLaneShortfallLines(shortfall),
645
+ clarificationPrompt,
646
+ ].join("\n"),
647
+ access: {
648
+ read_paths: [join(artifactsDir, "charter_clarification.json")],
649
+ write_paths: [answersPath],
650
+ },
651
+ submissionShortfall: shortfall,
652
+ });
653
+ });
654
+ const emitSystemicChallenge = emissionRow(async ({ root, artifactsDir }, result) => {
655
+ // Phase E second-order adversary (loop-until-dry): the tool has opened the loop
656
+ // and computed the language-neutral aggregate-metrics digest. The host dispatches
657
+ // a SEPARATE adversary agent whose mandate is optimization/better-way; it writes
658
+ // the round's improvement findings (true-lens) back, and the executor folds them
659
+ // + decides convergence. An empty submission converges the loop.
660
+ const continueCommand = nextStepCommand(root, artifactsDir);
661
+ const submissionPath = laneSubmissionPath(artifactsDir, GATE_LANES.systemic_challenge);
662
+ const metrics = result.bundle.systemic_challenge?.metrics ?? aggregateMetricsDigest(result.bundle);
663
+ const adversaryPrompt = renderSecondOrderAdversaryPrompt({
664
+ round: (result.bundle.systemic_challenge?.rounds.length ?? 0) + 1,
665
+ priorFindingCount: result.bundle.systemic_challenge?.findings.length ?? 0,
666
+ metrics,
667
+ submissionPath,
668
+ });
669
+ // Always-materialized (design resolution 2): the adversary prompt is a lane
670
+ // FILE — the adversary is a SEPARATE agent by lane class, on every host.
671
+ const fanout = await materializeFanoutLanes({
672
+ artifactsDir,
673
+ runId: AUDIT_GATE_SUBMISSION_SCOPE,
674
+ lanes: [
675
+ {
676
+ id: GATE_LANES.systemic_challenge,
677
+ label: "Second-order adversary (improvement-seeking challenge)",
678
+ promptFilename: "systemic-challenge-prompt.md",
679
+ promptText: adversaryPrompt,
355
680
  },
356
- submissionShortfall: contract.shortfall,
357
- });
358
- console.log(JSON.stringify(step, null, 2));
359
- return;
360
- }
361
- if (result.kind === "design_review_conceptual") {
362
- // Only the conceptual pass remains — shallow (one agent) or deep (N
363
- // independent perspective subagents + an independent judge), resolved JIT
364
- // from the user-confirmed checkpoint / session config.
365
- const continueCommand = nextStepCommand(root, artifactsDir);
366
- const conceptualSettings = resolveConceptualReviewSettings(result.bundle);
367
- const conceptual = await prepareConceptualPass(artifactsDir, result.bundle, conceptualSettings);
368
- const prompt = [
369
- "# Design review — conceptual pass",
681
+ ],
682
+ });
683
+ return currentStepPlan({
684
+ artifactsDir,
685
+ stepKind: "systemic_challenge",
686
+ status: "ready",
687
+ runId: null,
688
+ allowedCommands: [continueCommand],
689
+ stopCondition: "Execute the second-order-adversary lane prompt (a separate agent from the one that drove this audit), write its findings to the results path, then run next-step. An empty findings array converges the loop.",
690
+ repoRoot: root,
691
+ artifactPaths: fanout.artifactPaths,
692
+ prompt: [
693
+ "# audit-code systemic challenge (second-order adversary)",
694
+ "",
695
+ ...renderLaneShortfallLines(fanout.shortfall),
696
+ "This round's adversary lane challenges the audit process itself (optimization/better-way mandate). The adversary must NOT be the agent that drove this audit.",
697
+ "",
698
+ ...renderFanoutExecutionLines({
699
+ lanes: fanout.pendingLanes.map((lane) => ({
700
+ label: lane.label,
701
+ promptPath: lane.promptPath,
702
+ })),
703
+ }),
370
704
  "",
371
- ...renderLaneShortfallLines(conceptual.shortfall),
372
- conceptual.instructionLines.join("\n"),
705
+ "The executor must write its findings JSON to:",
373
706
  "",
374
- "When the conceptual results have been written, run:",
707
+ ` ${submissionPath}`,
708
+ "",
709
+ "An EMPTY findings array is the deliberate loop terminator (this round found nothing new).",
710
+ "",
711
+ "When the result file exists, run:",
375
712
  "",
376
713
  ` ${continueCommand}`,
377
714
  "",
378
- ].join("\n");
379
- const step = await writeCurrentStep({
380
- artifactsDir,
381
- stepKind: "design_review_conceptual",
382
- status: "ready",
383
- runId: null,
384
- allowedCommands: [continueCommand],
385
- stopCondition: conceptual.deep
386
- ? "Dispatch the conceptual perspective subagents in parallel, then the independent judge, then run next-step once the merged conceptual results are written."
387
- : "Write conceptual review findings to the results path, then run next-step.",
388
- repoRoot: root,
389
- artifactPaths: {
390
- design_review_conceptual_results: conceptual.conceptualResultsPath,
391
- ...conceptual.artifactPaths,
392
- },
393
- prompt,
394
- access: {
395
- read_paths: conceptual.readPaths,
396
- write_paths: conceptual.writePaths,
397
- },
398
- submissionShortfall: conceptual.shortfall,
399
- });
400
- console.log(JSON.stringify(step, null, 2));
401
- return;
402
- }
403
- if (result.kind === "charter_extraction") {
404
- // Phase C charter layer (conceptual, teleological): one blind, materialized
405
- // LANE per charter kind (design resolution 2 — independence is the shape of
406
- // the artifacts, not a merge instruction); the tool merges the per-kind
407
- // submissions and gates + routes them at ingest. Only reached at a deep+
408
- // ceiling (shallow omits deterministically without a host turn).
409
- const continueCommand = nextStepCommand(root, artifactsDir);
410
- const ceiling = resolveCharterCeiling(result.bundle.intent_checkpoint);
411
- const kinds = charterExtractionKindsForCeiling(ceiling);
412
- // Channel purity is a property of the INPUT (design resolution 4): each
413
- // lane gets a tool-materialized evidence packet holding only its channel's
414
- // material. Packets are (re)written on every emission — they derive from
415
- // the bundle + disk, so re-materializing is idempotent and keeps a resumed
416
- // lane's evidence current.
417
- const packetPaths = [];
418
- const laneSpecs = await Promise.all(kinds.map(async (kind) => {
419
- const lane = charterExtractionLane(kind);
420
- const submissionPath = laneSubmissionPath(artifactsDir, lane);
421
- const packetPath = join(laneAssetsDir(artifactsDir), charterExtractionPacketFilename(kind));
422
- await writeTextFile(packetPath, await materializeCharterPacket({ root, bundle: result.bundle, kind }));
423
- packetPaths.push(packetPath);
424
- return {
425
- id: lane,
426
- label: `Charter ${kind} author (blind lane)`,
427
- promptFilename: `charter-extraction-${kind}-prompt.md`,
428
- promptText: renderCharterKindLanePrompt(result.bundle, {
429
- kind,
430
- submissionPath,
431
- packetPath,
432
- }),
433
- };
434
- }));
435
- const fanout = await materializeFanoutLanes({
436
- artifactsDir,
437
- runId: AUDIT_GATE_SUBMISSION_SCOPE,
438
- lanes: laneSpecs,
439
- });
440
- const completedLanes = fanout.lanes.filter((lane) => lane.resultExists);
441
- const step = await writeCurrentStep({
442
- artifactsDir,
443
- stepKind: "charter_extraction",
444
- status: "ready",
445
- runId: null,
446
- allowedCommands: [continueCommand],
447
- stopCondition: "Execute each pending charter lane prompt (one blind agent per kind — subagents if available, else sequentially), write each lane's submission to its results path, then run next-step.",
448
- repoRoot: root,
449
- artifactPaths: fanout.artifactPaths,
450
- prompt: [
451
- "# audit-code charter extraction (per-kind blind lanes)",
452
- "",
453
- ...renderLaneShortfallLines(fanout.shortfall),
454
- "Each charter kind is authored by its OWN blind lane: a lane must not see another lane's prompt or output, so the later stated↔revealed delta is genuine disagreement rather than one author's self-consistent story. The tool merges the per-kind submissions at ingest.",
455
- "",
456
- ...renderFanoutExecutionLines({
457
- lanes: fanout.pendingLanes.map((lane) => ({
458
- label: lane.label,
459
- promptPath: lane.promptPath,
460
- resultPath: lane.resultPath,
461
- })),
462
- }),
463
- "",
464
- ...(completedLanes.length > 0
465
- ? [
466
- `Already complete (results on disk — do NOT redo these lanes): ${completedLanes
467
- .map((lane) => lane.label)
468
- .join(", ")}.`,
469
- "",
470
- ]
471
- : []),
472
- "When every pending lane's result file exists, run:",
473
- "",
474
- ` ${continueCommand}`,
475
- "",
476
- "Read and follow only the new step prompt returned by that command.",
477
- "",
478
- ].join("\n"),
479
- access: {
480
- // Prompts + packets only: the lanes' whole input is materialized, so no
481
- // repo/artifact read grant exists to leak another channel's evidence.
482
- read_paths: [...fanout.readPaths, ...packetPaths],
483
- write_paths: fanout.writePaths,
484
- },
485
- submissionShortfall: fanout.shortfall,
486
- });
487
- console.log(JSON.stringify(step, null, 2));
488
- return;
489
- }
490
- if (result.kind === "charter_delta") {
491
- // Phase C.2 charter delta-mining (conceptual, teleological): an INDEPENDENT
492
- // delta-miner reads the assembled charters (authored by a different pass, blind
493
- // to the gaps) and mines the pairwise deltas + the goal DAG; the tool routes +
494
- // gates them at ingest. Only reached at a deep+ ceiling whose extraction pass
495
- // produced ≥1 subsystem (charter_register.deltas_pending).
496
- // Always-materialized (design resolution 2): the miner prompt is a lane FILE.
497
- const continueCommand = nextStepCommand(root, artifactsDir);
498
- const submissionPath = laneSubmissionPath(artifactsDir, GATE_LANES.charter_delta);
499
- const lanePrompt = renderCharterDeltaPrompt(result.bundle, { submissionPath });
500
- const fanout = await materializeFanoutLanes({
501
- artifactsDir,
502
- runId: AUDIT_GATE_SUBMISSION_SCOPE,
503
- lanes: [
504
- {
505
- id: GATE_LANES.charter_delta,
506
- label: "Independent charter delta-miner",
507
- promptFilename: "charter-delta-prompt.md",
508
- promptText: lanePrompt,
509
- },
715
+ "Read and follow only the new step prompt returned by that command.",
716
+ "",
717
+ ].join("\n"),
718
+ access: {
719
+ read_paths: [
720
+ ...fanout.readPaths,
721
+ join(artifactsDir, "systemic_challenge.json"),
510
722
  ],
511
- });
512
- const step = await writeCurrentStep({
513
- artifactsDir,
514
- stepKind: "charter_delta",
515
- status: "ready",
516
- runId: null,
517
- allowedCommands: [continueCommand],
518
- stopCondition: "Execute the delta-miner lane prompt (subagent if available, else yourself), write the mined deltas + goal graph to the results path, then run next-step.",
519
- repoRoot: root,
520
- artifactPaths: fanout.artifactPaths,
521
- prompt: [
522
- "# audit-code charter delta-mining",
523
- "",
524
- ...renderLaneShortfallLines(fanout.shortfall),
525
- "The assembled charters are ready for the INDEPENDENT delta-miner (it did not author them).",
526
- "",
527
- ...renderFanoutExecutionLines({
528
- lanes: fanout.pendingLanes.map((lane) => ({
529
- label: lane.label,
530
- promptPath: lane.promptPath,
531
- })),
532
- }),
533
- "",
534
- "The executor must write its CharterDeltaSubmission JSON to:",
535
- "",
536
- ` ${submissionPath}`,
537
- "",
538
- "When the result file exists, run:",
539
- "",
540
- ` ${continueCommand}`,
541
- "",
542
- "Read and follow only the new step prompt returned by that command.",
543
- "",
544
- ].join("\n"),
545
- access: {
546
- read_paths: [
547
- ...fanout.readPaths,
548
- join(artifactsDir, "charter_register.json"),
549
- ],
550
- write_paths: fanout.writePaths,
551
- },
552
- submissionShortfall: fanout.shortfall,
553
- });
554
- console.log(JSON.stringify(step, null, 2));
555
- return;
556
- }
557
- if (result.kind === "charter_clarification") {
558
- // Phase D triangulation loop: the tool has already run the deterministic loop
559
- // (partition → VOI-rank → risk-gate → split by attention) and surfaces the
560
- // interactive queue here. The host relays each SYMMETRIC question and writes the
561
- // answers back; the executor applies them + re-splits (interruptible: unanswered
562
- // questions leave-open). Only reached at a deep+ ceiling with attention > 0 and
563
- // ≥1 open interactive question.
564
- const continueCommand = nextStepCommand(root, artifactsDir);
565
- const answersPath = laneSubmissionPath(artifactsDir, GATE_LANES.charter_clarification);
566
- const ceiling = resolveCharterCeiling(result.bundle.intent_checkpoint);
567
- const clarificationPrompt = renderCharterClarificationPrompt(result.bundle, {
568
- answersPath,
723
+ write_paths: fanout.writePaths,
724
+ },
725
+ submissionShortfall: fanout.shortfall,
726
+ });
727
+ });
728
+ const emitConfirmIntent = emissionRow(async ({ root, artifactsDir, argv }, result) => {
729
+ const intentCheckpointPath = join(artifactsDir, "intent_checkpoint.json");
730
+ const continueCommand = nextStepCommand(root, artifactsDir);
731
+ const preDigest = computeScopePreDigest(result.bundle, root, getFlag(argv, "--since"));
732
+ return currentStepPlan({
733
+ artifactsDir,
734
+ stepKind: "confirm_intent",
735
+ status: "ready",
736
+ runId: null,
737
+ allowedCommands: [continueCommand],
738
+ stopCondition: "Write intent_checkpoint.json with the confirmed scope and intent, then run next-step.",
739
+ repoRoot: root,
740
+ artifactPaths: {
741
+ intent_checkpoint: intentCheckpointPath,
742
+ },
743
+ prompt: renderConfirmIntentPrompt(preDigest, {
744
+ intentCheckpointPath,
569
745
  continueCommand,
570
- ceiling,
571
- });
572
- const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.charter_clarification, promptText: clarificationPrompt }]);
573
- const step = await writeCurrentStep({
574
- artifactsDir,
575
- stepKind: "charter_clarification",
576
- status: "ready",
577
- runId: null,
578
- allowedCommands: [continueCommand],
579
- stopCondition: "Relay each charter-alignment question to the user, write the answers to the answers path, then run next-step.",
580
- repoRoot: root,
581
- artifactPaths: {
582
- charter_clarification_answers: answersPath,
583
- },
584
- prompt: [
585
- ...renderLaneShortfallLines(shortfall),
586
- clarificationPrompt,
587
- ].join("\n"),
588
- access: {
589
- read_paths: [join(artifactsDir, "charter_clarification.json")],
590
- write_paths: [answersPath],
591
- },
592
- submissionShortfall: shortfall,
593
- });
594
- console.log(JSON.stringify(step, null, 2));
595
- return;
596
- }
597
- if (result.kind === "systemic_challenge") {
598
- // Phase E second-order adversary (loop-until-dry): the tool has opened the loop
599
- // and computed the language-neutral aggregate-metrics digest. The host dispatches
600
- // a SEPARATE adversary agent whose mandate is optimization/better-way; it writes
601
- // the round's improvement findings (true-lens) back, and the executor folds them
602
- // + decides convergence. An empty submission converges the loop.
603
- const continueCommand = nextStepCommand(root, artifactsDir);
604
- const submissionPath = laneSubmissionPath(artifactsDir, GATE_LANES.systemic_challenge);
605
- const metrics = result.bundle.systemic_challenge?.metrics ?? aggregateMetricsDigest(result.bundle);
606
- const adversaryPrompt = renderSecondOrderAdversaryPrompt({
607
- round: (result.bundle.systemic_challenge?.rounds.length ?? 0) + 1,
608
- priorFindingCount: result.bundle.systemic_challenge?.findings.length ?? 0,
609
- metrics,
610
- submissionPath,
611
- });
612
- // Always-materialized (design resolution 2): the adversary prompt is a lane
613
- // FILE — the adversary is a SEPARATE agent by lane class, on every host.
614
- const fanout = await materializeFanoutLanes({
615
- artifactsDir,
616
- runId: AUDIT_GATE_SUBMISSION_SCOPE,
617
- lanes: [
618
- {
619
- id: GATE_LANES.systemic_challenge,
620
- label: "Second-order adversary (improvement-seeking challenge)",
621
- promptFilename: "systemic-challenge-prompt.md",
622
- promptText: adversaryPrompt,
623
- },
624
- ],
625
- });
626
- const step = await writeCurrentStep({
627
- artifactsDir,
628
- stepKind: "systemic_challenge",
629
- status: "ready",
630
- runId: null,
631
- allowedCommands: [continueCommand],
632
- stopCondition: "Execute the second-order-adversary lane prompt (a separate agent from the one that drove this audit), write its findings to the results path, then run next-step. An empty findings array converges the loop.",
633
- repoRoot: root,
634
- artifactPaths: fanout.artifactPaths,
635
- prompt: [
636
- "# audit-code systemic challenge (second-order adversary)",
637
- "",
638
- ...renderLaneShortfallLines(fanout.shortfall),
639
- "This round's adversary lane challenges the audit process itself (optimization/better-way mandate). The adversary must NOT be the agent that drove this audit.",
640
- "",
641
- ...renderFanoutExecutionLines({
642
- lanes: fanout.pendingLanes.map((lane) => ({
643
- label: lane.label,
644
- promptPath: lane.promptPath,
645
- })),
646
- }),
647
- "",
648
- "The executor must write its findings JSON to:",
649
- "",
650
- ` ${submissionPath}`,
651
- "",
652
- "An EMPTY findings array is the deliberate loop terminator (this round found nothing new).",
653
- "",
654
- "When the result file exists, run:",
655
- "",
656
- ` ${continueCommand}`,
657
- "",
658
- "Read and follow only the new step prompt returned by that command.",
659
- "",
660
- ].join("\n"),
661
- access: {
662
- read_paths: [
663
- ...fanout.readPaths,
664
- join(artifactsDir, "systemic_challenge.json"),
665
- ],
666
- write_paths: fanout.writePaths,
667
- },
668
- submissionShortfall: fanout.shortfall,
669
- });
670
- console.log(JSON.stringify(step, null, 2));
671
- return;
672
- }
673
- if (result.kind === "confirm_intent") {
674
- const intentCheckpointPath = join(artifactsDir, "intent_checkpoint.json");
675
- const continueCommand = nextStepCommand(root, artifactsDir);
676
- const preDigest = computeScopePreDigest(result.bundle, root, getFlag(argv, "--since"));
677
- const step = await writeCurrentStep({
678
- artifactsDir,
679
- stepKind: "confirm_intent",
680
- status: "ready",
681
- runId: null,
682
- allowedCommands: [continueCommand],
683
- stopCondition: "Write intent_checkpoint.json with the confirmed scope and intent, then run next-step.",
684
- repoRoot: root,
685
- artifactPaths: {
686
- intent_checkpoint: intentCheckpointPath,
746
+ unresolvedConstraintClauses: unresolvedConstraintClauses(result.bundle.intent_checkpoint),
747
+ }),
748
+ });
749
+ });
750
+ const emitAnalyzerConsent = emissionRow(async ({ root, artifactsDir, analyzerPolicy }, result) => {
751
+ // Acquisition-bearing row: the durable policy must have loaded.
752
+ requireLoadedAnalyzerPolicy(analyzerPolicy, "analyzer_consent");
753
+ const decisionsPath = laneSubmissionPath(artifactsDir, GATE_LANES.analyzer_consent);
754
+ const continueCommand = nextStepCommand(root, artifactsDir);
755
+ const consentPrompt = renderAnalyzerConsentPrompt({
756
+ pending: result.pending,
757
+ decisionsPath,
758
+ continueCommand,
759
+ });
760
+ const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.analyzer_consent, promptText: consentPrompt }]);
761
+ return currentStepPlan({
762
+ artifactsDir,
763
+ stepKind: "analyzer_consent",
764
+ status: "ready",
765
+ runId: null,
766
+ allowedCommands: [continueCommand],
767
+ stopCondition: "Present the consent offer to the operator, write their decisions to the decisions path, then run next-step.",
768
+ repoRoot: root,
769
+ artifactPaths: {
770
+ analyzer_consent_decisions: decisionsPath,
771
+ },
772
+ prompt: [...renderLaneShortfallLines(shortfall), consentPrompt].join("\n"),
773
+ submissionShortfall: shortfall,
774
+ });
775
+ });
776
+ const emitAnalyzerInstall = emissionRow(async ({ root, artifactsDir, analyzerPolicy }, result) => {
777
+ // Acquisition-bearing row: the durable policy must have loaded.
778
+ requireLoadedAnalyzerPolicy(analyzerPolicy, "analyzer_install");
779
+ const decisionsPath = laneSubmissionPath(artifactsDir, GATE_LANES.analyzer_decisions);
780
+ const continueCommand = nextStepCommand(root, artifactsDir);
781
+ const installPrompt = renderAnalyzerInstallPrompt({
782
+ unresolved: result.unresolved,
783
+ decisionsPath,
784
+ continueCommand,
785
+ });
786
+ const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.analyzer_decisions, promptText: installPrompt }]);
787
+ return currentStepPlan({
788
+ artifactsDir,
789
+ stepKind: "analyzer_install",
790
+ status: "ready",
791
+ runId: null,
792
+ allowedCommands: [continueCommand],
793
+ stopCondition: "Write analyzer install decisions to the results path, then run next-step.",
794
+ repoRoot: root,
795
+ artifactPaths: {
796
+ analyzer_decisions: decisionsPath,
797
+ },
798
+ prompt: [...renderLaneShortfallLines(shortfall), installPrompt].join("\n"),
799
+ submissionShortfall: shortfall,
800
+ });
801
+ });
802
+ const emitEdgeReasoning = emissionRow(async ({ root, artifactsDir }, result) => {
803
+ const edgeReasoningResultsPath = laneSubmissionPath(artifactsDir, GATE_LANES.edge_reasoning);
804
+ const continueCommand = nextStepCommand(root, artifactsDir);
805
+ const basePrompt = buildEdgeReasoningPrompt(result.candidates);
806
+ const contentHash = edgeReasoningContentHash(result.candidates);
807
+ // A prior malformed submission was quarantined (not silently destroyed) —
808
+ // name it and the shape error in the re-emitted prompt so the producer
809
+ // fixes the shape instead of resubmitting the same honest mistake forever.
810
+ const rejectionNotice = await renderEdgeReasoningRejectionNotice(artifactsDir);
811
+ // Always-materialized (design resolution 2): the (potentially large)
812
+ // edge-list prompt lives in a lane file on every host — a subagent-capable
813
+ // host fans it out, any other host reads and follows the same file itself.
814
+ // The retired inline `edge_reasoning` step kind was this branch's other
815
+ // arm. Routed through the same lane materializer as every other fan-out
816
+ // step so the K-of-N/result-exists semantics stay single-sourced.
817
+ const fanout = await materializeFanoutLanes({
818
+ artifactsDir,
819
+ runId: AUDIT_GATE_SUBMISSION_SCOPE,
820
+ lanes: [
821
+ {
822
+ id: GATE_LANES.edge_reasoning,
823
+ label: "Edge-reasoning rewrites",
824
+ promptFilename: "edge-reasoning-prompt.md",
825
+ promptText: rejectionNotice
826
+ ? `${basePrompt}\n\n${rejectionNotice}`
827
+ : basePrompt,
687
828
  },
688
- prompt: renderConfirmIntentPrompt(preDigest, {
689
- intentCheckpointPath,
829
+ ],
830
+ });
831
+ const edgeReasoningPromptPath = fanout.lanes[0].promptPath;
832
+ const shortfallLines = renderLaneShortfallLines(fanout.shortfall);
833
+ return currentStepPlan({
834
+ artifactsDir,
835
+ stepKind: "edge_reasoning_dispatch",
836
+ status: "ready",
837
+ runId: null,
838
+ allowedCommands: [continueCommand],
839
+ stopCondition: "Execute the edge-reasoning lane prompt (subagent if available, else yourself), write the rewrites to the results path, then run next-step.",
840
+ repoRoot: root,
841
+ artifactPaths: fanout.artifactPaths,
842
+ prompt: [
843
+ ...shortfallLines,
844
+ renderEdgeReasoningDispatchPrompt({
845
+ promptPath: edgeReasoningPromptPath,
846
+ resultsPath: edgeReasoningResultsPath,
690
847
  continueCommand,
691
- unresolvedConstraintClauses: unresolvedConstraintClauses(result.bundle.intent_checkpoint),
848
+ contentHash,
849
+ candidateCount: result.candidates.length,
692
850
  }),
693
- });
694
- console.log(JSON.stringify(step, null, 2));
695
- return;
696
- }
697
- if (result.kind === "analyzer_consent") {
698
- const decisionsPath = laneSubmissionPath(artifactsDir, GATE_LANES.analyzer_consent);
699
- const continueCommand = nextStepCommand(root, artifactsDir);
700
- const consentPrompt = renderAnalyzerConsentPrompt({
701
- pending: result.pending,
702
- decisionsPath,
703
- continueCommand,
704
- });
705
- const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.analyzer_consent, promptText: consentPrompt }]);
706
- const step = await writeCurrentStep({
707
- artifactsDir,
708
- stepKind: "analyzer_consent",
709
- status: "ready",
710
- runId: null,
711
- allowedCommands: [continueCommand],
712
- stopCondition: "Present the consent offer to the operator, write their decisions to the decisions path, then run next-step.",
713
- repoRoot: root,
714
- artifactPaths: {
715
- analyzer_consent_decisions: decisionsPath,
716
- },
717
- prompt: [...renderLaneShortfallLines(shortfall), consentPrompt].join("\n"),
718
- submissionShortfall: shortfall,
719
- });
720
- console.log(JSON.stringify(step, null, 2));
721
- return;
722
- }
723
- if (result.kind === "analyzer_install") {
724
- const decisionsPath = laneSubmissionPath(artifactsDir, GATE_LANES.analyzer_decisions);
725
- const continueCommand = nextStepCommand(root, artifactsDir);
726
- const installPrompt = renderAnalyzerInstallPrompt({
727
- unresolved: result.unresolved,
728
- decisionsPath,
729
- continueCommand,
730
- });
731
- const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.analyzer_decisions, promptText: installPrompt }]);
732
- const step = await writeCurrentStep({
733
- artifactsDir,
734
- stepKind: "analyzer_install",
735
- status: "ready",
736
- runId: null,
737
- allowedCommands: [continueCommand],
738
- stopCondition: "Write analyzer install decisions to the results path, then run next-step.",
739
- repoRoot: root,
740
- artifactPaths: {
741
- analyzer_decisions: decisionsPath,
742
- },
743
- prompt: [...renderLaneShortfallLines(shortfall), installPrompt].join("\n"),
744
- submissionShortfall: shortfall,
745
- });
746
- console.log(JSON.stringify(step, null, 2));
747
- return;
748
- }
749
- if (result.kind === "edge_reasoning") {
750
- const edgeReasoningResultsPath = laneSubmissionPath(artifactsDir, GATE_LANES.edge_reasoning);
751
- const continueCommand = nextStepCommand(root, artifactsDir);
752
- const basePrompt = buildEdgeReasoningPrompt(result.candidates);
753
- const contentHash = edgeReasoningContentHash(result.candidates);
754
- // A prior malformed submission was quarantined (not silently destroyed) —
755
- // name it and the shape error in the re-emitted prompt so the producer
756
- // fixes the shape instead of resubmitting the same honest mistake forever.
757
- const rejectionNotice = await renderEdgeReasoningRejectionNotice(artifactsDir);
758
- // Always-materialized (design resolution 2): the (potentially large)
759
- // edge-list prompt lives in a lane file on every host — a subagent-capable
760
- // host fans it out, any other host reads and follows the same file itself.
761
- // The retired inline `edge_reasoning` step kind was this branch's other
762
- // arm. Routed through the same lane materializer as every other fan-out
763
- // step so the K-of-N/result-exists semantics stay single-sourced.
764
- const fanout = await materializeFanoutLanes({
765
- artifactsDir,
766
- runId: AUDIT_GATE_SUBMISSION_SCOPE,
767
- lanes: [
768
- {
769
- id: GATE_LANES.edge_reasoning,
770
- label: "Edge-reasoning rewrites",
771
- promptFilename: "edge-reasoning-prompt.md",
772
- promptText: rejectionNotice
773
- ? `${basePrompt}\n\n${rejectionNotice}`
774
- : basePrompt,
775
- },
776
- ],
777
- });
778
- const edgeReasoningPromptPath = fanout.lanes[0].promptPath;
779
- const shortfallLines = renderLaneShortfallLines(fanout.shortfall);
780
- const step = await writeCurrentStep({
781
- artifactsDir,
782
- stepKind: "edge_reasoning_dispatch",
783
- status: "ready",
784
- runId: null,
785
- allowedCommands: [continueCommand],
786
- stopCondition: "Execute the edge-reasoning lane prompt (subagent if available, else yourself), write the rewrites to the results path, then run next-step.",
787
- repoRoot: root,
788
- artifactPaths: fanout.artifactPaths,
789
- prompt: [
790
- ...shortfallLines,
791
- renderEdgeReasoningDispatchPrompt({
792
- promptPath: edgeReasoningPromptPath,
793
- resultsPath: edgeReasoningResultsPath,
794
- continueCommand,
795
- contentHash,
796
- candidateCount: result.candidates.length,
797
- }),
798
- ].join("\n"),
799
- access: {
800
- read_paths: [edgeReasoningPromptPath],
801
- write_paths: [edgeReasoningResultsPath],
802
- },
803
- submissionShortfall: fanout.shortfall,
804
- });
805
- console.log(JSON.stringify(step, null, 2));
806
- return;
807
- }
808
- if (result.kind === "intent_equivalence") {
809
- const verdictPath = laneSubmissionPath(artifactsDir, GATE_LANES.intent_equivalence);
810
- const continueCommand = nextStepCommand(root, artifactsDir);
811
- const status = deriveIntentEquivalenceStatus(result.bundle);
812
- const pending = status.kind === "prose_judgment_pending" ? status : undefined;
813
- const fullPrompt = [
814
- "# Intent-equivalence judgment (bounded)",
815
- "",
816
- "The intent checkpoint's PROSE changed since the planning artifacts derived",
817
- "(structured fields are identical — this is wording only). Judge whether the",
818
- "two prose forms express the SAME audit intent. Judge STRICTLY: any change",
819
- "in scope, emphasis, constraint, or goal — however small — is `changed`.",
820
- "Only pure rephrasing (wording, ordering, formatting) is `equivalent`.",
821
- "An `equivalent` verdict keeps every planning artifact fresh; `changed`",
822
- "re-derives the planning cascade against the new intent.",
823
- "",
824
- "## Prior prose normal form (what planning derived against)",
825
- "",
826
- "```json",
827
- pending?.prior_prose ?? "(unavailable — re-run next-step)",
828
- "```",
829
- "",
830
- "## Current prose normal form",
831
- "",
832
- "```json",
833
- pending?.current_prose ?? "(unavailable — re-run next-step)",
834
- "```",
835
- "",
836
- "## Verdict contract",
837
- "",
838
- "Write EXACTLY this JSON object (no extra fields) to:",
839
- "",
840
- ` ${verdictPath}`,
841
- "",
842
- "```json",
843
- JSON.stringify({
844
- verdict: "equivalent | changed",
845
- judged_pair: {
846
- prior_hash: pending?.prior_hash ?? "",
847
- new_hash: pending?.new_hash ?? "",
848
- },
849
- }, null, 2),
850
- "```",
851
- "",
852
- "`judged_pair` must carry the two hashes shown above verbatim — they bind",
853
- "the verdict to this exact pair; a checkpoint edited again mid-judgment is",
854
- "detected and re-judged.",
855
- "",
856
- `Then run: ${continueCommand}`,
857
- "",
858
- ].join("\n");
859
- const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.intent_equivalence, promptText: fullPrompt }]);
860
- const step = await writeCurrentStep({
861
- artifactsDir,
862
- stepKind: "intent_equivalence",
863
- status: "ready",
864
- runId: null,
865
- allowedCommands: [continueCommand],
866
- stopCondition: "Write the equivalence verdict to the results path, then run next-step.",
867
- repoRoot: root,
868
- artifactPaths: {
869
- intent_equivalence_verdict: verdictPath,
870
- },
871
- prompt: [...renderLaneShortfallLines(shortfall), fullPrompt].join("\n"),
872
- access: {
873
- read_paths: [],
874
- write_paths: [verdictPath],
851
+ ].join("\n"),
852
+ access: {
853
+ read_paths: [edgeReasoningPromptPath],
854
+ write_paths: [edgeReasoningResultsPath],
855
+ },
856
+ submissionShortfall: fanout.shortfall,
857
+ });
858
+ });
859
+ const emitIntentEquivalence = emissionRow(async ({ root, artifactsDir }, result) => {
860
+ const verdictPath = laneSubmissionPath(artifactsDir, GATE_LANES.intent_equivalence);
861
+ const continueCommand = nextStepCommand(root, artifactsDir);
862
+ const status = deriveIntentEquivalenceStatus(result.bundle);
863
+ const pending = status.kind === "prose_judgment_pending" ? status : undefined;
864
+ const fullPrompt = [
865
+ "# Intent-equivalence judgment (bounded)",
866
+ "",
867
+ "The intent checkpoint's PROSE changed since the planning artifacts derived",
868
+ "(structured fields are identical — this is wording only). Judge whether the",
869
+ "two prose forms express the SAME audit intent. Judge STRICTLY: any change",
870
+ "in scope, emphasis, constraint, or goal however small is `changed`.",
871
+ "Only pure rephrasing (wording, ordering, formatting) is `equivalent`.",
872
+ "An `equivalent` verdict keeps every planning artifact fresh; `changed`",
873
+ "re-derives the planning cascade against the new intent.",
874
+ "",
875
+ "## Prior prose normal form (what planning derived against)",
876
+ "",
877
+ "```json",
878
+ pending?.prior_prose ?? "(unavailable re-run next-step)",
879
+ "```",
880
+ "",
881
+ "## Current prose normal form",
882
+ "",
883
+ "```json",
884
+ pending?.current_prose ?? "(unavailable — re-run next-step)",
885
+ "```",
886
+ "",
887
+ "## Verdict contract",
888
+ "",
889
+ "Write EXACTLY this JSON object (no extra fields) to:",
890
+ "",
891
+ ` ${verdictPath}`,
892
+ "",
893
+ "```json",
894
+ JSON.stringify({
895
+ verdict: "equivalent | changed",
896
+ judged_pair: {
897
+ prior_hash: pending?.prior_hash ?? "",
898
+ new_hash: pending?.new_hash ?? "",
875
899
  },
876
- submissionShortfall: shortfall,
877
- });
878
- console.log(JSON.stringify(step, null, 2));
879
- return;
880
- }
881
- if (result.kind === "critical_flow_fallback") {
882
- const fallbackResultsPath = laneSubmissionPath(artifactsDir, GATE_LANES.critical_flow_fallback);
883
- const continueCommand = nextStepCommand(root, artifactsDir);
884
- const basePrompt = result.bundle.critical_flows
885
- ? renderCriticalFlowFallbackPrompt(result.bundle.critical_flows)
886
- : "# Critical-flow fallback\n\nNo critical_flows manifest is available; write an empty flows array.";
887
- // Always-materialized (design resolution 2): the (potentially ~340-line)
888
- // flow-stub prompt is a lane FILE, never inlined into the step prompt. The
889
- // results-path section is the lane materializer's, not this emitter's.
890
- const lanePrompt = basePrompt;
891
- const fanout = await materializeFanoutLanes({
892
- artifactsDir,
893
- runId: AUDIT_GATE_SUBMISSION_SCOPE,
894
- lanes: [
895
- {
896
- id: GATE_LANES.critical_flow_fallback,
897
- label: "Critical-flow fallback enrichment",
898
- promptFilename: "critical-flow-fallback-prompt.md",
899
- promptText: lanePrompt,
900
- },
901
- ],
902
- });
903
- const step = await writeCurrentStep({
904
- artifactsDir,
905
- stepKind: "critical_flow_fallback",
906
- status: "ready",
907
- runId: null,
908
- allowedCommands: [continueCommand],
909
- stopCondition: "Execute the critical-flow lane prompt (subagent if available, else yourself), write the enrichment to the results path, then run next-step.",
910
- repoRoot: root,
911
- artifactPaths: fanout.artifactPaths,
912
- prompt: [
913
- "# audit-code critical-flow fallback",
914
- "",
915
- ...renderLaneShortfallLines(fanout.shortfall),
916
- ...renderFanoutExecutionLines({
917
- lanes: fanout.pendingLanes.map((lane) => ({
918
- label: lane.label,
919
- promptPath: lane.promptPath,
920
- })),
921
- }),
922
- "",
923
- "The executor must write the CriticalFlowFallbackResult JSON object to:",
924
- "",
925
- ` ${fallbackResultsPath}`,
926
- "",
927
- "When the result file exists, run:",
928
- "",
929
- ` ${continueCommand}`,
930
- "",
931
- "Read and follow only the new step prompt returned by that command.",
932
- "",
933
- ].join("\n"),
934
- access: {
935
- read_paths: fanout.readPaths,
936
- write_paths: fanout.writePaths,
900
+ }, null, 2),
901
+ "```",
902
+ "",
903
+ "`judged_pair` must carry the two hashes shown above verbatim — they bind",
904
+ "the verdict to this exact pair; a checkpoint edited again mid-judgment is",
905
+ "detected and re-judged.",
906
+ "",
907
+ `Then run: ${continueCommand}`,
908
+ "",
909
+ ].join("\n");
910
+ const shortfall = await recordExpectedLanes(artifactsDir, AUDIT_GATE_SUBMISSION_SCOPE, [{ lane: GATE_LANES.intent_equivalence, promptText: fullPrompt }]);
911
+ return currentStepPlan({
912
+ artifactsDir,
913
+ stepKind: "intent_equivalence",
914
+ status: "ready",
915
+ runId: null,
916
+ allowedCommands: [continueCommand],
917
+ stopCondition: "Write the equivalence verdict to the results path, then run next-step.",
918
+ repoRoot: root,
919
+ artifactPaths: {
920
+ intent_equivalence_verdict: verdictPath,
921
+ },
922
+ prompt: [...renderLaneShortfallLines(shortfall), fullPrompt].join("\n"),
923
+ access: {
924
+ read_paths: [],
925
+ write_paths: [verdictPath],
926
+ },
927
+ submissionShortfall: shortfall,
928
+ });
929
+ });
930
+ const emitCriticalFlowFallback = emissionRow(async ({ root, artifactsDir }, result) => {
931
+ const fallbackResultsPath = laneSubmissionPath(artifactsDir, GATE_LANES.critical_flow_fallback);
932
+ const continueCommand = nextStepCommand(root, artifactsDir);
933
+ const basePrompt = result.bundle.critical_flows
934
+ ? renderCriticalFlowFallbackPrompt(result.bundle.critical_flows)
935
+ : "# Critical-flow fallback\n\nNo critical_flows manifest is available; write an empty flows array.";
936
+ // Always-materialized (design resolution 2): the (potentially ~340-line)
937
+ // flow-stub prompt is a lane FILE, never inlined into the step prompt. The
938
+ // results-path section is the lane materializer's, not this emitter's.
939
+ const lanePrompt = basePrompt;
940
+ const fanout = await materializeFanoutLanes({
941
+ artifactsDir,
942
+ runId: AUDIT_GATE_SUBMISSION_SCOPE,
943
+ lanes: [
944
+ {
945
+ id: GATE_LANES.critical_flow_fallback,
946
+ label: "Critical-flow fallback enrichment",
947
+ promptFilename: "critical-flow-fallback-prompt.md",
948
+ promptText: lanePrompt,
937
949
  },
938
- submissionShortfall: fanout.shortfall,
939
- });
940
- console.log(JSON.stringify(step, null, 2));
941
- return;
942
- }
943
- if (result.kind === "synthesis_narrative") {
944
- const narrativeResultsPath = laneSubmissionPath(artifactsDir, GATE_LANES.synthesis_narrative);
945
- const continueCommand = nextStepCommand(root, artifactsDir);
946
- const basePrompt = result.bundle.audit_findings
947
- ? renderSynthesisNarrativePrompt(result.bundle.audit_findings)
948
- : "# Synthesis narrative\n\nNo findings report is available; write an empty themes array.";
949
- // Always-materialized (design resolution 2): the findings digest (up to 120
950
- // findings) is a lane FILE, never inlined into the step prompt. This step
951
- // previously carried no access block at all — the lane form declares one.
952
- // The results-path section is the lane materializer's, not this emitter's.
953
- const lanePrompt = basePrompt;
954
- const fanout = await materializeFanoutLanes({
955
- artifactsDir,
956
- runId: AUDIT_GATE_SUBMISSION_SCOPE,
957
- lanes: [
958
- {
959
- id: GATE_LANES.synthesis_narrative,
960
- label: "Synthesis narrative (themes / exec summary / top risks)",
961
- promptFilename: "synthesis-narrative-prompt.md",
962
- promptText: lanePrompt,
963
- },
964
- ],
965
- });
966
- const step = await writeCurrentStep({
967
- artifactsDir,
968
- stepKind: "synthesis_narrative",
969
- status: "ready",
970
- runId: null,
971
- allowedCommands: [continueCommand],
972
- stopCondition: "Execute the synthesis-narrative lane prompt (subagent if available, else yourself), write the narrative to the results path, then run next-step.",
973
- repoRoot: root,
974
- artifactPaths: fanout.artifactPaths,
975
- prompt: [
976
- "# audit-code synthesis narrative",
977
- "",
978
- ...renderLaneShortfallLines(fanout.shortfall),
979
- ...renderFanoutExecutionLines({
980
- lanes: fanout.pendingLanes.map((lane) => ({
981
- label: lane.label,
982
- promptPath: lane.promptPath,
983
- })),
984
- }),
985
- "",
986
- "The executor must write the SynthesisNarrative JSON object to:",
987
- "",
988
- ` ${narrativeResultsPath}`,
989
- "",
990
- "When the result file exists, run:",
991
- "",
992
- ` ${continueCommand}`,
993
- "",
994
- "Read and follow only the new step prompt returned by that command.",
995
- "",
996
- ].join("\n"),
997
- access: {
998
- read_paths: fanout.readPaths,
999
- write_paths: fanout.writePaths,
950
+ ],
951
+ });
952
+ return currentStepPlan({
953
+ artifactsDir,
954
+ stepKind: "critical_flow_fallback",
955
+ status: "ready",
956
+ runId: null,
957
+ allowedCommands: [continueCommand],
958
+ stopCondition: "Execute the critical-flow lane prompt (subagent if available, else yourself), write the enrichment to the results path, then run next-step.",
959
+ repoRoot: root,
960
+ artifactPaths: fanout.artifactPaths,
961
+ prompt: [
962
+ "# audit-code critical-flow fallback",
963
+ "",
964
+ ...renderLaneShortfallLines(fanout.shortfall),
965
+ ...renderFanoutExecutionLines({
966
+ lanes: fanout.pendingLanes.map((lane) => ({
967
+ label: lane.label,
968
+ promptPath: lane.promptPath,
969
+ })),
970
+ }),
971
+ "",
972
+ "The executor must write the CriticalFlowFallbackResult JSON object to:",
973
+ "",
974
+ ` ${fallbackResultsPath}`,
975
+ "",
976
+ "When the result file exists, run:",
977
+ "",
978
+ ` ${continueCommand}`,
979
+ "",
980
+ "Read and follow only the new step prompt returned by that command.",
981
+ "",
982
+ ].join("\n"),
983
+ access: {
984
+ read_paths: fanout.readPaths,
985
+ write_paths: fanout.writePaths,
986
+ },
987
+ submissionShortfall: fanout.shortfall,
988
+ });
989
+ });
990
+ const emitSynthesisNarrative = emissionRow(async ({ root, artifactsDir }, result) => {
991
+ const narrativeResultsPath = laneSubmissionPath(artifactsDir, GATE_LANES.synthesis_narrative);
992
+ const continueCommand = nextStepCommand(root, artifactsDir);
993
+ const basePrompt = result.bundle.audit_findings
994
+ ? renderSynthesisNarrativePrompt(result.bundle.audit_findings)
995
+ : "# Synthesis narrative\n\nNo findings report is available; write an empty themes array.";
996
+ // Always-materialized (design resolution 2): the findings digest (up to 120
997
+ // findings) is a lane FILE, never inlined into the step prompt. This step
998
+ // previously carried no access block at all — the lane form declares one.
999
+ // The results-path section is the lane materializer's, not this emitter's.
1000
+ const lanePrompt = basePrompt;
1001
+ const fanout = await materializeFanoutLanes({
1002
+ artifactsDir,
1003
+ runId: AUDIT_GATE_SUBMISSION_SCOPE,
1004
+ lanes: [
1005
+ {
1006
+ id: GATE_LANES.synthesis_narrative,
1007
+ label: "Synthesis narrative (themes / exec summary / top risks)",
1008
+ promptFilename: "synthesis-narrative-prompt.md",
1009
+ promptText: lanePrompt,
1000
1010
  },
1001
- submissionShortfall: fanout.shortfall,
1002
- });
1003
- console.log(JSON.stringify(step, null, 2));
1004
- return;
1005
- }
1006
- const step = await renderSemanticReviewStep({
1007
- root,
1011
+ ],
1012
+ });
1013
+ return currentStepPlan({
1008
1014
  artifactsDir,
1009
- activeReviewRun: result.activeReviewRun,
1010
- selectedExecutor: result.selectedExecutor,
1011
- inProcessMadeProgress: result.inProcessMadeProgress,
1012
- ingestIssues: result.ingestIssues,
1015
+ stepKind: "synthesis_narrative",
1016
+ status: "ready",
1017
+ runId: null,
1018
+ allowedCommands: [continueCommand],
1019
+ stopCondition: "Execute the synthesis-narrative lane prompt (subagent if available, else yourself), write the narrative to the results path, then run next-step.",
1020
+ repoRoot: root,
1021
+ artifactPaths: fanout.artifactPaths,
1022
+ prompt: [
1023
+ "# audit-code synthesis narrative",
1024
+ "",
1025
+ ...renderLaneShortfallLines(fanout.shortfall),
1026
+ ...renderFanoutExecutionLines({
1027
+ lanes: fanout.pendingLanes.map((lane) => ({
1028
+ label: lane.label,
1029
+ promptPath: lane.promptPath,
1030
+ })),
1031
+ }),
1032
+ "",
1033
+ "The executor must write the SynthesisNarrative JSON object to:",
1034
+ "",
1035
+ ` ${narrativeResultsPath}`,
1036
+ "",
1037
+ "When the result file exists, run:",
1038
+ "",
1039
+ ` ${continueCommand}`,
1040
+ "",
1041
+ "Read and follow only the new step prompt returned by that command.",
1042
+ "",
1043
+ ].join("\n"),
1044
+ access: {
1045
+ read_paths: fanout.readPaths,
1046
+ write_paths: fanout.writePaths,
1047
+ },
1048
+ submissionShortfall: fanout.shortfall,
1013
1049
  });
1014
- console.log(JSON.stringify(step, null, 2));
1015
- }
1050
+ });
1051
+ /**
1052
+ * The step-emission dispatch table: exactly one row per host-actionable result
1053
+ * kind. Typed as a TOTAL record over the kind union, so a kind added upstream
1054
+ * with no row here is a compile error rather than a step that silently falls
1055
+ * through to the semantic-review dispatch.
1056
+ *
1057
+ * Exported together with the handled-kinds set below so a drift guard imports
1058
+ * the real thing instead of reconstructing it by reading a branch chain.
1059
+ */
1060
+ export const NEXT_STEP_EMISSION_TABLE = {
1061
+ complete: emitComplete,
1062
+ blocked: emitBlocked,
1063
+ design_review_parallel: emitDesignReviewParallel,
1064
+ design_review_contract: emitDesignReviewContract,
1065
+ design_review_conceptual: emitDesignReviewConceptual,
1066
+ charter_extraction: emitCharterExtraction,
1067
+ charter_delta: emitCharterDelta,
1068
+ charter_clarification: emitCharterClarification,
1069
+ systemic_challenge: emitSystemicChallenge,
1070
+ confirm_intent: emitConfirmIntent,
1071
+ analyzer_consent: emitAnalyzerConsent,
1072
+ analyzer_install: emitAnalyzerInstall,
1073
+ edge_reasoning: emitEdgeReasoning,
1074
+ intent_equivalence: emitIntentEquivalence,
1075
+ critical_flow_fallback: emitCriticalFlowFallback,
1076
+ synthesis_narrative: emitSynthesisNarrative,
1077
+ };
1078
+ /**
1079
+ * The one emission site for `audit-code next-step`. Rows return plans; this
1080
+ * writes and logs exactly once, whichever row (or the fallback) produced it.
1081
+ */
1082
+ const NEXT_STEP_EMISSION = createStepEmissionScaffold({
1083
+ table: NEXT_STEP_EMISSION_TABLE,
1084
+ // `semantic_review` is deliberately the FALLBACK rather than a row: the
1085
+ // validation boundary here has always been "anything unmatched is a
1086
+ // semantic-review dispatch", never an error on an unrecognized kind.
1087
+ fallback: (ctx) => {
1088
+ const result = ctx.result;
1089
+ return semanticReviewPlan({
1090
+ root: ctx.root,
1091
+ artifactsDir: ctx.artifactsDir,
1092
+ activeReviewRun: result.activeReviewRun,
1093
+ selectedExecutor: result.selectedExecutor,
1094
+ inProcessMadeProgress: result.inProcessMadeProgress,
1095
+ ingestIssues: result.ingestIssues,
1096
+ });
1097
+ },
1098
+ write: writeAuditStep,
1099
+ // The tool's only externally-observable per-invocation contract.
1100
+ log: (step) => {
1101
+ console.log(JSON.stringify(step, null, 2));
1102
+ },
1103
+ });
1104
+ /**
1105
+ * The handled-kinds set, DERIVED from the table's own keys — never a second,
1106
+ * hand-listed copy that could disagree with the table it describes. A drift
1107
+ * guard consumes this as the real B side of its seam assertion.
1108
+ */
1109
+ export const NEXT_STEP_EMISSION_KINDS = NEXT_STEP_EMISSION.handledKeys;
1016
1110
  //# sourceMappingURL=nextStepCommand.js.map