audit-tools 0.52.3 → 0.52.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/README.md +31 -1
  2. package/dist/audit/cli/args.d.ts.map +1 -1
  3. package/dist/audit/cli/args.js +6 -2
  4. package/dist/audit/cli/args.js.map +1 -1
  5. package/dist/audit/cli/argumentContract.d.ts +11 -0
  6. package/dist/audit/cli/argumentContract.d.ts.map +1 -0
  7. package/dist/audit/cli/argumentContract.js +45 -0
  8. package/dist/audit/cli/argumentContract.js.map +1 -0
  9. package/dist/audit/cli/auditReviewBindings.d.ts +49 -0
  10. package/dist/audit/cli/auditReviewBindings.d.ts.map +1 -0
  11. package/dist/audit/cli/auditReviewBindings.js +61 -0
  12. package/dist/audit/cli/auditReviewBindings.js.map +1 -0
  13. package/dist/audit/cli/auditStep.d.ts +1 -27
  14. package/dist/audit/cli/auditStep.d.ts.map +1 -1
  15. package/dist/audit/cli/auditStep.js +7 -32
  16. package/dist/audit/cli/auditStep.js.map +1 -1
  17. package/dist/audit/cli/charterComparisonPrompt.d.ts +2 -0
  18. package/dist/audit/cli/charterComparisonPrompt.d.ts.map +1 -1
  19. package/dist/audit/cli/charterComparisonPrompt.js +9 -0
  20. package/dist/audit/cli/charterComparisonPrompt.js.map +1 -1
  21. package/dist/audit/cli/charterExtractionPrompt.d.ts.map +1 -1
  22. package/dist/audit/cli/charterExtractionPrompt.js +17 -3
  23. package/dist/audit/cli/charterExtractionPrompt.js.map +1 -1
  24. package/dist/audit/cli/conceptualDispatch.d.ts +1 -1
  25. package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
  26. package/dist/audit/cli/conceptualDispatch.js +15 -2
  27. package/dist/audit/cli/conceptualDispatch.js.map +1 -1
  28. package/dist/audit/cli/confirmIntentStep.d.ts.map +1 -1
  29. package/dist/audit/cli/confirmIntentStep.js +10 -17
  30. package/dist/audit/cli/confirmIntentStep.js.map +1 -1
  31. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  32. package/dist/audit/cli/dispatch/hostHandoff.js +231 -229
  33. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  34. package/dist/audit/cli/explainTaskCommand.d.ts.map +1 -1
  35. package/dist/audit/cli/explainTaskCommand.js +3 -1
  36. package/dist/audit/cli/explainTaskCommand.js.map +1 -1
  37. package/dist/audit/cli/fanoutLanes.d.ts +10 -14
  38. package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
  39. package/dist/audit/cli/fanoutLanes.js +50 -16
  40. package/dist/audit/cli/fanoutLanes.js.map +1 -1
  41. package/dist/audit/cli/functionalPreflight.d.ts +96 -0
  42. package/dist/audit/cli/functionalPreflight.d.ts.map +1 -0
  43. package/dist/audit/cli/functionalPreflight.js +103 -0
  44. package/dist/audit/cli/functionalPreflight.js.map +1 -0
  45. package/dist/audit/cli/laneSubmissions.d.ts +5 -1
  46. package/dist/audit/cli/laneSubmissions.d.ts.map +1 -1
  47. package/dist/audit/cli/laneSubmissions.js +45 -34
  48. package/dist/audit/cli/laneSubmissions.js.map +1 -1
  49. package/dist/audit/cli/nextStepCommand.d.ts +1 -9
  50. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  51. package/dist/audit/cli/nextStepCommand.js +101 -38
  52. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  53. package/dist/audit/cli/nextStepHelpers.d.ts +10 -25
  54. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  55. package/dist/audit/cli/nextStepHelpers.js +57 -67
  56. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  57. package/dist/audit/cli/prompts.d.ts.map +1 -1
  58. package/dist/audit/cli/prompts.js +9 -4
  59. package/dist/audit/cli/prompts.js.map +1 -1
  60. package/dist/audit/cli/recoverSubmissionCommand.d.ts.map +1 -1
  61. package/dist/audit/cli/recoverSubmissionCommand.js +8 -1
  62. package/dist/audit/cli/recoverSubmissionCommand.js.map +1 -1
  63. package/dist/audit/cli/reviewRun.js +1 -1
  64. package/dist/audit/cli/reviewRun.js.map +1 -1
  65. package/dist/audit/cli/reviewSubmission.d.ts +24 -0
  66. package/dist/audit/cli/reviewSubmission.d.ts.map +1 -0
  67. package/dist/audit/cli/reviewSubmission.js +97 -0
  68. package/dist/audit/cli/reviewSubmission.js.map +1 -0
  69. package/dist/audit/cli/sampleRunCommand.d.ts +3 -3
  70. package/dist/audit/cli/steps.d.ts +18 -18
  71. package/dist/audit/cli/steps.d.ts.map +1 -1
  72. package/dist/audit/cli/steps.js +1 -0
  73. package/dist/audit/cli/steps.js.map +1 -1
  74. package/dist/audit/cli/unacceptResultsCommand.d.ts.map +1 -1
  75. package/dist/audit/cli/unacceptResultsCommand.js +2 -1
  76. package/dist/audit/cli/unacceptResultsCommand.js.map +1 -1
  77. package/dist/audit/cli.d.ts +2 -1
  78. package/dist/audit/cli.d.ts.map +1 -1
  79. package/dist/audit/cli.js +27 -19
  80. package/dist/audit/cli.js.map +1 -1
  81. package/dist/audit/contracts/workerSchemas.d.ts +10 -10
  82. package/dist/audit/contracts/wrapperResponse.d.ts +34 -34
  83. package/dist/audit/extractors/commentDecomposition.d.ts +14 -0
  84. package/dist/audit/extractors/commentDecomposition.d.ts.map +1 -1
  85. package/dist/audit/extractors/commentDecomposition.js +2 -1
  86. package/dist/audit/extractors/commentDecomposition.js.map +1 -1
  87. package/dist/audit/extractors/graph.d.ts +1 -1
  88. package/dist/audit/extractors/graph.d.ts.map +1 -1
  89. package/dist/audit/extractors/graph.js +12 -7
  90. package/dist/audit/extractors/graph.js.map +1 -1
  91. package/dist/audit/extractors/graphPythonImports.d.ts +3 -0
  92. package/dist/audit/extractors/graphPythonImports.d.ts.map +1 -1
  93. package/dist/audit/extractors/graphPythonImports.js +24 -5
  94. package/dist/audit/extractors/graphPythonImports.js.map +1 -1
  95. package/dist/audit/extractors/graphRouteSources.d.ts +7 -0
  96. package/dist/audit/extractors/graphRouteSources.d.ts.map +1 -0
  97. package/dist/audit/extractors/graphRouteSources.js +208 -0
  98. package/dist/audit/extractors/graphRouteSources.js.map +1 -0
  99. package/dist/audit/extractors/graphRoutes.d.ts +1 -1
  100. package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
  101. package/dist/audit/extractors/graphRoutes.js +65 -68
  102. package/dist/audit/extractors/graphRoutes.js.map +1 -1
  103. package/dist/audit/io/artifacts.d.ts +1 -0
  104. package/dist/audit/io/artifacts.d.ts.map +1 -1
  105. package/dist/audit/io/artifacts.js +29 -10
  106. package/dist/audit/io/artifacts.js.map +1 -1
  107. package/dist/audit/io/runArtifacts.d.ts +1 -1
  108. package/dist/audit/io/runArtifacts.d.ts.map +1 -1
  109. package/dist/audit/io/runArtifacts.js +3 -0
  110. package/dist/audit/io/runArtifacts.js.map +1 -1
  111. package/dist/audit/io/toolingManifest.d.ts.map +1 -1
  112. package/dist/audit/io/toolingManifest.js +11 -32
  113. package/dist/audit/io/toolingManifest.js.map +1 -1
  114. package/dist/audit/orchestrator/acquisitionExecutor.d.ts +2 -3
  115. package/dist/audit/orchestrator/acquisitionExecutor.d.ts.map +1 -1
  116. package/dist/audit/orchestrator/acquisitionExecutor.js.map +1 -1
  117. package/dist/audit/orchestrator/advanceTypes.d.ts +1 -2
  118. package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
  119. package/dist/audit/orchestrator/autoFixExecutor.d.ts +2 -2
  120. package/dist/audit/orchestrator/autoFixExecutor.d.ts.map +1 -1
  121. package/dist/audit/orchestrator/autoFixExecutor.js +2 -2
  122. package/dist/audit/orchestrator/autoFixExecutor.js.map +1 -1
  123. package/dist/audit/orchestrator/charterFidelityPacket.d.ts +2 -0
  124. package/dist/audit/orchestrator/charterFidelityPacket.d.ts.map +1 -1
  125. package/dist/audit/orchestrator/charterFidelityPacket.js +6 -1
  126. package/dist/audit/orchestrator/charterFidelityPacket.js.map +1 -1
  127. package/dist/audit/orchestrator/charterPackets.d.ts +2 -0
  128. package/dist/audit/orchestrator/charterPackets.d.ts.map +1 -1
  129. package/dist/audit/orchestrator/charterPackets.js +6 -0
  130. package/dist/audit/orchestrator/charterPackets.js.map +1 -1
  131. package/dist/audit/orchestrator/designReviewProjection.d.ts +5 -8
  132. package/dist/audit/orchestrator/designReviewProjection.d.ts.map +1 -1
  133. package/dist/audit/orchestrator/designReviewProjection.js +7 -1
  134. package/dist/audit/orchestrator/designReviewProjection.js.map +1 -1
  135. package/dist/audit/orchestrator/designReviewPrompt.d.ts +3 -47
  136. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  137. package/dist/audit/orchestrator/designReviewPrompt.js +28 -135
  138. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  139. package/dist/audit/orchestrator/designReviewSnapshot.d.ts +24 -1
  140. package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
  141. package/dist/audit/orchestrator/designReviewSnapshot.js +8 -2
  142. package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
  143. package/dist/audit/orchestrator/designReviewTask.d.ts +46 -0
  144. package/dist/audit/orchestrator/designReviewTask.d.ts.map +1 -0
  145. package/dist/audit/orchestrator/designReviewTask.js +44 -0
  146. package/dist/audit/orchestrator/designReviewTask.js.map +1 -0
  147. package/dist/audit/orchestrator/intentCheckpointGate.d.ts +1 -1
  148. package/dist/audit/orchestrator/intentCheckpointGate.d.ts.map +1 -1
  149. package/dist/audit/orchestrator/intentCheckpointGate.js +4 -2
  150. package/dist/audit/orchestrator/intentCheckpointGate.js.map +1 -1
  151. package/dist/audit/orchestrator/intentEquivalenceExecutor.d.ts +3 -0
  152. package/dist/audit/orchestrator/intentEquivalenceExecutor.d.ts.map +1 -1
  153. package/dist/audit/orchestrator/intentEquivalenceExecutor.js +2 -0
  154. package/dist/audit/orchestrator/intentEquivalenceExecutor.js.map +1 -1
  155. package/dist/audit/orchestrator/intentInterpreter.d.ts +1 -9
  156. package/dist/audit/orchestrator/intentInterpreter.d.ts.map +1 -1
  157. package/dist/audit/orchestrator/intentInterpreter.js +6 -11
  158. package/dist/audit/orchestrator/intentInterpreter.js.map +1 -1
  159. package/dist/audit/orchestrator/selectiveDeepening/shared.d.ts +2 -2
  160. package/dist/audit/orchestrator/selectiveDeepening/shared.d.ts.map +1 -1
  161. package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
  162. package/dist/audit/orchestrator/staleness.js +32 -40
  163. package/dist/audit/orchestrator/staleness.js.map +1 -1
  164. package/dist/audit/orchestrator/state.d.ts.map +1 -1
  165. package/dist/audit/orchestrator/state.js +8 -14
  166. package/dist/audit/orchestrator/state.js.map +1 -1
  167. package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts +19 -0
  168. package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts.map +1 -1
  169. package/dist/audit/orchestrator/systemicChallengeExecutor.js +15 -1
  170. package/dist/audit/orchestrator/systemicChallengeExecutor.js.map +1 -1
  171. package/dist/audit/reporting/criticalFlowFallbackPrompt.d.ts +1 -1
  172. package/dist/audit/reporting/criticalFlowFallbackPrompt.d.ts.map +1 -1
  173. package/dist/audit/reporting/criticalFlowFallbackPrompt.js +4 -2
  174. package/dist/audit/reporting/criticalFlowFallbackPrompt.js.map +1 -1
  175. package/dist/audit/reporting/mergeFindings.d.ts.map +1 -1
  176. package/dist/audit/reporting/mergeFindings.js +43 -2
  177. package/dist/audit/reporting/mergeFindings.js.map +1 -1
  178. package/dist/audit/types/auditScope.d.ts +2 -2
  179. package/dist/audit/types/auditState.d.ts +6 -6
  180. package/dist/audit/types/conceptualAdjudication.d.ts +19 -19
  181. package/dist/audit/types/conceptualAdjudication.d.ts.map +1 -1
  182. package/dist/audit/types/conceptualAdjudication.js +3 -3
  183. package/dist/audit/types/conceptualAdjudication.js.map +1 -1
  184. package/dist/audit/types.d.ts +17 -19
  185. package/dist/audit/types.d.ts.map +1 -1
  186. package/dist/audit/types.js +19 -20
  187. package/dist/audit/types.js.map +1 -1
  188. package/dist/remediate/contractPipeline/artifactStore.d.ts +3 -1
  189. package/dist/remediate/contractPipeline/artifactStore.d.ts.map +1 -1
  190. package/dist/remediate/contractPipeline/artifactStore.js +26 -31
  191. package/dist/remediate/contractPipeline/artifactStore.js.map +1 -1
  192. package/dist/remediate/contractPipeline/changeClassification.d.ts.map +1 -1
  193. package/dist/remediate/contractPipeline/changeClassification.js +20 -8
  194. package/dist/remediate/contractPipeline/changeClassification.js.map +1 -1
  195. package/dist/remediate/contractPipeline/contractReviewBinding.d.ts +28 -0
  196. package/dist/remediate/contractPipeline/contractReviewBinding.d.ts.map +1 -0
  197. package/dist/remediate/contractPipeline/contractReviewBinding.js +75 -0
  198. package/dist/remediate/contractPipeline/contractReviewBinding.js.map +1 -0
  199. package/dist/remediate/contractPipeline/repairState.d.ts +3 -1
  200. package/dist/remediate/contractPipeline/repairState.d.ts.map +1 -1
  201. package/dist/remediate/contractPipeline/repairState.js +1 -0
  202. package/dist/remediate/contractPipeline/repairState.js.map +1 -1
  203. package/dist/remediate/contractPipeline/sketchSource.d.ts +1 -0
  204. package/dist/remediate/contractPipeline/sketchSource.d.ts.map +1 -1
  205. package/dist/remediate/contractPipeline/sketchSource.js +1 -0
  206. package/dist/remediate/contractPipeline/sketchSource.js.map +1 -1
  207. package/dist/remediate/dedup/crossLensDedup.d.ts +1 -2
  208. package/dist/remediate/dedup/crossLensDedup.d.ts.map +1 -1
  209. package/dist/remediate/dedup/crossLensDedup.js +1 -0
  210. package/dist/remediate/dedup/crossLensDedup.js.map +1 -1
  211. package/dist/remediate/index.d.ts.map +1 -1
  212. package/dist/remediate/index.js +92 -52
  213. package/dist/remediate/index.js.map +1 -1
  214. package/dist/remediate/intake.d.ts +54 -0
  215. package/dist/remediate/intake.d.ts.map +1 -1
  216. package/dist/remediate/intake.js +15 -1
  217. package/dist/remediate/intake.js.map +1 -1
  218. package/dist/remediate/intakeSelection.d.ts +27 -0
  219. package/dist/remediate/intakeSelection.d.ts.map +1 -0
  220. package/dist/remediate/intakeSelection.js +37 -0
  221. package/dist/remediate/intakeSelection.js.map +1 -0
  222. package/dist/remediate/intent/intentPersistence.d.ts +41 -0
  223. package/dist/remediate/intent/intentPersistence.d.ts.map +1 -0
  224. package/dist/remediate/intent/intentPersistence.js +129 -0
  225. package/dist/remediate/intent/intentPersistence.js.map +1 -0
  226. package/dist/remediate/phases/close.d.ts +2 -1
  227. package/dist/remediate/phases/close.d.ts.map +1 -1
  228. package/dist/remediate/phases/close.js +19 -2
  229. package/dist/remediate/phases/close.js.map +1 -1
  230. package/dist/remediate/phases/closeReviewProvenance.d.ts +4 -0
  231. package/dist/remediate/phases/closeReviewProvenance.d.ts.map +1 -0
  232. package/dist/remediate/phases/closeReviewProvenance.js +16 -0
  233. package/dist/remediate/phases/closeReviewProvenance.js.map +1 -0
  234. package/dist/remediate/phases/triage.d.ts.map +1 -1
  235. package/dist/remediate/phases/triage.js +3 -1
  236. package/dist/remediate/phases/triage.js.map +1 -1
  237. package/dist/remediate/review/filterDispositions.d.ts +13 -0
  238. package/dist/remediate/review/filterDispositions.d.ts.map +1 -0
  239. package/dist/remediate/review/filterDispositions.js +32 -0
  240. package/dist/remediate/review/filterDispositions.js.map +1 -0
  241. package/dist/remediate/state/runIdentity.d.ts +27 -0
  242. package/dist/remediate/state/runIdentity.d.ts.map +1 -0
  243. package/dist/remediate/state/runIdentity.js +38 -0
  244. package/dist/remediate/state/runIdentity.js.map +1 -0
  245. package/dist/remediate/state/store.d.ts +80 -1
  246. package/dist/remediate/state/store.d.ts.map +1 -1
  247. package/dist/remediate/state/store.js +82 -1
  248. package/dist/remediate/state/store.js.map +1 -1
  249. package/dist/remediate/state/types.d.ts +215 -42
  250. package/dist/remediate/state/types.d.ts.map +1 -1
  251. package/dist/remediate/state/types.js +11 -3
  252. package/dist/remediate/state/types.js.map +1 -1
  253. package/dist/remediate/steps/contractPipeline.d.ts +61 -3
  254. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  255. package/dist/remediate/steps/contractPipeline.js +121 -47
  256. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  257. package/dist/remediate/steps/contractPipelinePrompts.d.ts +27 -4
  258. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  259. package/dist/remediate/steps/contractPipelinePrompts.js +34 -32
  260. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  261. package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts +31 -0
  262. package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts.map +1 -0
  263. package/dist/remediate/steps/dispatch/contractConformanceReview.js +98 -0
  264. package/dist/remediate/steps/dispatch/contractConformanceReview.js.map +1 -0
  265. package/dist/remediate/steps/dispatch/hostContracts.d.ts +133 -0
  266. package/dist/remediate/steps/dispatch/hostContracts.d.ts.map +1 -0
  267. package/dist/remediate/steps/dispatch/hostContracts.js +104 -0
  268. package/dist/remediate/steps/dispatch/hostContracts.js.map +1 -0
  269. package/dist/remediate/steps/dispatch/hostCorroboration.d.ts +70 -0
  270. package/dist/remediate/steps/dispatch/hostCorroboration.d.ts.map +1 -0
  271. package/dist/remediate/steps/dispatch/hostCorroboration.js +350 -0
  272. package/dist/remediate/steps/dispatch/hostCorroboration.js.map +1 -0
  273. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +8 -162
  274. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  275. package/dist/remediate/steps/dispatch/hostHandoff.js +137 -690
  276. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  277. package/dist/remediate/steps/dispatch/requiredTests.d.ts +83 -0
  278. package/dist/remediate/steps/dispatch/requiredTests.d.ts.map +1 -0
  279. package/dist/remediate/steps/dispatch/requiredTests.js +262 -0
  280. package/dist/remediate/steps/dispatch/requiredTests.js.map +1 -0
  281. package/dist/remediate/steps/finalGate.d.ts +8 -7
  282. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  283. package/dist/remediate/steps/finalGate.js +14 -11
  284. package/dist/remediate/steps/finalGate.js.map +1 -1
  285. package/dist/remediate/steps/frictionCloseout.d.ts +34 -0
  286. package/dist/remediate/steps/frictionCloseout.d.ts.map +1 -0
  287. package/dist/remediate/steps/frictionCloseout.js +113 -0
  288. package/dist/remediate/steps/frictionCloseout.js.map +1 -0
  289. package/dist/remediate/steps/gateCommands.d.ts +7 -9
  290. package/dist/remediate/steps/gateCommands.d.ts.map +1 -1
  291. package/dist/remediate/steps/gateCommands.js +37 -11
  292. package/dist/remediate/steps/gateCommands.js.map +1 -1
  293. package/dist/remediate/steps/intakeResolver.d.ts +2 -0
  294. package/dist/remediate/steps/intakeResolver.d.ts.map +1 -1
  295. package/dist/remediate/steps/intakeResolver.js +36 -11
  296. package/dist/remediate/steps/intakeResolver.js.map +1 -1
  297. package/dist/remediate/steps/nextStep.d.ts +23 -110
  298. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  299. package/dist/remediate/steps/nextStep.js +232 -508
  300. package/dist/remediate/steps/nextStep.js.map +1 -1
  301. package/dist/remediate/steps/prompts.d.ts +1 -1
  302. package/dist/remediate/steps/prompts.d.ts.map +1 -1
  303. package/dist/remediate/steps/prompts.js +7 -2
  304. package/dist/remediate/steps/prompts.js.map +1 -1
  305. package/dist/remediate/steps/recoverIngest.d.ts +57 -0
  306. package/dist/remediate/steps/recoverIngest.d.ts.map +1 -0
  307. package/dist/remediate/steps/recoverIngest.js +171 -0
  308. package/dist/remediate/steps/recoverIngest.js.map +1 -0
  309. package/dist/remediate/steps/stepWriter.d.ts +5 -4
  310. package/dist/remediate/steps/stepWriter.d.ts.map +1 -1
  311. package/dist/remediate/steps/stepWriter.js +2 -1
  312. package/dist/remediate/steps/stepWriter.js.map +1 -1
  313. package/dist/remediate/steps/types.d.ts +2 -3
  314. package/dist/remediate/steps/types.d.ts.map +1 -1
  315. package/dist/remediate/steps/types.js.map +1 -1
  316. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  317. package/dist/remediate/validation/artifacts.js +5 -12
  318. package/dist/remediate/validation/artifacts.js.map +1 -1
  319. package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
  320. package/dist/remediate/validation/contractPipeline.js +10 -7
  321. package/dist/remediate/validation/contractPipeline.js.map +1 -1
  322. package/dist/remediate/validation/contractPipelineGates.d.ts +13 -67
  323. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  324. package/dist/remediate/validation/contractPipelineGates.js +65 -139
  325. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  326. package/dist/remediate/validation/remediationState.d.ts.map +1 -1
  327. package/dist/remediate/validation/remediationState.js +9 -3
  328. package/dist/remediate/validation/remediationState.js.map +1 -1
  329. package/dist/shared/agentReflections.d.ts +2 -2
  330. package/dist/shared/analyzerPolicy.d.ts +2 -16
  331. package/dist/shared/analyzerPolicy.d.ts.map +1 -1
  332. package/dist/shared/analyzerPolicy.js +22 -24
  333. package/dist/shared/analyzerPolicy.js.map +1 -1
  334. package/dist/shared/analyzerRunConsent.d.ts +33 -0
  335. package/dist/shared/analyzerRunConsent.d.ts.map +1 -0
  336. package/dist/shared/analyzerRunConsent.js +42 -0
  337. package/dist/shared/analyzerRunConsent.js.map +1 -0
  338. package/dist/shared/analyzers/acquisitionEngine.d.ts +3 -8
  339. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  340. package/dist/shared/analyzers/acquisitionEngine.js +1 -0
  341. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  342. package/dist/shared/decompose/charterExtraction.d.ts +8 -8
  343. package/dist/shared/decompose/systemicChallenge.d.ts +12 -12
  344. package/dist/shared/engine/obligationEngine.d.ts +2 -2
  345. package/dist/shared/findings/dedupe.d.ts.map +1 -1
  346. package/dist/shared/findings/dedupe.js +16 -0
  347. package/dist/shared/findings/dedupe.js.map +1 -1
  348. package/dist/shared/graph/orderedReachability.d.ts +9 -0
  349. package/dist/shared/graph/orderedReachability.d.ts.map +1 -0
  350. package/dist/shared/graph/orderedReachability.js +21 -0
  351. package/dist/shared/graph/orderedReachability.js.map +1 -0
  352. package/dist/shared/index.d.ts +9 -3
  353. package/dist/shared/index.d.ts.map +1 -1
  354. package/dist/shared/index.js +6 -3
  355. package/dist/shared/index.js.map +1 -1
  356. package/dist/shared/intake/guidanceBootstrap.d.ts +2 -0
  357. package/dist/shared/intake/guidanceBootstrap.d.ts.map +1 -1
  358. package/dist/shared/intake/guidanceBootstrap.js +10 -1
  359. package/dist/shared/intake/guidanceBootstrap.js.map +1 -1
  360. package/dist/shared/intent/constraintClauses.d.ts +3 -0
  361. package/dist/shared/intent/constraintClauses.d.ts.map +1 -1
  362. package/dist/shared/intent/constraintClauses.js +9 -0
  363. package/dist/shared/intent/constraintClauses.js.map +1 -1
  364. package/dist/shared/io/artifactTreeHold.d.ts +5 -0
  365. package/dist/shared/io/artifactTreeHold.d.ts.map +1 -0
  366. package/dist/shared/io/artifactTreeHold.js +13 -0
  367. package/dist/shared/io/artifactTreeHold.js.map +1 -0
  368. package/dist/shared/io/auditToolsPaths.d.ts +3 -1
  369. package/dist/shared/io/auditToolsPaths.d.ts.map +1 -1
  370. package/dist/shared/io/auditToolsPaths.js +6 -1
  371. package/dist/shared/io/auditToolsPaths.js.map +1 -1
  372. package/dist/shared/io/collectFilesSorted.d.ts +10 -0
  373. package/dist/shared/io/collectFilesSorted.d.ts.map +1 -0
  374. package/dist/shared/io/collectFilesSorted.js +38 -0
  375. package/dist/shared/io/collectFilesSorted.js.map +1 -0
  376. package/dist/shared/io/json.d.ts.map +1 -1
  377. package/dist/shared/io/json.js +11 -4
  378. package/dist/shared/io/json.js.map +1 -1
  379. package/dist/shared/io/stepContractWriter.d.ts.map +1 -1
  380. package/dist/shared/io/stepContractWriter.js +8 -0
  381. package/dist/shared/io/stepContractWriter.js.map +1 -1
  382. package/dist/shared/loopCorePaths.d.ts.map +1 -1
  383. package/dist/shared/loopCorePaths.js +9 -0
  384. package/dist/shared/loopCorePaths.js.map +1 -1
  385. package/dist/shared/observability/rootLogObservations.d.ts +10 -0
  386. package/dist/shared/observability/rootLogObservations.d.ts.map +1 -0
  387. package/dist/shared/observability/rootLogObservations.js +147 -0
  388. package/dist/shared/observability/rootLogObservations.js.map +1 -0
  389. package/dist/shared/prompts.d.ts +7 -7
  390. package/dist/shared/prompts.d.ts.map +1 -1
  391. package/dist/shared/prompts.js +11 -11
  392. package/dist/shared/prompts.js.map +1 -1
  393. package/dist/shared/submission/submissionLedger.d.ts +7 -1
  394. package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
  395. package/dist/shared/submission/submissionLedger.js +8 -0
  396. package/dist/shared/submission/submissionLedger.js.map +1 -1
  397. package/dist/shared/tooling/testCommand.d.ts +1 -0
  398. package/dist/shared/tooling/testCommand.d.ts.map +1 -1
  399. package/dist/shared/tooling/testCommand.js +4 -0
  400. package/dist/shared/tooling/testCommand.js.map +1 -1
  401. package/dist/shared/types/charter.d.ts +8 -8
  402. package/dist/shared/types/conceptualPerspective.d.ts +66 -0
  403. package/dist/shared/types/conceptualPerspective.d.ts.map +1 -0
  404. package/dist/shared/types/conceptualPerspective.js +135 -0
  405. package/dist/shared/types/conceptualPerspective.js.map +1 -0
  406. package/dist/shared/types/contractPipeline/design.d.ts +5 -0
  407. package/dist/shared/types/contractPipeline/design.d.ts.map +1 -1
  408. package/dist/shared/types/contractPipeline/design.js +6 -0
  409. package/dist/shared/types/contractPipeline/design.js.map +1 -1
  410. package/dist/shared/types/contractPipeline/implementation.d.ts +54 -0
  411. package/dist/shared/types/contractPipeline/implementation.d.ts.map +1 -1
  412. package/dist/shared/types/contractPipeline/implementation.js +10 -5
  413. package/dist/shared/types/contractPipeline/implementation.js.map +1 -1
  414. package/dist/shared/types/contractPipeline/obligations.d.ts +21 -10
  415. package/dist/shared/types/contractPipeline/obligations.d.ts.map +1 -1
  416. package/dist/shared/types/contractPipeline/obligations.js +13 -1
  417. package/dist/shared/types/contractPipeline/obligations.js.map +1 -1
  418. package/dist/shared/types/disposition.d.ts +6 -6
  419. package/dist/shared/types/finding.d.ts +50 -50
  420. package/dist/shared/types/intentCheckpoint.d.ts +66 -11
  421. package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
  422. package/dist/shared/types/intentCheckpoint.js +10 -2
  423. package/dist/shared/types/intentCheckpoint.js.map +1 -1
  424. package/dist/shared/types/remediationOutcome.d.ts +252 -24
  425. package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
  426. package/dist/shared/types/remediationOutcome.js +4 -0
  427. package/dist/shared/types/remediationOutcome.js.map +1 -1
  428. package/dist/shared/types/reviewIndependence.d.ts +225 -0
  429. package/dist/shared/types/reviewIndependence.d.ts.map +1 -0
  430. package/dist/shared/types/reviewIndependence.js +67 -0
  431. package/dist/shared/types/reviewIndependence.js.map +1 -0
  432. package/dist/shared/types/stepContract.d.ts +8 -2
  433. package/dist/shared/types/stepContract.d.ts.map +1 -1
  434. package/dist/shared/types/stepContract.js +4 -1
  435. package/dist/shared/types/stepContract.js.map +1 -1
  436. package/dist/shared/validation/findingsReport.d.ts.map +1 -1
  437. package/dist/shared/validation/findingsReport.js +13 -0
  438. package/dist/shared/validation/findingsReport.js.map +1 -1
  439. package/docs/audit-pkg/contracts.md +38 -2
  440. package/docs/audit-pkg/operator-guide.md +59 -4
  441. package/package.json +5 -5
  442. package/remediate-code.mjs +16 -3
  443. package/skills/audit-code/audit-code.prompt.md +1 -31
  444. package/skills/remediate-code/remediate-code.prompt.md +12 -6
@@ -1,28 +1,34 @@
1
+ import { stateRunId, requireStateRunId, currentHostBoundaryState } from "../state/runIdentity.js";
2
+ import { buildFrictionWalkStep, presentReportStep, decideRemediateFrictionCloseout } from "./frictionCloseout.js";
3
+ import { reviewFilterDispositionsPath, persistReviewFilterDispositions } from "../review/filterDispositions.js";
4
+ import { INTENT_INTERPRETATION_FILENAME, readPersistedIntentInterpretationSync, readOrRepairIntentInterpretation } from "../intent/intentPersistence.js";
5
+ import { requestedFindingSelection, renderFindingSelection } from "../intakeSelection.js";
6
+ import { parseCommandString } from "../../shared/tooling/commandShape.js";
1
7
  // sites-pinned: tests/remediate/friction-capture-closeout.test.ts, tests/remediate/next-step-lifecycle.test.ts, tests/remediate/next-step-pipeline-dispatch.test.ts, tests/remediate/next-step-outcomes-contract.test.ts, tests/remediate/integration-pipeline.test.ts, tests/remediate/outcomes-roundtrip.test.ts, tests/remediate/phase-close.test.ts, tests/remediate/grounding.test.ts, tests/remediate/clarification-round-contract.test.ts, tests/remediate/next-step-review-gate.test.ts, tests/remediate/n-r04-intent-checkpoint.test.ts, tests/remediate/final-gate-red-pause.test.ts
2
8
  // (the free-form branch's write
3
9
  // scope is normalized — a backslash-spelled citation no longer wedges prepare)
4
10
  import { AUDIT_TOOLS_DIRNAME } from "../../shared/io/auditToolsPaths.js";
5
11
  import { loadRemediateSessionConfig } from "./sessionConfigLoad.js";
6
12
  import { z } from "zod";
7
- import { existsSync, readFileSync, statSync } from "node:fs";
13
+ import { existsSync, statSync } from "node:fs";
8
14
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
15
  import { dirname, join, resolve } from "node:path";
10
- import { StateStore } from "../state/store.js";
16
+ import { StateStore, OPERATOR_LIFECYCLE_FILENAME } from "../state/store.js";
11
17
  // IO / validation / rendering helpers
12
- import { discardOnSchemaVersionMismatch, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, auditReadOf, formatValidationIssues, isRecord, renderIngestReportLines, withFsRetry, RunLogger, coerceJsonObjectArg, renderPromptCommand, headCommit, projectAuditFindingsReportSubset,
18
+ import { discardOnSchemaVersionMismatch, applyGuidanceFile, applyGuidanceText, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, auditReadOf, formatValidationIssues, isRecord, renderIngestReportLines, withFsRetry, RunLogger, coerceJsonObjectArg, renderPromptCommand, headCommit, hashContent, projectAuditFindingsReportSubset,
13
19
  // obligation engine + intent
14
- interpretFreeFormIntent, interpretIntent, unresolvedFromClauses, advance, describeStoppedFold, decideFrictionTriage, buildFrictionTriageBlock, linkFrictionRunIds,
20
+ interpretFreeFormIntent, unresolvedFromClauses, advance, describeStoppedFold, linkFrictionRunIds,
15
21
  // domain constants
16
- LENSES, SEVERITIES, CLOSING_ACTIONS, SKIP_WRITE, invalidateStepContracts, detectProjectFacts, isClosingAction, neutralProjectFacts, repoRelativePath, } from "audit-tools/shared";
22
+ LENSES, SEVERITIES, CLOSING_ACTIONS, detectProjectFacts, isClosingAction, neutralProjectFacts, repoRelativePath, } from "audit-tools/shared";
17
23
  import { applyPlanPipeline, buildCoverageLedger } from "../phases/plan.js";
18
24
  import { groundExtractedFindings, } from "../phases/grounding.js";
19
25
  import { runTriagePhase } from "../phases/triage.js";
20
26
  import { runClosePhase } from "../phases/close.js";
21
27
  import { validateRemediationPlan } from "../validation/remediationState.js";
22
28
  import { readExtractedPlanIfPresent, } from "./dispatch/marshal.js";
23
- import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingBlocks, precomputeRecoveryTestVerdicts, prepareRemediationHostHandoff, remediationIssueRemedy, workloadBindingIdentity, } from "./dispatch/hostHandoff.js";
29
+ import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingBlocks, prepareRemediationHostHandoff, RemediationHostPreparationError, remediationIssueRemedy } from "./dispatch/hostHandoff.js";
24
30
  import { FileLockTimeoutError, withFileLock, } from "../../shared/io/fileLock.js";
25
- import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, } from "../../shared/io/auditToolsPaths.js";
31
+ import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, remediationRequiredTestLogsDir, } from "../../shared/io/auditToolsPaths.js";
26
32
  import { callerWorkingDirectory, discoverRepoRoot, resolveRepoRoot, } from "../../shared/io/repoRoot.js";
27
33
  import { writeCurrentStep } from "./stepWriter.js";
28
34
  import { isTerminalStatus, isVerifiedCompleteStatus, } from "../state/itemStatus.js";
@@ -32,14 +38,13 @@ import { applyIntentOrdering } from "../intent/intentOrdering.js";
32
38
  import { resolveIntakeStep } from "./intakeResolver.js";
33
39
  import { carriesGateVerdict } from "../../shared/types/remediationOutcome.js";
34
40
  import { RUNTIME_RESIDUAL_DECLARATION, readFinalGateVerdict, runToolOwnedFinalGate, writeFinalGateRedRecord, writeFinalGateOutcomeRecord, writeFinalGateVerdict, } from "./finalGate.js";
35
- import { renderGateAttribution, worktreeContentId, } from "./gateCommands.js";
41
+ import { renderGateAttribution, worktreeContentId, finalGateBinding, } from "./gateCommands.js";
36
42
  import { buildNextContractPipelineStep, shouldEnterContractPipeline, readSeedAuditRead, writePathASeedFromFindings, normalizeBlockTouchedFiles, checkWriteScopePathsAgainstTrackedTree, } from "./contractPipeline.js";
37
43
  import { compareCodeUnits } from "../../shared/compareCodeUnits.js";
38
44
  import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
39
45
  import { buildReviewRequest, applyReviewResolution, parseReviewResolution, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
40
46
  import { buildAutonomousReviewDecision } from "../review/autonomousGate.js";
41
47
  import { runFindingFilterPass } from "../findingFilter.js";
42
- import { droppedFindingsRecordPath, renderDroppedFindingsRecord, } from "../droppedFindingsRecord.js";
43
48
  import { intakePaths, isIntakeReady, manifestIsInputBound, readIntakeArtifacts, readProjectFacts, readSourceManifest, writeProjectFacts, resolveManifestSources, writeRemediationBrief, } from "../intake.js";
44
49
  import { ensureIntakeRiskSignal, readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, findingRiskEvidence, distinctAffectedFiles, } from "../riskSignal.js";
45
50
  import { isLegacyDraftCheckpoint, readIntentCheckpoint, readIntentCheckpointLenient, } from "audit-tools/shared";
@@ -82,37 +87,6 @@ function resolveArtifactsDir(root, artifactsDir) {
82
87
  // honored verbatim.
83
88
  return artifactsDir ? resolve(artifactsDir) : remediationArtifactsDir(root);
84
89
  }
85
- /**
86
- * The run's friction-record key: the plan id, or `null` when there is no plan.
87
- *
88
- * There is DELIBERATELY no fallback key. A fallback ("run") is a path every
89
- * planless state shares, and `decideFrictionTriage` MATERIALIZES the record it
90
- * is handed — so a planless caller could MINT a fresh empty record under it,
91
- * which the close gate then blocks on. That is the 2026-08-24 defect exactly.
92
- *
93
- * The key is therefore the plan id or nothing, and a caller handed `null` has
94
- * no walk to key: the walk ran before the close archived the plan-keyed record,
95
- * and the plan is where its identity came from.
96
- */
97
- function stateRunId(state) {
98
- return state?.plan?.plan_id ?? null;
99
- }
100
- /**
101
- * The run key for a caller holding a LOADED state that must carry a plan id —
102
- * the host-handoff/ingest paths, whose contracts declare `run_id: string`.
103
- *
104
- * It throws rather than inventing a key: an absent plan id there is a
105
- * reachable-but-unexpected state, and silently falling back would mint a
106
- * shared path exactly as the fallback key did. Callers that can legitimately
107
- * hold a planless state use {@link stateRunId} and handle the `null`.
108
- */
109
- function requireStateRunId(state) {
110
- const runId = stateRunId(state);
111
- if (!runId) {
112
- throw new Error("remediate: a loaded state with no plan id cannot name a run");
113
- }
114
- return runId;
115
- }
116
90
  /**
117
91
  * Where an autonomous run's LEFTOVER deliverable pair lands.
118
92
  *
@@ -547,7 +521,7 @@ function customCommandOf(checkpoint) {
547
521
  : null;
548
522
  }
549
523
  async function saveStateForPlan(artifactsDir, existing, plan, planCoverage) {
550
- const { host_handoff: _staleHostHandoff, ...carryForwardState } = existing;
524
+ const { host_handoff: _staleHostHandoff, conformance_review: priorConformance, ...carryForwardState } = existing;
551
525
  const items = {};
552
526
  const blockIds = blockIdsByFinding(plan);
553
527
  for (const finding of plan.findings) {
@@ -559,6 +533,7 @@ async function saveStateForPlan(artifactsDir, existing, plan, planCoverage) {
559
533
  }
560
534
  const state = {
561
535
  ...carryForwardState,
536
+ ...(priorConformance ? { conformance_review: { ...priorConformance, run_id: plan.plan_id } } : {}),
562
537
  status: "planning",
563
538
  plan,
564
539
  items,
@@ -668,6 +643,7 @@ async function forceReplanFromExistingIntake(root, artifactsDir, previous, store
668
643
  status: "pending",
669
644
  started_at: previous.started_at,
670
645
  step_count: previous.step_count,
646
+ ...(previous.conformance_review ? { conformance_review: previous.conformance_review } : {}),
671
647
  // Carry the run-lifetime staging-manifest fields across a force-replan.
672
648
  // Dropping run_start_dirty here would make handlePendingExtractedPlan's
673
649
  // capture-once guard re-capture AFTER edits have landed, misclassifying
@@ -700,256 +676,6 @@ async function forceReplanFromExistingIntake(root, artifactsDir, previous, store
700
676
  await store.saveState(carried);
701
677
  return carried;
702
678
  }
703
- /**
704
- * The BLOCKING friction close-out step for a run whose walk is still owed.
705
- *
706
- * Emitted from `handleClosing` BEFORE the close touches disk, so the record the
707
- * host is told to write (`triage.recordPath`) is the plan-keyed one this very
708
- * decision just materialized — and so the run's identity in that path is the
709
- * plan id, not a fallback. The step is a `closing`-phase gate: `next-step`
710
- * re-decides the same walk on the next call, and only a disposed walk lets the
711
- * close proceed.
712
- */
713
- async function buildFrictionWalkStep(root, artifactsDir, state, triage) {
714
- return writeCurrentStep({
715
- stepKind: "close_run",
716
- status: "ready",
717
- runId: requireStateRunId(state),
718
- repoRoot: root,
719
- artifactsDir,
720
- prompt: `# Remediation Run Friction Triage\n\nComplete the friction close-out walk before the run may close.\n${buildFrictionTriageBlock(triage)}`,
721
- allowedCommands: [],
722
- stopCondition: "Complete friction triage (write dispositions and open_observations), then call next-step again.",
723
- artifactPaths: { friction_record: triage.recordPath },
724
- });
725
- }
726
- async function presentReportStep(root, artifactsDir, state) {
727
- const reportPath = join(dirname(artifactsDir), "remediation-report.md");
728
- // Terminal friction-TRIAGE close-out, folded into present_report (single-sourced in
729
- // `audit-tools/shared`). MANDATORY + BLOCKING: stays "dispose" until every mechanical
730
- // event + reflection is disposed AND ≥1 open observation written. Never trivially
731
- // satisfied by an empty event set — the host must actively confirm the friction state.
732
- //
733
- // NO FRICTION RECORD IS EVER MINTED HERE. The decision was made — and the
734
- // record, under the run's plan-keyed path, was walked — before the close
735
- // archived it: `handleClosing` consults `decideRemediateFrictionCloseout`
736
- // against the LIVE state and short-circuits the close until the walk is
737
- // disposed. So ON THE GREEN PATH — the close ran to `complete`, archived the
738
- // run and removed the artifacts dir — the walk was already completed when the
739
- // close started, the plan-keyed record left with the other deliverables, and
740
- // all that is left is to RENDER the already-recorded walk for the host.
741
- //
742
- // NOT-GREEN IS THE OTHER HALF, and it is not "mint nothing": a close that
743
- // returned a non-`complete` state PRESERVES the artifacts dir, so the record
744
- // this step decides remains where the run wrote it — the caller reaches here
745
- // with the saved state still carrying its plan id, `decideFrictionTriage`
746
- // materializes (or re-reads) that same plan-keyed file, and the walk is
747
- // rendered from the record that is still in place.
748
- //
749
- // It is decided — and so materialized — ONLY when the state names a run. The
750
- // guard is the plan id, not the artifacts dir: the dir is not evidence of
751
- // anything here, since `decideNextStepLoop` mkdirs it unconditionally on entry,
752
- // `RunLogger.event` mkdirs it again for `run.log.jsonl`, and `writeJsonFile`
753
- // mkdirs any parent it is handed. So a dir-existence test is true on every
754
- // path, including a run whose state the close deleted. Guarding on the dir was
755
- // the 2026-08-24 defect: the decider ran with a null state, took a fallback
756
- // key, and because `decideFrictionTriage` MATERIALIZES the record it is given,
757
- // minted a fresh EMPTY record inside the directory the close had just deleted —
758
- // re-blocking the run on the record it had created. `stateRunId` now answers
759
- // `null` for a planless state and this call therefore decides nothing.
760
- //
761
- // A `complete` state reaching here is a run that never ran the close walk (a
762
- // re-delivery of an already-complete run, whose plan the state still carries),
763
- // so the walk is genuinely still owed and deciding it here is correct — it
764
- // renders as the blocking step. Only a planless state renders nothing.
765
- const triage = await decideRemediateFrictionCloseout(artifactsDir, state);
766
- const frictionBlock = triage ? buildFrictionTriageBlock(triage) : "";
767
- const isBlocked = triage?.action === "dispose";
768
- return writeCurrentStep({
769
- stepKind: "present_report",
770
- status: isBlocked ? "ready" : "complete",
771
- runId: stateRunId(state),
772
- repoRoot: root,
773
- artifactsDir,
774
- prompt: isBlocked
775
- ? `# Remediation Run Friction Triage\n\nComplete friction triage before presenting the report.\n${frictionBlock}`
776
- : `# Present Remediation Report\n\nRead \`${reportPath}\` and summarize the remediation outcome.\nMention resolved, ignored, and deemed-inappropriate counts plus the closing action.\n${frictionBlock}`,
777
- allowedCommands: [],
778
- stopCondition: isBlocked
779
- ? "Complete friction triage (write dispositions and open_observations), then call next-step again."
780
- : "Present the remediation report summary and stop.",
781
- artifactPaths: {
782
- final_report: reportPath,
783
- ...(triage ? { friction_record: triage.recordPath } : {}),
784
- },
785
- });
786
- }
787
- function currentHostBoundaryState(state) {
788
- return {
789
- contract_version: "remediate-code-state/v1alpha1",
790
- ...state,
791
- };
792
- }
793
- /**
794
- * The `recover-ingest` verb's whole body: ingest the host's landed results in
795
- * RECOVERY mode and persist through the same file-locked, atomically-writing
796
- * store, with the same `contract_version` strip.
797
- *
798
- * It is a separate verb rather than a flag on `next-step` because the
799
- * relaxation it enables must be an operator's explicit act — see
800
- * `ingestRemediationHostResults`, which states what is waived and the residual
801
- * risk. Nothing else here differs from the normal ingestion: the same workload,
802
- * the same contract gates, the same eligibility frontier.
803
- *
804
- * ## Why this runs in two phases
805
- *
806
- * A required-test rerun is `spawnSync`, which blocks the event loop for its
807
- * whole duration. Run inside the state lock, it would starve the lock's own
808
- * heartbeat timer (`setInterval` in the shared fileLock) — the held lock's mtime
809
- * would stop being refreshed, a second acquirer would classify it as stale at
810
- * ~30s and steal it, and mutual exclusion would be gone precisely during the
811
- * longest critical section in the codebase. Holding a lock across a blocking
812
- * spawn is therefore not merely slow; it is unsound.
813
- *
814
- * So:
815
- *
816
- * - **Phase 1, UNLOCKED.** Snapshot the state, capture HEAD and the workload
817
- * binding, and run every distinct required-test command exactly once
818
- * (`precomputeRecoveryTestVerdicts`). Both identities are captured BEFORE the
819
- * spawns, not after, because a host-authored command that MOVES HEAD would
820
- * otherwise produce verdicts of mixed provenance and go undetected. (The HEAD
821
- * guard compares commit shas: it sees HEAD movement, not worktree dirt — a
822
- * command that only dirties files is invisible to it, which is acceptable
823
- * because phase 2's corroboration is commit-based.)
824
- * - **Phase 2, LOCKED.** Re-read both identities and abort the whole recovery if
825
- * either moved — `tree_moved_between_phases` for HEAD,
826
- * `state_moved_between_phases` for the binding — because the phase-1 verdicts
827
- * would describe a tree or a frontier that no longer exists, and nothing is
828
- * accepted or appended. Otherwise ingest with the pre-computed verdicts, which
829
- * the ingest only READS: in recovery mode it never spawns, and a command
830
- * missing from the table fails closed.
831
- *
832
- * What remains inside the lock is git plumbing (ancestry, ref scan, diff-tree),
833
- * the ledger append, and the state write — sub-second work, comfortably inside
834
- * heartbeat coverage. The two unchanged-identity guards close the gap the phase
835
- * split opens; the operational protocol is still one writer at a time, now
836
- * enforced by a lock that cannot be stolen mid-hold instead of by convention.
837
- *
838
- * A recovery that changes nothing writes nothing: phase 2 returns the locked
839
- * store's `SKIP_WRITE` sentinel, which `StateStore.mutate` now honors, so the
840
- * retry loop of a genuinely-empty recovery no longer replaces `state.json` with
841
- * byte-identical content on every pass.
842
- */
843
- export async function recoverIngestHostResults(options) {
844
- const root = resolveRoot(options.root);
845
- const artifactsDir = resolveArtifactsDir(root, options.artifactsDir);
846
- const store = new StateStore(artifactsDir);
847
- // ── Phase 1: unlocked ────────────────────────────────────────────────────
848
- const snapshot = await store.loadState();
849
- if (!snapshot) {
850
- throw new Error(`No remediation state at ${artifactsDir} — there is nothing to ingest.`);
851
- }
852
- const headBeforeTests = await headCommit(root);
853
- const bindingBeforeTests = workloadBindingIdentity(currentHostBoundaryState(snapshot));
854
- const requiredTestVerdicts = await precomputeRecoveryTestVerdicts({
855
- root,
856
- artifactsDir,
857
- runId: options.runId,
858
- state: currentHostBoundaryState(snapshot),
859
- });
860
- if (requiredTestVerdicts === "unsupported_retired_state") {
861
- throw new Error("Remediation state uses a retired dispatch shape and cannot cross the host handoff boundary.");
862
- }
863
- // ── Phase 2: locked; children are async + deadline-bounded only ──────────
864
- // The long-running required tests were precomputed in Phase 1, outside the
865
- // lock. What still spawns under the hold is the corroboration git probes —
866
- // async on the tracked twin with the shared deadline (INV-SSF), so the
867
- // hold's heartbeat keeps beating through every probe.
868
- let ingested;
869
- await store.mutate(async (state) => {
870
- if (!state) {
871
- throw new Error(`No remediation state at ${artifactsDir} — there is nothing to ingest.`);
872
- }
873
- const headNow = await headCommit(root);
874
- const bindingNow = workloadBindingIdentity(currentHostBoundaryState(state));
875
- // TWO identities, because they catch different writers. HEAD moves when the
876
- // tree does; the workload binding moves when a concurrent state writer
877
- // settles items or re-mints the workload without committing anything. The
878
- // verdict table describes the frontier as it was at phase 1, so either
879
- // change invalidates it — and a stale table is not merely imprecise: the
880
- // commands it no longer covers read as `required_test_failed`, which
881
- // attributes a bookkeeping race to the host's work.
882
- const moved = headNow !== headBeforeTests
883
- ? "tree"
884
- : bindingNow !== bindingBeforeTests
885
- ? "state"
886
- : null;
887
- if (moved !== null) {
888
- ingested = {
889
- accepted_count: 0,
890
- completed_work_item_ids: [],
891
- pending_work_item_ids: state.host_handoff?.work_item_ids ?? [],
892
- // This verdict aborts the whole recovery BEFORE any item is read, so
893
- // there is no per-item observation to report — an empty map is the
894
- // honest statement of that, not a missing field.
895
- work_item_outcomes: new Map(),
896
- issues: [
897
- moved === "tree"
898
- ? {
899
- code: "tree_moved_between_phases",
900
- message: `HEAD moved from ${headBeforeTests ?? "(none)"} to ${headNow ?? "(none)"} ` +
901
- "while the required tests were running, so their verdicts no longer describe " +
902
- "this tree. Nothing was accepted; re-run recover-ingest on a settled tree.",
903
- }
904
- : {
905
- code: "state_moved_between_phases",
906
- message: "the run's workload binding changed while the required tests were running, " +
907
- "so their verdicts no longer describe the pending frontier. Nothing was " +
908
- "accepted; re-run recover-ingest once no other writer is advancing this run.",
909
- },
910
- ],
911
- state_changed: false,
912
- state: currentHostBoundaryState(state),
913
- };
914
- // The lock was taken to WRITE. Nothing changed, so write nothing: the
915
- // no-op sentinel is what keeps a settled recovery from replacing
916
- // state.json with byte-identical content on every retry.
917
- return SKIP_WRITE;
918
- }
919
- const outcome = await ingestRemediationHostResults({
920
- root,
921
- artifactsDir,
922
- runId: options.runId,
923
- state: currentHostBoundaryState(state),
924
- recovery: { requiredTestVerdicts },
925
- });
926
- if (outcome === "unsupported_retired_state") {
927
- throw new Error("Remediation state uses a retired dispatch shape and cannot cross the host handoff boundary.");
928
- }
929
- ingested = outcome;
930
- // A nothing-to-recover pass returns the sentinel, not the state it read: the
931
- // mutation is a no-op and must not rewrite the file (see StateStore.mutate).
932
- if (!outcome.state_changed)
933
- return SKIP_WRITE;
934
- // No `contract_version` strip. The boundary helper below stamps the version
935
- // onto the state it hands the host handoff, and this used to peel it back
936
- // off on the way to disk so `state.json` matched what was written before.
937
- // The STORE now owns that field end to end — it stamps it on read and the
938
- // write hook validates it — so peeling it here would persist a state
939
- // without the identity the store just established, and the next read would
940
- // have to re-invent it.
941
- return outcome.state;
942
- });
943
- // The run moved, so the persisted step contract no longer describes it: it
944
- // names work this ingest just resolved, against a workload binding this ingest
945
- // may have cleared. Invalidate it — but only when something actually changed,
946
- // because a no-op recovery must leave the tree byte-identical (which is the
947
- // same reason the mutation above writes nothing).
948
- if (ingested.state_changed) {
949
- await invalidateStepContracts(artifactsDir);
950
- }
951
- return ingested;
952
- }
953
679
  /**
954
680
  * The ingest report for the implement-dispatch prompt, as prompt lines.
955
681
  *
@@ -1014,25 +740,81 @@ async function buildImplementDispatchStep(ctx) {
1014
740
  await store.saveState(ingested.state);
1015
741
  return { kind: "transition", state: ingested.state };
1016
742
  }
743
+ const conformance = ingested.issues.filter(issue => issue.review_request_path !== undefined);
744
+ if (conformance.length > 0) {
745
+ const nextCommand = loaderCommand("next-step");
746
+ const unavailable = conformance.some(issue => issue.code === "conformance_review_unavailable");
747
+ const repair = conformance.some(issue => issue.code === "conformance_review_insufficient");
748
+ return {
749
+ kind: "emit",
750
+ step: await writeCurrentStep({
751
+ stepKind: "review_contract_conformance", status: unavailable ? "blocked" : "ready",
752
+ runId, repoRoot: root, artifactsDir,
753
+ prompt: [
754
+ "# Independent contract conformance review",
755
+ "",
756
+ "The results named below passed mechanical validation. They are not accepted until their bound independent reviews pass.",
757
+ "Read each request file and give it to a context that did not author the implementation and cannot see the author's reasoning. The request contains the exact result, obligations, contracts, response schema and response path.",
758
+ "",
759
+ ...conformance.flatMap(issue => [
760
+ `- ${issue.work_item_id}: ${issue.message}`,
761
+ ` Request: ${issue.review_request_path}`,
762
+ ]),
763
+ "",
764
+ ...(repair ? ["Repair the implementation or its obligation evidence as explained by the review, then obtain a fresh review bound to the corrected result. Do not change contracts merely to make the review pass."] : []),
765
+ ...(unavailable ? ["Stop until an independent context is available. Degraded/self review cannot replace the required review."] : []),
766
+ "Independence is a host declaration; the tool validates the declaration and content binding, not the identity of the reviewer.",
767
+ `When the responses are ready, run \`${nextCommand}\`.`,
768
+ ].join("\n"),
769
+ allowedCommands: [nextCommand],
770
+ stopCondition: "Stop after the independent review responses are written and next-step is run; if independent review is unavailable, remain paused.",
771
+ artifactPaths: Object.fromEntries(conformance.map(issue => [`review_${issue.work_item_id}`, issue.review_request_path])),
772
+ }),
773
+ };
774
+ }
1017
775
  const baselineCommit = await headCommit(root);
1018
776
  if (!baselineCommit) {
1019
777
  throw new Error("Cannot prepare remediation host work without a repository HEAD commit.");
1020
778
  }
1021
- const handoff = await prepareRemediationHostHandoff({
1022
- root,
1023
- artifactsDir,
1024
- runId,
1025
- baselineCommit,
1026
- state: boundaryState,
1027
- });
779
+ let handoff;
780
+ try {
781
+ handoff = await prepareRemediationHostHandoff({
782
+ root, artifactsDir, runId, baselineCommit, state: boundaryState,
783
+ });
784
+ }
785
+ catch (error) {
786
+ if (!(error instanceof RemediationHostPreparationError))
787
+ throw error;
788
+ const sourcePath = join(artifactsDir, "state.json");
789
+ const diagnosticsPath = join(artifactsDir, "steps", "handoff-repair.json");
790
+ await writeJsonFile(diagnosticsPath, { code: error.code, message: error.message, source: sourcePath });
791
+ const excerpt = error.message.length <= 3_000 ? error.message : `${error.message.slice(0, 3_000)}… [excerpt truncated]`;
792
+ return {
793
+ kind: "emit",
794
+ step: await writeCurrentStep({
795
+ stepKind: "repair_handoff", status: "blocked", runId, repoRoot: root, artifactsDir,
796
+ prompt: `# Repair the planning output before dispatch\n\n${excerpt}\n\nFull validation details: ${diagnosticsPath}\n\n` +
797
+ `Owning source: ${sourcePath} (plan.blocks and its generated handoff binding).\n` +
798
+ `Upstream planning input: ${join(artifactsDir, "extracted-plan.json")}.\n\n` +
799
+ "The current run and completed work are preserved. This dispatch cannot safely repair its upstream producer. " +
800
+ "Return the named validation errors to the planning producer; do not edit generated state, discard accepted work, or submit results under invalid bindings. " +
801
+ "After the upstream producer has corrected the plan, run next-step again.\n",
802
+ allowedCommands: [],
803
+ stopCondition: "Stop and report the named upstream planning defect; do not execute or reset this handoff.",
804
+ artifactPaths: { source_plan: sourcePath, repair_diagnostics: diagnosticsPath },
805
+ access: { read_paths: [sourcePath, diagnosticsPath], write_paths: [] },
806
+ }),
807
+ };
808
+ }
1028
809
  if (handoff === "unsupported_retired_state") {
1029
810
  throw new Error("Remediation state uses a retired dispatch shape and cannot cross the host handoff boundary.");
1030
811
  }
1031
- if (state.host_handoff?.workload_sha256 !==
812
+ if (!state.conformance_review || state.host_handoff?.workload_sha256 !==
1032
813
  handoff.handoff_record.workload_sha256) {
1033
814
  await store.saveState({
1034
815
  ...state,
1035
816
  host_handoff: handoff.handoff_record,
817
+ conformance_review: handoff.conformance_review,
1036
818
  });
1037
819
  }
1038
820
  // Name the runs this dispatch round relates to on the friction record (semantics:
@@ -1081,7 +863,13 @@ async function buildImplementDispatchStep(ctx) {
1081
863
  nextCommand,
1082
864
  ],
1083
865
  stopCondition: "Stop after every emitted work item has a complete result and next-step has been run.",
1084
- artifactPaths: { host_workload: handoff.workload_path },
866
+ artifactPaths: { host_workload: handoff.workload_path, required_test_logs: remediationRequiredTestLogsDir(root) },
867
+ access: {
868
+ read_paths: [root, handoff.workload_path, remediationRequiredTestLogsDir(root)],
869
+ write_paths: [...new Set(handoff.workload.work_items.flatMap((item) => [
870
+ ...item.allowed_files.map((path) => resolve(root, path)), resolve(root, item.result_path),
871
+ ]))],
872
+ },
1085
873
  }),
1086
874
  };
1087
875
  }
@@ -1127,29 +915,6 @@ next-step joins in and takes on unclaimed work.
1127
915
  async function handleComplete(root, artifactsDir, state) {
1128
916
  return presentReportStep(root, artifactsDir, state);
1129
917
  }
1130
- /**
1131
- * The terminal friction-TRIAGE close-out for the remediate half. Thin delegation to
1132
- * the single-sourced `decideFrictionTriage` (`audit-tools/shared`) — the exact analog
1133
- * of audit-code's `decideAuditFrictionCloseout`, so the triage shape, disposition
1134
- * vocabulary, blocking semantics, and close-out logic cannot drift between the two
1135
- * halves. Drops the former false-green (an empty up-front record no longer satisfies):
1136
- * the blocking triage stays unsatisfied ("dispose") until every captured mechanical
1137
- * event AND every surfaced agent-feedback reflection carries a disposition; an empty
1138
- * set (zero events AND zero reflections) is trivially "disposed". Keyed only off
1139
- * `(artifactsDir, runId)`; never coupled to any repo's backlog doc.
1140
- *
1141
- * A state with NO plan id can name no run, and since the decision MATERIALIZES
1142
- * the record it keys, guessing a key there would mint one. It returns `null`
1143
- * instead — "there is no run here to close out", which is the truthful answer
1144
- * for a state whose plan is gone. Callers that hold the run's plan get a
1145
- * decision; callers past the run boundary get nothing to render.
1146
- */
1147
- export async function decideRemediateFrictionCloseout(artifactsDir, state) {
1148
- const runId = stateRunId(state);
1149
- if (!runId)
1150
- return null;
1151
- return decideFrictionTriage(artifactsDir, runId, "remediate-code");
1152
- }
1153
918
  /**
1154
919
  * Copy an unusable extracted plan somewhere recoverable and PROVE the copy
1155
920
  * landed, returning the archive path. Read back and compared byte-for-byte:
@@ -1650,32 +1415,6 @@ sourceAuditRead) {
1650
1415
  `path=${findingsPath}`,
1651
1416
  });
1652
1417
  }
1653
- // ── Path-A filter dispositions (persisted for the coverage ledger) ──────────────
1654
- // The single filter pass runs at intake over the ORIGINAL findings; its
1655
- // dispositions are persisted here so handlePendingExtractedPlan can build the
1656
- // coverage ledger over the originals (every audit finding → exactly one
1657
- // disposition), even though it runs after the pipeline has collapsed the approved
1658
- // survivors into DAG nodes. Maps are serialized as entry arrays for JSON.
1659
- const REVIEW_FILTER_DISPOSITIONS_FILENAME = "review_filter_dispositions.json";
1660
- function reviewFilterDispositionsPath(artifactsDir) {
1661
- return join(artifactsDir, REVIEW_FILTER_DISPOSITIONS_FILENAME);
1662
- }
1663
- async function persistReviewFilterDispositions(artifactsDir, originals, filter) {
1664
- const payload = {
1665
- originals,
1666
- mergeMap: [...filter.mergeMap.entries()],
1667
- droppedNoEvidence: filter.droppedNoEvidence,
1668
- droppedPhantomPaths: [...filter.droppedPhantomPaths.entries()],
1669
- phantomPathsRemoved: [...filter.phantomPathsRemoved.entries()],
1670
- droppedByCheckpoint: filter.droppedByCheckpoint,
1671
- };
1672
- await writeJsonFile(reviewFilterDispositionsPath(artifactsDir), payload);
1673
- // The human half of the same fact. The JSON above keeps only IDS, which told a
1674
- // reader an id and nothing else; this states what each removed finding WAS and
1675
- // why it went. Written beside it, on every pass, so its absence means the pass
1676
- // did not run rather than "nothing was dropped".
1677
- await writeFile(droppedFindingsRecordPath(artifactsDir), renderDroppedFindingsRecord(originals, filter), "utf8");
1678
- }
1679
1418
  async function handleReadyIntakeContractPipeline(root, artifactsDir, options, runLogger) {
1680
1419
  // Fast path: if an extracted-plan.json already exists (pipeline complete or
1681
1420
  // promoted from a previous contract pipeline run), consume it directly without
@@ -1850,6 +1589,7 @@ async function handlePendingIntake(root, artifactsDir, options, runLogger) {
1850
1589
  root,
1851
1590
  artifactsDir,
1852
1591
  input: options.input,
1592
+ findingSelection: options,
1853
1593
  inputResolution,
1854
1594
  loaderCommand,
1855
1595
  randomRunId,
@@ -1880,19 +1620,28 @@ async function handleNoState(root, artifactsDir) {
1880
1620
  },
1881
1621
  });
1882
1622
  }
1883
- async function handleInputConflict(root, artifactsDir, state, inputResolution) {
1623
+ async function handleInputConflict(root, artifactsDir, state, inputResolution, selectionConflict) {
1884
1624
  const planId = state.plan?.plan_id ?? "(none)";
1885
1625
  const itemCount = state.items ? Object.keys(state.items).length : 0;
1886
1626
  const suppliedInline = inputResolution.checked.length > 0
1887
1627
  ? inputResolution.checked.map((p) => `\`${p}\``).join(", ")
1888
1628
  : "(new intake source via `--guidance-file`)";
1629
+ const selectionPrompt = selectionConflict ? [
1630
+ "# Finding selection changed during an existing intake", "",
1631
+ "The earlier selected report and its scope have been preserved. No new selection has been published.",
1632
+ selectionConflict.sourceChanged
1633
+ ? "The original audit report changed. Restore the original source bytes to resume the existing intake, or explicitly start a fresh run from the changed report. A bare resume cannot accept changed source evidence."
1634
+ : "The requested severity/ID selectors differ from this intake's selection. Drop the new selector flags to resume the existing selection, or explicitly start a fresh run with the new selectors.",
1635
+ `To start fresh, first move aside the existing ${artifactsDir} directory, then rerun next-step with --input and the intended --severity/--finding-id flags.`,
1636
+ "Stop and present this choice to the user before continuing.",
1637
+ ].join("\n") : undefined;
1889
1638
  return writeCurrentStep({
1890
1639
  stepKind: "input_conflict",
1891
1640
  status: "blocked",
1892
1641
  runId: stateRunId(state),
1893
1642
  repoRoot: root,
1894
1643
  artifactsDir,
1895
- prompt: `
1644
+ prompt: selectionPrompt ?? `
1896
1645
  # New intake source given, but a remediation run is already in progress
1897
1646
 
1898
1647
  A remediation run already exists in \`${artifactsDir}\` and has advanced past intake,
@@ -2372,14 +2121,15 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
2372
2121
  const candidates = detectPlanAmbiguities(findings, state.items);
2373
2122
  if (candidates.length === 0)
2374
2123
  return null;
2375
- await writeJsonFile(requestPath, candidates);
2124
+ await writeJsonFile(requestPath, { candidates, findings });
2376
2125
  return writeCurrentStep({
2377
2126
  stepKind: "collect_clarifications",
2378
2127
  status: "blocked",
2379
2128
  runId: stateRunId(state),
2380
2129
  repoRoot: root,
2381
2130
  artifactsDir,
2382
- prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id)),
2131
+ prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), undefined, requestPath),
2132
+ access: { read_paths: [requestPath, root], write_paths: [resolutionPath] },
2383
2133
  allowedCommands: [loaderCommand("next-step")],
2384
2134
  stopCondition: "Stop after reviewing the candidate ambiguities (and asking the user any genuine ones), unless the resolution is already written and the prompt told you to continue.",
2385
2135
  artifactPaths: {
@@ -2401,15 +2151,16 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
2401
2151
  const refusal = await planClarificationRefusal(root, state, parsed, new Set(findings.map((f) => f.id)), "not in the plan");
2402
2152
  if (refusal !== null || !parsed.ok) {
2403
2153
  await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
2404
- const candidates = (await readOptionalJsonFile(requestPath)) ??
2405
- detectPlanAmbiguities(findings, state.items);
2154
+ const candidates = detectPlanAmbiguities(findings, state.items);
2155
+ await writeJsonFile(requestPath, { candidates, findings });
2406
2156
  return writeCurrentStep({
2407
2157
  stepKind: "collect_clarifications",
2408
2158
  status: "blocked",
2409
2159
  runId: stateRunId(state),
2410
2160
  repoRoot: root,
2411
2161
  artifactsDir,
2412
- prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), refusal ?? "the resolution could not be read"),
2162
+ prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), refusal ?? "the resolution could not be read", requestPath),
2163
+ access: { read_paths: [requestPath, root], write_paths: [resolutionPath] },
2413
2164
  allowedCommands: [loaderCommand("next-step")],
2414
2165
  stopCondition: "Stop after re-submitting a corrected ambiguity resolution, unless it is already written and the prompt told you to continue.",
2415
2166
  artifactPaths: {
@@ -2674,6 +2425,7 @@ async function emitFinalGateRedStep(ctx) {
2674
2425
  "minutes. With no edit, next-step shows this same red at once and runs\n" +
2675
2426
  "nothing. So fix something before you run next-step again.";
2676
2427
  const nextCommand = loaderCommand("next-step");
2428
+ const recoveryCommand = `${nextCommand} --verification-command "<operator-approved command>"`;
2677
2429
  return {
2678
2430
  kind: "emit",
2679
2431
  step: await writeCurrentStep({
@@ -2682,10 +2434,23 @@ async function emitFinalGateRedStep(ctx) {
2682
2434
  runId: stateRunId(state),
2683
2435
  repoRoot: root,
2684
2436
  artifactsDir,
2685
- prompt: `
2437
+ prompt: gate.results.length === 0 ? `
2438
+ # Verification command required
2439
+
2440
+ This repository declares no executable build, typecheck, lint, or test command.
2441
+ No verification ran, and this run cannot advance or close as verified.
2442
+
2443
+ Ask the operator to supply a verification command, then run:
2444
+
2445
+ \`${recoveryCommand}\`
2446
+
2447
+ The command replaces only the test role for this run. Any declared build,
2448
+ typecheck, and lint checks still run. Alternatively, stop here with work preserved.
2449
+ Do not invent a passing command or waive the gate.
2450
+ ` : `
2686
2451
  # Remediation paused — the repository suite is red
2687
2452
 
2688
- ${ctx.where}, the tool ran the repository's build, typecheck and tests.
2453
+ ${ctx.where}, the tool ran the repository's declared verification commands.
2689
2454
  One command failed:
2690
2455
 
2691
2456
  \`${failingCommand}\`
@@ -2707,8 +2472,10 @@ When the suite is green, the run continues from where it stopped.
2707
2472
 
2708
2473
  ${bindingBlock}
2709
2474
  `,
2710
- allowedCommands: [nextCommand],
2711
- stopCondition: "Stop. Make the repository suite green, then re-run next-step to resume the run.",
2475
+ allowedCommands: gate.results.length === 0 ? [nextCommand, recoveryCommand] : [nextCommand],
2476
+ stopCondition: gate.results.length === 0
2477
+ ? "Stop. Obtain an explicit verification command from the operator or leave this run paused."
2478
+ : "Stop. Make the repository suite green, then re-run next-step to resume the run.",
2712
2479
  artifactPaths: { final_gate_record: recordPath },
2713
2480
  }),
2714
2481
  };
@@ -2730,6 +2497,10 @@ ${bindingBlock}
2730
2497
  * pass OR the gate is GREEN — in which case the caller proceeds to dispatch the
2731
2498
  * phase.
2732
2499
  */
2500
+ function explicitFinalGateTestCommand(state) {
2501
+ return state.plan?.test_command && state.plan.test_command_source !== "project_facts"
2502
+ ? parseCommandString(state.plan.test_command) : undefined;
2503
+ }
2733
2504
  async function runPhaseBoundaryGate(ctx) {
2734
2505
  const { root, artifactsDir, state, options, runLogger } = ctx;
2735
2506
  // Whether a gate is DUE is decided BEFORE whether it is suppressed. The old
@@ -2764,6 +2535,8 @@ async function runPhaseBoundaryGate(ctx) {
2764
2535
  // A boundary that re-dispatches work will get a different id on the next call
2765
2536
  // and the floor re-runs — which is correct: the tree it certified is gone.
2766
2537
  const tree = await worktreeContentId(root);
2538
+ const testCommand = explicitFinalGateTestCommand(state);
2539
+ const binding = finalGateBinding(root, testCommand);
2767
2540
  const gateKey = `phase_boundary_gate phase=${phase}`;
2768
2541
  // THE CACHE. The fold re-enters this boundary on every next-step taken before
2769
2542
  // the next phase dispatches, and the floor is build + typecheck + the whole
@@ -2772,7 +2545,7 @@ async function runPhaseBoundaryGate(ctx) {
2772
2545
  // known and was already recorded. A HIT is therefore a full substitute,
2773
2546
  // including for a RED one (see {@link readFinalGateVerdict} — the pause is
2774
2547
  // rebuilt from the cached results rather than re-derived).
2775
- const cached = await readFinalGateVerdict(artifactsDir, scope, tree);
2548
+ const cached = await readFinalGateVerdict(artifactsDir, scope, tree, binding);
2776
2549
  if (cached !== undefined) {
2777
2550
  // Recorded as a distinct outcome from a real run: the judge is `history`,
2778
2551
  // not a spawned command, and `commands_run` counts what HISTORY held, never
@@ -2824,10 +2597,11 @@ async function runPhaseBoundaryGate(ctx) {
2824
2597
  obligation: state.status,
2825
2598
  note: gateKey,
2826
2599
  });
2827
- const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner });
2600
+ const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner, testCommand: explicitFinalGateTestCommand(state) });
2828
2601
  await writeFinalGateVerdict(artifactsDir, {
2829
2602
  scope,
2830
2603
  tree,
2604
+ binding,
2831
2605
  passed: gate.passed,
2832
2606
  scoped_out: gate.scoped_out,
2833
2607
  outcome: gate.outcome,
@@ -2843,7 +2617,7 @@ async function runPhaseBoundaryGate(ctx) {
2843
2617
  passed: gate.passed,
2844
2618
  commandsRun: gate.results.length,
2845
2619
  ...(gate.outcome === "scoped_out"
2846
- ? { reason: "target is not the audit-tools monorepo" }
2620
+ ? { reason: "no executable verification command declared; operator command required" }
2847
2621
  : {}),
2848
2622
  durationMs: Date.now() - gateStart,
2849
2623
  });
@@ -2885,7 +2659,7 @@ async function handleAllTerminalTransition(root, artifactsDir, state, store, opt
2885
2659
  obligation: state.status,
2886
2660
  note: "tool_owned_final_gate",
2887
2661
  });
2888
- const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner });
2662
+ const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner, testCommand: explicitFinalGateTestCommand(state) });
2889
2663
  await recordFinalGateOutcome({
2890
2664
  artifactsDir,
2891
2665
  state,
@@ -2896,7 +2670,7 @@ async function handleAllTerminalTransition(root, artifactsDir, state, store, opt
2896
2670
  passed: gate.passed,
2897
2671
  commandsRun: gate.results.length,
2898
2672
  ...(gate.outcome === "scoped_out"
2899
- ? { reason: "target is not the audit-tools monorepo" }
2673
+ ? { reason: "no executable verification command declared; operator command required" }
2900
2674
  : {}),
2901
2675
  durationMs: Date.now() - gateStart,
2902
2676
  });
@@ -3092,6 +2866,39 @@ Report this diagnostic to the user and stop. Do not attempt to advance the run.
3092
2866
  stopCondition: "Stop after reporting the diagnostic to the user.",
3093
2867
  });
3094
2868
  }
2869
+ function isPlanOnlyBoundary(state) {
2870
+ return state?.status === "implementing" || state?.status === "closing";
2871
+ }
2872
+ async function buildOperatorControlStep(root, artifactsDir, state, control) {
2873
+ const cancelled = control.mode === "cancelled";
2874
+ const active = control.mode === "active";
2875
+ const next = loaderCommand(["next-step", "--root", root, "--artifacts-dir", artifactsDir]);
2876
+ const resume = loaderCommand(["resume", "--root", root, "--artifacts-dir", artifactsDir]);
2877
+ return writeCurrentStep({
2878
+ stepKind: cancelled ? "operator_cancelled" : active ? "operator_resumed" : "operator_paused",
2879
+ status: cancelled ? "complete" : active ? "ready" : "blocked",
2880
+ runId: stateRunId(state), repoRoot: root, artifactsDir,
2881
+ prompt: active
2882
+ ? `# Remediation resumed\n\nAccepted work and the live workload binding are unchanged. Run \`${next}\` to derive the next continuation.`
2883
+ : cancelled
2884
+ ? "# Remediation cancelled\n\nThe operator cancelled this run. No further work will be accepted or dispatched. All run artifacts and accepted work are preserved."
2885
+ : `# Remediation paused\n\n${control.reason === "plan-only" ? "Planning finished; implementation has not been dispatched by this plan-only continuation." : "The operator paused this run."} The live phase, accepted items and workload binding are preserved.\n\nRun \`${resume}\`, then next-step, to continue from the live state.`,
2886
+ allowedCommands: cancelled ? [] : [active ? next : resume],
2887
+ stopCondition: cancelled ? "This run is terminally cancelled. Stop." : active ? "Run next-step to continue from the live state." : "Stop until the operator explicitly resumes this run.",
2888
+ artifactPaths: { operator_lifecycle: join(artifactsDir, OPERATOR_LIFECYCLE_FILENAME) },
2889
+ });
2890
+ }
2891
+ /** Operator commands serialize with every advancing or recovery writer. */
2892
+ export async function changeOperatorLifecycle(options) {
2893
+ const root = resolveRoot(options.root);
2894
+ const artifactsDir = resolveArtifactsDir(root, options.artifactsDir);
2895
+ await mkdir(artifactsDir, { recursive: true });
2896
+ const store = new StateStore(artifactsDir);
2897
+ return withFileLock(join(artifactsDir, "phase.lock"), async () => {
2898
+ const control = await store.setOperatorLifecycleUnderPhaseLock(options.action, options.hostReport);
2899
+ return buildOperatorControlStep(root, artifactsDir, await store.loadState(), control);
2900
+ });
2901
+ }
3095
2902
  export async function decideNextStep(options = {}) {
3096
2903
  const normalizedOptions = coerceJsonObjectArg(options, "decideNextStep options");
3097
2904
  const root = resolveRoot(normalizedOptions.root);
@@ -3233,6 +3040,7 @@ async function buildConfirmIntentStep(ctx) {
3233
3040
  // The proposal comes from the intake summary — the one file the host wrote at
3234
3041
  // synthesis. There is no draft checkpoint: the facts live in one place.
3235
3042
  const summary = (await readIntakeArtifacts(artifactsDir)).summary;
3043
+ const selection = (await readSourceManifest(intakePaths(artifactsDir).sourceManifest))?.finding_selection;
3236
3044
  // Closing action: DETECTED candidates, presented for the host to choose
3237
3045
  // from; the tool never selects one (owner decision 92b0e2dd7cfdc06d).
3238
3046
  const facts = await detectProjectFacts(root);
@@ -3263,7 +3071,7 @@ async function buildConfirmIntentStep(ctx) {
3263
3071
  ].join("\n") || "- None";
3264
3072
  const filtersBlock = Object.keys(summary.filters).length > 0
3265
3073
  ? `\`\`\`json\n${JSON.stringify(summary.filters, null, 2)}\n\`\`\``
3266
- : "(none — remediating all findings)";
3074
+ : selection ? "(none — keeping every finding in the selected source)" : "(none — remediating all findings)";
3267
3075
  prompt = `
3268
3076
  ${refusal}# Confirm Remediation Scope and Intent
3269
3077
 
@@ -3314,6 +3122,8 @@ Adjust \`filters\`, \`excluded_scope\`, \`must_not_touch\`, or \`free_form_inten
3314
3122
  narrow scope. Valid severities: ${VALID_SEVERITIES_PROSE}.
3315
3123
  Valid lenses: ${VALID_LENSES_PROSE}.
3316
3124
 
3125
+ If the operator requests independent contract conformance review, add \`"conformance_review": true\` before the first implementation handoff. It is off by default and the tool keeps the confirmed choice for this run.
3126
+
3317
3127
  Once written with \`"confirmed_by": "host"\`, run:
3318
3128
 
3319
3129
  \`${nextCommand}\`
@@ -3363,6 +3173,7 @@ Once the file is written, run:
3363
3173
  \`${nextCommand}\`
3364
3174
  `;
3365
3175
  }
3176
+ prompt += renderFindingSelection(selection);
3366
3177
  return writeCurrentStep({
3367
3178
  stepKind: "confirm_intent",
3368
3179
  status: "ready",
@@ -3377,138 +3188,6 @@ Once the file is written, run:
3377
3188
  },
3378
3189
  });
3379
3190
  }
3380
- // ---------------------------------------------------------------------------
3381
- // Deterministic free_form_intent interpretation at the call site (INV-S04)
3382
- // ---------------------------------------------------------------------------
3383
- //
3384
- // The IntentCheckpoint contract states `free_form_intent` is "interpreted into
3385
- // priority/lens/scope signals at planning time via freeFormIntentInterpreter.
3386
- // Never threaded verbatim into worker or dispatch prompts (INV-S04)." This is
3387
- // the call site that honours that: when a CONFIRMED checkpoint carries a
3388
- // free_form_intent, we run the shared deterministic interpreter HERE — never
3389
- // pass the raw string downstream — and persist the structured signals so
3390
- // planning consumes the encoded lens-weights/priority/scope, and so the
3391
- // unencodable clauses are surfaced (never silently dropped) rather than relying
3392
- // on an LLM-authored free-text `intent_interpretation`.
3393
- /** Sidecar artifact recording the deterministic interpretation of free_form_intent. */
3394
- export const INTENT_INTERPRETATION_FILENAME = "intent-interpretation.json";
3395
- // v1alpha2: unencodable_clauses carries identity-keyed records (clause_id +
3396
- // checkpoint_question), not bare strings — the shape the blocking consumer
3397
- // reads. A v1alpha1 sidecar (string[]) is stale and is repaired by
3398
- // re-derivation; it had no readers, so no migration path is owed.
3399
- export const INTENT_INTERPRETATION_SCHEMA_VERSION = "remediate-code-intent-interpretation/v1alpha2";
3400
- /**
3401
- * Interpret a confirmed checkpoint's `free_form_intent` via the shared
3402
- * deterministic interpreter and persist the structured signals to a sidecar
3403
- * artifact. Idempotent and best-effort: returns the persisted interpretation (or
3404
- * null when there is nothing to interpret / no confirmed checkpoint) and never
3405
- * throws into the decide loop. The raw `free_form_intent` string is NOT returned
3406
- * or threaded anywhere — only the structured `InterpretedIntent` is (INV-S04).
3407
- */
3408
- export async function interpretConfirmedCheckpointIntent(artifactsDir, checkpoint,
3409
- // Optional so the exported helper stays callable standalone; the decide loop
3410
- // always supplies it, because an unencodable clause is an operator-visible
3411
- // loss of intent and belongs in the durable log, not only on stderr.
3412
- runLogger) {
3413
- if (!checkpoint || checkpoint.confirmed_by !== "host")
3414
- return null;
3415
- const raw = checkpoint.free_form_intent;
3416
- if (typeof raw !== "string" || raw.trim().length === 0)
3417
- return null;
3418
- const interpreted = interpretFreeFormIntent(raw);
3419
- // The clause pipeline (interpretIntent) owns identity + blocking questions;
3420
- // the hint interpreter above owns lens/priority/scope signals. Both are
3421
- // deterministic draws over the same input.
3422
- const clauseResult = interpretIntent(raw);
3423
- const unencodable_clauses = [];
3424
- for (const clause of clauseResult.clauses) {
3425
- if (clause.encodable || !clause.checkpoint_question)
3426
- continue;
3427
- unencodable_clauses.push({
3428
- clause_id: clause.clause_id,
3429
- text: clause.text,
3430
- checkpoint_question: clause.checkpoint_question,
3431
- });
3432
- }
3433
- const persisted = {
3434
- schema_version: INTENT_INTERPRETATION_SCHEMA_VERSION,
3435
- interpreted,
3436
- unencodable_clauses,
3437
- created_at: new Date().toISOString(),
3438
- };
3439
- try {
3440
- await writeJsonFile(join(artifactsDir, INTENT_INTERPRETATION_FILENAME), persisted);
3441
- }
3442
- catch {
3443
- // Best-effort WRITE: a write failure must never crash the decide loop.
3444
- // Enforcement does not depend on it — the consumer re-derives when the
3445
- // sidecar is missing (readOrRepairIntentInterpretation).
3446
- }
3447
- if (unencodable_clauses.length > 0) {
3448
- const clauseTexts = unencodable_clauses.map((c) => c.text);
3449
- runLogger?.event({
3450
- phase: "next-step",
3451
- kind: "outcome",
3452
- obligation: "interpret_intent",
3453
- note: `intent_unencodable_clauses count=${String(unencodable_clauses.length)} ` +
3454
- `clauses=${clauseTexts.join("; ")}`,
3455
- });
3456
- process.stderr.write(`[remediate-code] free_form_intent: ${unencodable_clauses.length} ` +
3457
- `clause(s) could not be encoded as lens/priority/scope signals and ` +
3458
- `block planning until answered via constraint_clauses: ` +
3459
- `${clauseTexts.join("; ")}\n`);
3460
- }
3461
- return persisted;
3462
- }
3463
- /**
3464
- * Read the persisted intent interpretation — the LOAD-BEARING input to the
3465
- * constraint-clause gate — repairing it by re-derivation when it is missing,
3466
- * unparseable, or carries a stale schema_version. Returns null only when
3467
- * there is nothing to interpret (no confirmed checkpoint / empty intent).
3468
- */
3469
- export async function readOrRepairIntentInterpretation(artifactsDir, checkpoint, runLogger) {
3470
- if (!checkpoint || checkpoint.confirmed_by !== "host")
3471
- return null;
3472
- const raw = checkpoint.free_form_intent;
3473
- if (typeof raw !== "string" || raw.trim().length === 0)
3474
- return null;
3475
- const sidecarPath = join(artifactsDir, INTENT_INTERPRETATION_FILENAME);
3476
- try {
3477
- const parsed = parsePersistedIntentInterpretation(JSON.parse(await readFile(sidecarPath, "utf8")));
3478
- if (parsed)
3479
- return parsed;
3480
- }
3481
- catch {
3482
- // Missing or unreadable — fall through to repair.
3483
- }
3484
- return interpretConfirmedCheckpointIntent(artifactsDir, checkpoint, runLogger);
3485
- }
3486
- /** Pure shape gate for the sidecar: current version + record-shaped clauses, else null. */
3487
- function parsePersistedIntentInterpretation(parsed) {
3488
- if (isRecord(parsed) &&
3489
- parsed.schema_version === INTENT_INTERPRETATION_SCHEMA_VERSION &&
3490
- Array.isArray(parsed.unencodable_clauses) &&
3491
- parsed.unencodable_clauses.every((c) => isRecord(c) &&
3492
- typeof c.clause_id === "string" &&
3493
- typeof c.text === "string" &&
3494
- typeof c.checkpoint_question === "string")) {
3495
- return parsed;
3496
- }
3497
- return null;
3498
- }
3499
- /**
3500
- * Sync sidecar read for the obligation's derive scan. Returns the persisted
3501
- * interpretation, or null when the sidecar is missing, unreadable, or stale —
3502
- * the execute path repairs via {@link readOrRepairIntentInterpretation}.
3503
- */
3504
- function readPersistedIntentInterpretationSync(sidecarPath) {
3505
- try {
3506
- return parsePersistedIntentInterpretation(JSON.parse(readFileSync(sidecarPath, "utf8")));
3507
- }
3508
- catch {
3509
- return null;
3510
- }
3511
- }
3512
3191
  /**
3513
3192
  * Narrow a nullable engine state to non-null inside an executor whose `derive`
3514
3193
  * only marks it actionable when the state is present — a violation is an engine
@@ -3542,7 +3221,7 @@ export const PRE_INTAKE_PRIORITY = [
3542
3221
  */
3543
3222
  export function buildPreIntakeObligations(ctx, snapshot) {
3544
3223
  const { artifactsDir, inputResolution } = ctx;
3545
- const { existingCheckpoint, rejectedCheckpointFields, resumeAck, entryState, suppliedInputUnchanged, guidanceFileSupplied } = snapshot;
3224
+ const { existingCheckpoint, rejectedCheckpointFields, resumeAck, entryState, suppliedInputUnchanged, inputSelectionChanged, selectionAlreadyBound, selectedSourceChanged, guidanceFileSupplied } = snapshot;
3546
3225
  const ip = intakePaths(artifactsDir);
3547
3226
  const checkpointPath = join(artifactsDir, "intent_checkpoint.json");
3548
3227
  const ackPath = join(artifactsDir, "confirm_resume_ack.json");
@@ -3561,18 +3240,18 @@ export function buildPreIntakeObligations(ctx, snapshot) {
3561
3240
  // "unchanged" notion, so any guidance file against an advanced run conflicts;
3562
3241
  // bare follow-ups don't set the flag). Derives from the frozen entry state.
3563
3242
  id: "input_conflict",
3564
- derive: () => ((inputResolution.supplied && !suppliedInputUnchanged) ||
3565
- guidanceFileSupplied) &&
3566
- entryState != null &&
3567
- entryState.status !== "pending"
3243
+ derive: () => (selectionAlreadyBound && inputSelectionChanged) ||
3244
+ (((inputResolution.supplied && !suppliedInputUnchanged) ||
3245
+ inputSelectionChanged || guidanceFileSupplied) &&
3246
+ entryState != null && entryState.status !== "pending")
3568
3247
  ? "missing"
3569
3248
  : "satisfied",
3570
3249
  execute: async (_state, c) => {
3571
- const s = requireState(entryState);
3572
- await c.countStep(s);
3250
+ const s = entryState ?? { status: "pending" };
3251
+ await c.countStep(entryState);
3573
3252
  return {
3574
3253
  kind: "emit",
3575
- step: await handleInputConflict(c.root, c.artifactsDir, s, c.inputResolution),
3254
+ step: await handleInputConflict(c.root, c.artifactsDir, s, c.inputResolution, selectionAlreadyBound && inputSelectionChanged ? { sourceChanged: selectedSourceChanged === true } : undefined),
3576
3255
  };
3577
3256
  },
3578
3257
  },
@@ -3799,6 +3478,7 @@ Then run:
3799
3478
  * cascade tail's guard order exactly so selection cannot drift.
3800
3479
  */
3801
3480
  export const MAIN_PRIORITY = [
3481
+ "operator_plan_only",
3802
3482
  "waiting_for_clarification",
3803
3483
  "waiting_for_triage",
3804
3484
  "planning_documentable",
@@ -3825,6 +3505,14 @@ export function buildMainObligations(ctx) {
3825
3505
  const clarificationResolutionPath = join(artifactsDir, "clarification_resolution.json");
3826
3506
  const triageResolutionPath = join(artifactsDir, "triage_resolution.json");
3827
3507
  return [
3508
+ {
3509
+ id: "operator_plan_only",
3510
+ derive: (state) => options.planOnly === true && isPlanOnlyBoundary(state) ? "missing" : "satisfied",
3511
+ execute: async (state) => {
3512
+ const control = await store.setOperatorLifecycleUnderPhaseLock("pause", undefined, "plan-only");
3513
+ return { kind: "emit", step: await buildOperatorControlStep(root, artifactsDir, state, control) };
3514
+ },
3515
+ },
3828
3516
  {
3829
3517
  // Plan-phase clarification wait: apply a resolution if present (transition →
3830
3518
  // re-scan), else surface the wait step.
@@ -4052,6 +3740,32 @@ async function advanceUnderPhaseLock(deps) {
4052
3740
  // Loaded FRESH under the mutex: a peer may have advanced and persisted state
4053
3741
  // between the entry read and this process winning the lock.
4054
3742
  let state = await store.loadState();
3743
+ let control = await store.loadOperatorLifecycle();
3744
+ if (control && control.mode !== "active") {
3745
+ return buildOperatorControlStep(root, artifactsDir, state, control);
3746
+ }
3747
+ if (options.planOnly === true)
3748
+ control = await store.setOperatorLifecycleUnderPhaseLock("plan-only");
3749
+ // Covers a crash after planning saved its phase but before its pause was
3750
+ // persisted. No override, guidance, ingestion or dispatch may slip through.
3751
+ if (control?.plan_only && isPlanOnlyBoundary(state) && !options.forceReplan) {
3752
+ control = await store.setOperatorLifecycleUnderPhaseLock("pause", undefined, "plan-only");
3753
+ return buildOperatorControlStep(root, artifactsDir, state, control);
3754
+ }
3755
+ if (options.guidanceFile && options.guidanceText !== undefined)
3756
+ throw new Error("Choose --guidance or --guidance-file, not both.");
3757
+ if (options.guidanceFile)
3758
+ applyGuidanceFile(artifactsDir, options.guidanceFile);
3759
+ if (options.guidanceText !== undefined)
3760
+ applyGuidanceText(artifactsDir, options.guidanceText);
3761
+ if (options.verificationCommand !== undefined && state?.plan) {
3762
+ if (parseCommandString(options.verificationCommand).length === 0) {
3763
+ throw new Error("--verification-command must name an executable command");
3764
+ }
3765
+ state.plan.test_command = options.verificationCommand;
3766
+ state.plan.test_command_source = "explicit";
3767
+ await store.saveState(state);
3768
+ }
4055
3769
  // step_count is incremented once per host invocation. The `counted` flag guards
4056
3770
  // the shared `countStep` closure so the forceReplan preamble, the pre-intake
4057
3771
  // obligation executors, and the post-intake count point can never double-count
@@ -4120,7 +3834,14 @@ async function advanceUnderPhaseLock(deps) {
4120
3834
  // Whether a supplied `--input` matches the input the existing run was built
4121
3835
  // from — so re-passing the same `--input` (the loader does this each next-step)
4122
3836
  // resumes rather than tripping the input_conflict gate.
4123
- const suppliedInputUnchanged = suppliedInputMatchesRun(inputResolution, await readSourceManifest(intakePaths(artifactsDir).sourceManifest));
3837
+ const sourceManifest = await readSourceManifest(intakePaths(artifactsDir).sourceManifest);
3838
+ const requestedSelection = requestedFindingSelection(options);
3839
+ const recordedSelection = sourceManifest?.finding_selection;
3840
+ const selectedSourceChanged = recordedSelection !== undefined && (!existsSync(recordedSelection.source_path) ||
3841
+ hashContent(await readFile(recordedSelection.source_path, "utf8")) !== recordedSelection.source_hash);
3842
+ const inputSelectionChanged = selectedSourceChanged ||
3843
+ (requestedSelection !== undefined && JSON.stringify(requestedSelection) !== JSON.stringify(recordedSelection?.criteria));
3844
+ const suppliedInputUnchanged = !inputSelectionChanged && suppliedInputMatchesRun(inputResolution, sourceManifest);
4124
3845
  // The linear pre-intake gates run as obligations through the shared advance
4125
3846
  // loop. An emit returns to the host; a transition re-scans within this call;
4126
3847
  // exhausting them (step === null) means the run is past intake and falls
@@ -4128,7 +3849,7 @@ async function advanceUnderPhaseLock(deps) {
4128
3849
  const ctx = {
4129
3850
  root,
4130
3851
  artifactsDir,
4131
- options,
3852
+ options: { ...options, planOnly: control?.plan_only === true },
4132
3853
  runLogger,
4133
3854
  store,
4134
3855
  inputResolution,
@@ -4142,6 +3863,9 @@ async function advanceUnderPhaseLock(deps) {
4142
3863
  resumeAck,
4143
3864
  entryState: state,
4144
3865
  suppliedInputUnchanged,
3866
+ inputSelectionChanged,
3867
+ selectionAlreadyBound: recordedSelection !== undefined,
3868
+ selectedSourceChanged,
4145
3869
  guidanceFileSupplied: Boolean(options.guidanceFileSupplied),
4146
3870
  }),
4147
3871
  }, state, ctx);