audit-tools 0.52.0 → 0.52.1

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 (258) hide show
  1. package/dist/audit/cli/auditStep.d.ts.map +1 -1
  2. package/dist/audit/cli/auditStep.js +2 -2
  3. package/dist/audit/cli/auditStep.js.map +1 -1
  4. package/dist/audit/cli/charterClarificationPrompt.d.ts +16 -8
  5. package/dist/audit/cli/charterClarificationPrompt.d.ts.map +1 -1
  6. package/dist/audit/cli/charterClarificationPrompt.js +95 -42
  7. package/dist/audit/cli/charterClarificationPrompt.js.map +1 -1
  8. package/dist/audit/cli/charterComparisonPrompt.d.ts +16 -6
  9. package/dist/audit/cli/charterComparisonPrompt.d.ts.map +1 -1
  10. package/dist/audit/cli/charterComparisonPrompt.js +76 -29
  11. package/dist/audit/cli/charterComparisonPrompt.js.map +1 -1
  12. package/dist/audit/cli/charterExtractionPrompt.d.ts +5 -4
  13. package/dist/audit/cli/charterExtractionPrompt.d.ts.map +1 -1
  14. package/dist/audit/cli/charterExtractionPrompt.js +64 -42
  15. package/dist/audit/cli/charterExtractionPrompt.js.map +1 -1
  16. package/dist/audit/cli/charterFidelityPrompt.d.ts +17 -4
  17. package/dist/audit/cli/charterFidelityPrompt.d.ts.map +1 -1
  18. package/dist/audit/cli/charterFidelityPrompt.js +72 -21
  19. package/dist/audit/cli/charterFidelityPrompt.js.map +1 -1
  20. package/dist/audit/cli/dispatch/hostHandoff.d.ts +23 -1
  21. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  22. package/dist/audit/cli/dispatch/hostHandoff.js +204 -41
  23. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  24. package/dist/audit/cli/dispatch/packetFilter.d.ts +8 -0
  25. package/dist/audit/cli/dispatch/packetFilter.d.ts.map +1 -1
  26. package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
  27. package/dist/audit/cli/fanoutLanes.js +20 -4
  28. package/dist/audit/cli/fanoutLanes.js.map +1 -1
  29. package/dist/audit/cli/laneValidators.d.ts +19 -7
  30. package/dist/audit/cli/laneValidators.d.ts.map +1 -1
  31. package/dist/audit/cli/laneValidators.js +73 -18
  32. package/dist/audit/cli/laneValidators.js.map +1 -1
  33. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  34. package/dist/audit/cli/nextStepCommand.js +13 -6
  35. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  36. package/dist/audit/cli/nextStepHelpers.d.ts +25 -2
  37. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  38. package/dist/audit/cli/nextStepHelpers.js +105 -35
  39. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  40. package/dist/audit/cli/recoverSubmissionCommand.d.ts.map +1 -1
  41. package/dist/audit/cli/recoverSubmissionCommand.js +89 -4
  42. package/dist/audit/cli/recoverSubmissionCommand.js.map +1 -1
  43. package/dist/audit/cli/sampleRunCommand.d.ts +10 -0
  44. package/dist/audit/cli/sampleRunCommand.d.ts.map +1 -1
  45. package/dist/audit/cli/semanticReviewStep.d.ts +4 -4
  46. package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
  47. package/dist/audit/cli/semanticReviewStep.js +35 -71
  48. package/dist/audit/cli/semanticReviewStep.js.map +1 -1
  49. package/dist/audit/contracts/findingContractPrompt.d.ts.map +1 -1
  50. package/dist/audit/contracts/findingContractPrompt.js +7 -1
  51. package/dist/audit/contracts/findingContractPrompt.js.map +1 -1
  52. package/dist/audit/contracts/workerSchemas.d.ts +280 -4
  53. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  54. package/dist/audit/contracts/workerSchemas.js +24 -2
  55. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  56. package/dist/audit/orchestrator/charterClarificationExecutor.d.ts.map +1 -1
  57. package/dist/audit/orchestrator/charterClarificationExecutor.js +39 -0
  58. package/dist/audit/orchestrator/charterClarificationExecutor.js.map +1 -1
  59. package/dist/audit/orchestrator/charterExtractionExecutor.d.ts +12 -5
  60. package/dist/audit/orchestrator/charterExtractionExecutor.d.ts.map +1 -1
  61. package/dist/audit/orchestrator/charterExtractionExecutor.js +39 -9
  62. package/dist/audit/orchestrator/charterExtractionExecutor.js.map +1 -1
  63. package/dist/audit/orchestrator/designReviewPrompt.d.ts +7 -0
  64. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  65. package/dist/audit/orchestrator/designReviewPrompt.js +174 -19
  66. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  67. package/dist/audit/orchestrator/intentCheckpointGate.d.ts.map +1 -1
  68. package/dist/audit/orchestrator/intentCheckpointGate.js +1 -5
  69. package/dist/audit/orchestrator/intentCheckpointGate.js.map +1 -1
  70. package/dist/audit/orchestrator/requeueCommand.d.ts +8 -0
  71. package/dist/audit/orchestrator/requeueCommand.d.ts.map +1 -1
  72. package/dist/audit/orchestrator/selectiveDeepening/lensVerification.d.ts.map +1 -1
  73. package/dist/audit/orchestrator/selectiveDeepening/lensVerification.js +94 -71
  74. package/dist/audit/orchestrator/selectiveDeepening/lensVerification.js.map +1 -1
  75. package/dist/audit/orchestrator/selectiveDeepening/shared.d.ts +0 -2
  76. package/dist/audit/orchestrator/selectiveDeepening/shared.d.ts.map +1 -1
  77. package/dist/audit/orchestrator/selectiveDeepening/shared.js +9 -2
  78. package/dist/audit/orchestrator/selectiveDeepening/shared.js.map +1 -1
  79. package/dist/audit/orchestrator/selectiveDeepening/stewardFollowup.d.ts.map +1 -1
  80. package/dist/audit/orchestrator/selectiveDeepening/stewardFollowup.js +28 -4
  81. package/dist/audit/orchestrator/selectiveDeepening/stewardFollowup.js.map +1 -1
  82. package/dist/audit/reporting/synthesis.d.ts +16 -3
  83. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  84. package/dist/audit/reporting/synthesis.js +40 -11
  85. package/dist/audit/reporting/synthesis.js.map +1 -1
  86. package/dist/audit/reporting/synthesisNarrativePrompt.d.ts +26 -1
  87. package/dist/audit/reporting/synthesisNarrativePrompt.d.ts.map +1 -1
  88. package/dist/audit/reporting/synthesisNarrativePrompt.js +53 -13
  89. package/dist/audit/reporting/synthesisNarrativePrompt.js.map +1 -1
  90. package/dist/audit/systemic/secondOrderAdversaryPrompt.d.ts.map +1 -1
  91. package/dist/audit/systemic/secondOrderAdversaryPrompt.js +67 -40
  92. package/dist/audit/systemic/secondOrderAdversaryPrompt.js.map +1 -1
  93. package/dist/audit/systemic/systemicChallengeLoop.d.ts.map +1 -1
  94. package/dist/audit/systemic/systemicChallengeLoop.js +13 -1
  95. package/dist/audit/systemic/systemicChallengeLoop.js.map +1 -1
  96. package/dist/audit/types/conceptualAdjudication.d.ts +26 -2
  97. package/dist/audit/types/conceptualAdjudication.d.ts.map +1 -1
  98. package/dist/audit/types/conceptualAdjudication.js +26 -7
  99. package/dist/audit/types/conceptualAdjudication.js.map +1 -1
  100. package/dist/audit/types.d.ts +279 -0
  101. package/dist/audit/types.d.ts.map +1 -1
  102. package/dist/audit/types.js +52 -0
  103. package/dist/audit/types.js.map +1 -1
  104. package/dist/audit/validation/auditResults.d.ts.map +1 -1
  105. package/dist/audit/validation/auditResults.js +51 -6
  106. package/dist/audit/validation/auditResults.js.map +1 -1
  107. package/dist/audit/validation/ingestIssueCodes.d.ts +3 -1
  108. package/dist/audit/validation/ingestIssueCodes.d.ts.map +1 -1
  109. package/dist/audit/validation/ingestIssueCodes.js +27 -15
  110. package/dist/audit/validation/ingestIssueCodes.js.map +1 -1
  111. package/dist/remediate/contractPipeline/sketchSource.d.ts +13 -3
  112. package/dist/remediate/contractPipeline/sketchSource.d.ts.map +1 -1
  113. package/dist/remediate/contractPipeline/sketchSource.js +14 -8
  114. package/dist/remediate/contractPipeline/sketchSource.js.map +1 -1
  115. package/dist/remediate/intake.d.ts +140 -36
  116. package/dist/remediate/intake.d.ts.map +1 -1
  117. package/dist/remediate/intake.js +159 -38
  118. package/dist/remediate/intake.js.map +1 -1
  119. package/dist/remediate/intent/checkpointFilter.d.ts +1 -2
  120. package/dist/remediate/intent/checkpointFilter.d.ts.map +1 -1
  121. package/dist/remediate/intent/checkpointFilter.js +1 -5
  122. package/dist/remediate/intent/checkpointFilter.js.map +1 -1
  123. package/dist/remediate/phases/close.d.ts.map +1 -1
  124. package/dist/remediate/phases/close.js +2 -2
  125. package/dist/remediate/phases/close.js.map +1 -1
  126. package/dist/remediate/phases/plan.d.ts +1 -1
  127. package/dist/remediate/review/reviewGate.d.ts +68 -39
  128. package/dist/remediate/review/reviewGate.d.ts.map +1 -1
  129. package/dist/remediate/review/reviewGate.js +133 -50
  130. package/dist/remediate/review/reviewGate.js.map +1 -1
  131. package/dist/remediate/state/store.d.ts +1 -2
  132. package/dist/remediate/state/store.d.ts.map +1 -1
  133. package/dist/remediate/state/store.js +124 -3
  134. package/dist/remediate/state/store.js.map +1 -1
  135. package/dist/remediate/state/types.d.ts +28 -4
  136. package/dist/remediate/state/types.d.ts.map +1 -1
  137. package/dist/remediate/state/types.js.map +1 -1
  138. package/dist/remediate/steps/contractPipeline.d.ts +17 -1
  139. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  140. package/dist/remediate/steps/contractPipeline.js +100 -52
  141. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  142. package/dist/remediate/steps/contractPipelinePrompts.d.ts +32 -8
  143. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  144. package/dist/remediate/steps/contractPipelinePrompts.js +145 -131
  145. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  146. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +6 -4
  147. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  148. package/dist/remediate/steps/dispatch/hostHandoff.js +291 -155
  149. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  150. package/dist/remediate/steps/intakeResolver.d.ts +1 -1
  151. package/dist/remediate/steps/intakeResolver.d.ts.map +1 -1
  152. package/dist/remediate/steps/intakeResolver.js +50 -49
  153. package/dist/remediate/steps/intakeResolver.js.map +1 -1
  154. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  155. package/dist/remediate/steps/nextStep.js +315 -288
  156. package/dist/remediate/steps/nextStep.js.map +1 -1
  157. package/dist/remediate/steps/prompts.d.ts +10 -3
  158. package/dist/remediate/steps/prompts.d.ts.map +1 -1
  159. package/dist/remediate/steps/prompts.js +191 -228
  160. package/dist/remediate/steps/prompts.js.map +1 -1
  161. package/dist/remediate/steps/types.d.ts +2 -2
  162. package/dist/remediate/steps/types.d.ts.map +1 -1
  163. package/dist/remediate/steps/types.js +10 -3
  164. package/dist/remediate/steps/types.js.map +1 -1
  165. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  166. package/dist/remediate/validation/artifacts.js +5 -22
  167. package/dist/remediate/validation/artifacts.js.map +1 -1
  168. package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
  169. package/dist/remediate/validation/contractPipeline.js +25 -8
  170. package/dist/remediate/validation/contractPipeline.js.map +1 -1
  171. package/dist/remediate/validation/remediationState.d.ts +0 -1
  172. package/dist/remediate/validation/remediationState.d.ts.map +1 -1
  173. package/dist/remediate/validation/remediationState.js +1 -20
  174. package/dist/remediate/validation/remediationState.js.map +1 -1
  175. package/dist/shared/decompose/charterClarification.d.ts +7 -0
  176. package/dist/shared/decompose/charterClarification.d.ts.map +1 -1
  177. package/dist/shared/decompose/charterClarification.js +8 -1
  178. package/dist/shared/decompose/charterClarification.js.map +1 -1
  179. package/dist/shared/decompose/charterExtraction.d.ts +204 -16
  180. package/dist/shared/decompose/charterExtraction.d.ts.map +1 -1
  181. package/dist/shared/decompose/charterExtraction.js +70 -13
  182. package/dist/shared/decompose/charterExtraction.js.map +1 -1
  183. package/dist/shared/decompose/systemicChallenge.d.ts +42 -1
  184. package/dist/shared/decompose/systemicChallenge.d.ts.map +1 -1
  185. package/dist/shared/decompose/systemicChallenge.js +51 -0
  186. package/dist/shared/decompose/systemicChallenge.js.map +1 -1
  187. package/dist/shared/index.d.ts +14 -11
  188. package/dist/shared/index.d.ts.map +1 -1
  189. package/dist/shared/index.js +16 -9
  190. package/dist/shared/index.js.map +1 -1
  191. package/dist/shared/io/stepContractWriter.d.ts.map +1 -1
  192. package/dist/shared/io/stepContractWriter.js +13 -4
  193. package/dist/shared/io/stepContractWriter.js.map +1 -1
  194. package/dist/shared/submission/hostHandoffCore.d.ts +7 -0
  195. package/dist/shared/submission/hostHandoffCore.d.ts.map +1 -1
  196. package/dist/shared/submission/hostHandoffCore.js +15 -2
  197. package/dist/shared/submission/hostHandoffCore.js.map +1 -1
  198. package/dist/shared/submission/hostResultOutcomes.d.ts +60 -10
  199. package/dist/shared/submission/hostResultOutcomes.d.ts.map +1 -1
  200. package/dist/shared/submission/hostResultOutcomes.js +42 -10
  201. package/dist/shared/submission/hostResultOutcomes.js.map +1 -1
  202. package/dist/shared/submission/ingestReport.d.ts +54 -0
  203. package/dist/shared/submission/ingestReport.d.ts.map +1 -0
  204. package/dist/shared/submission/ingestReport.js +44 -0
  205. package/dist/shared/submission/ingestReport.js.map +1 -0
  206. package/dist/shared/submission/ingestionChecks.d.ts +8 -13
  207. package/dist/shared/submission/ingestionChecks.d.ts.map +1 -1
  208. package/dist/shared/submission/ingestionChecks.js +9 -14
  209. package/dist/shared/submission/ingestionChecks.js.map +1 -1
  210. package/dist/shared/submission/submissionClassifier.d.ts +6 -0
  211. package/dist/shared/submission/submissionClassifier.d.ts.map +1 -1
  212. package/dist/shared/submission/submissionClassifier.js +16 -0
  213. package/dist/shared/submission/submissionClassifier.js.map +1 -1
  214. package/dist/shared/submission/workerPromptBinding.d.ts +18 -0
  215. package/dist/shared/submission/workerPromptBinding.d.ts.map +1 -0
  216. package/dist/shared/submission/workerPromptBinding.js +38 -0
  217. package/dist/shared/submission/workerPromptBinding.js.map +1 -0
  218. package/dist/shared/tooling/exec.d.ts +24 -0
  219. package/dist/shared/tooling/exec.d.ts.map +1 -1
  220. package/dist/shared/tooling/exec.js +30 -0
  221. package/dist/shared/tooling/exec.js.map +1 -1
  222. package/dist/shared/types/charter.d.ts +143 -7
  223. package/dist/shared/types/charter.d.ts.map +1 -1
  224. package/dist/shared/types/charter.js +63 -7
  225. package/dist/shared/types/charter.js.map +1 -1
  226. package/dist/shared/types/charterPacket.d.ts +13 -0
  227. package/dist/shared/types/charterPacket.d.ts.map +1 -1
  228. package/dist/shared/types/charterPacket.js.map +1 -1
  229. package/dist/shared/types/contractPipeline/obligations.d.ts +30 -11
  230. package/dist/shared/types/contractPipeline/obligations.d.ts.map +1 -1
  231. package/dist/shared/types/contractPipeline/obligations.js +6 -1
  232. package/dist/shared/types/contractPipeline/obligations.js.map +1 -1
  233. package/dist/shared/types/contractPropertyShapes.d.ts.map +1 -1
  234. package/dist/shared/types/contractPropertyShapes.js +13 -0
  235. package/dist/shared/types/contractPropertyShapes.js.map +1 -1
  236. package/dist/shared/types/finding.d.ts +154 -15
  237. package/dist/shared/types/finding.d.ts.map +1 -1
  238. package/dist/shared/types/finding.js +41 -5
  239. package/dist/shared/types/finding.js.map +1 -1
  240. package/dist/shared/types/intentCheckpoint.d.ts +26 -40
  241. package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
  242. package/dist/shared/types/intentCheckpoint.js +30 -33
  243. package/dist/shared/types/intentCheckpoint.js.map +1 -1
  244. package/dist/shared/types/remediationOutcome.d.ts +8 -8
  245. package/dist/shared/validation/citationGrounding.d.ts +29 -5
  246. package/dist/shared/validation/citationGrounding.d.ts.map +1 -1
  247. package/dist/shared/validation/citationGrounding.js +56 -9
  248. package/dist/shared/validation/citationGrounding.js.map +1 -1
  249. package/dist/shared/validation/designFindingGrounding.d.ts +30 -9
  250. package/dist/shared/validation/designFindingGrounding.d.ts.map +1 -1
  251. package/dist/shared/validation/designFindingGrounding.js +32 -12
  252. package/dist/shared/validation/designFindingGrounding.js.map +1 -1
  253. package/docs/audit-pkg/contracts.md +4 -5
  254. package/package.json +1 -1
  255. package/schemas/audit_result.schema.json +51 -0
  256. package/schemas/audit_results.schema.json +51 -0
  257. package/schemas/audit_task.schema.json +44 -0
  258. package/schemas/finding.schema.json +4 -0
@@ -1,14 +1,15 @@
1
- // 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
1
+ // 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
2
  // (the free-form branch's write
3
3
  // scope is normalized — a backslash-spelled citation no longer wedges prepare)
4
4
  import { AUDIT_TOOLS_DIRNAME } from "../../shared/io/auditToolsPaths.js";
5
5
  import { loadRemediateSessionConfig } from "./sessionConfigLoad.js";
6
+ import { z } from "zod";
6
7
  import { existsSync, readFileSync, statSync } from "node:fs";
7
8
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
9
  import { dirname, join, resolve } from "node:path";
9
10
  import { StateStore } from "../state/store.js";
10
11
  // IO / validation / rendering helpers
11
- import { discardOnSchemaVersionMismatch, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, auditReadOf, formatValidationIssues, isMissingObservation, isRecord, withFsRetry, RunLogger, coerceJsonObjectArg, renderPromptCommand, headCommit, projectAuditFindingsReportSubset,
12
+ import { discardOnSchemaVersionMismatch, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, auditReadOf, formatValidationIssues, isRecord, renderIngestReportLines, withFsRetry, RunLogger, coerceJsonObjectArg, renderPromptCommand, headCommit, projectAuditFindingsReportSubset,
12
13
  // obligation engine + intent
13
14
  interpretFreeFormIntent, interpretIntent, unresolvedFromClauses, advance, describeStoppedFold, decideFrictionTriage, buildFrictionTriageBlock, linkFrictionRunIds,
14
15
  // domain constants
@@ -19,7 +20,7 @@ import { runTriagePhase } from "../phases/triage.js";
19
20
  import { runClosePhase } from "../phases/close.js";
20
21
  import { validateRemediationPlan } from "../validation/remediationState.js";
21
22
  import { readExtractedPlanIfPresent, } from "./dispatch/marshal.js";
22
- import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingBlocks, precomputeRecoveryTestVerdicts, prepareRemediationHostHandoff, workloadBindingIdentity, } from "./dispatch/hostHandoff.js";
23
+ import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingBlocks, precomputeRecoveryTestVerdicts, prepareRemediationHostHandoff, remediationIssueRemedy, workloadBindingIdentity, } from "./dispatch/hostHandoff.js";
23
24
  import { FileLockTimeoutError, withFileLock, } from "../../shared/io/fileLock.js";
24
25
  import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, } from "../../shared/io/auditToolsPaths.js";
25
26
  import { callerWorkingDirectory, discoverRepoRoot, resolveRepoRoot, } from "../../shared/io/repoRoot.js";
@@ -35,13 +36,13 @@ import { renderGateAttribution, worktreeContentId, } from "./gateCommands.js";
35
36
  import { buildNextContractPipelineStep, shouldEnterContractPipeline, readSeedAuditRead, writePathASeedFromFindings, normalizeBlockTouchedFiles, checkWriteScopePathsAgainstTrackedTree, } from "./contractPipeline.js";
36
37
  import { compareCodeUnits } from "../../shared/compareCodeUnits.js";
37
38
  import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
38
- import { buildReviewRequest, applyReviewResolution, isResolutionForRequest, screenResolutionIds, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
39
+ import { buildReviewRequest, applyReviewResolution, parseReviewResolution, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
39
40
  import { buildAutonomousReviewDecision } from "../review/autonomousGate.js";
40
41
  import { runFindingFilterPass } from "../findingFilter.js";
41
42
  import { droppedFindingsRecordPath, renderDroppedFindingsRecord, } from "../droppedFindingsRecord.js";
42
- import { intakePaths, isIntakeReady, manifestIsInputBound, readIntakeArtifacts, readProjectFacts, writeProjectFacts, resolveManifestSources, } from "../intake.js";
43
+ import { intakePaths, isIntakeReady, manifestIsInputBound, readIntakeArtifacts, readProjectFacts, readSourceManifest, writeProjectFacts, resolveManifestSources, writeRemediationBrief, } from "../intake.js";
43
44
  import { ensureIntakeRiskSignal, readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, findingRiskEvidence, distinctAffectedFiles, } from "../riskSignal.js";
44
- import { readIntentCheckpoint, readIntentCheckpointLenient, } from "audit-tools/shared";
45
+ import { isLegacyDraftCheckpoint, readIntentCheckpoint, readIntentCheckpointLenient, } from "audit-tools/shared";
45
46
  import { ambiguityReviewPrompt, clarificationPrompt, collectIntakeClarificationsPrompt, collectStartingPointPrompt, extractedPlanDiscardedPrompt, loaderCommand, reviewApprovalPrompt, synthesizeIntakePrompt, triagePrompt, } from "./prompts.js";
46
47
  // Single-sourced prose renders of the canonical lens / severity vocabularies
47
48
  // (`audit-tools/shared` `LENSES` / `SEVERITIES`) for the intent-checkpoint
@@ -950,34 +951,24 @@ export async function recoverIngestHostResults(options) {
950
951
  return ingested;
951
952
  }
952
953
  /**
953
- * Render the host-facing result status for an ingest, split by outcome.
954
+ * The ingest report for the implement-dispatch prompt, as prompt lines.
954
955
  *
955
- * Takes the whole summary rather than just its issues, because the split needs
956
- * BOTH halves: which items were refused (from the issues) and which landed
957
- * without a result (from `work_item_outcomes`, where the run's corroborated
958
- * commit for the item is what separates that case from plain unfinished work).
956
+ * Takes the whole summary rather than just its issues, because the report needs
957
+ * BOTH halves: the classified issues, and the items that landed without a
958
+ * result (from `work_item_outcomes`, where the run's corroborated commit for
959
+ * the item is what separates that case from plain unfinished work). The
960
+ * sections are the shared renderer's; only the remedy map is this draw's.
959
961
  */
960
- function remediationResultDiagnostics(ingested) {
961
- const describe = (issue) => `- ${issue.work_item_id ? `\`${issue.work_item_id}\`: ` : ""}${issue.message}${issue.result_path ? ` (\`${issue.result_path}\`)` : ""}`;
962
- const section = (heading, lines) => lines.length === 0 ? "" : `\n${heading}\n\n${lines.join("\n")}\n`;
963
- const rejected = ingested.issues.filter((issue) => !isMissingObservation(issue));
964
- const landedWithoutResult = [...ingested.work_item_outcomes]
965
- .filter(([, outcome]) => outcome === "missing_result_with_commit")
966
- .map(([id]) => id)
967
- .sort(compareCodeUnits);
968
- const notYetWritten = ingested.issues.filter((issue) => isMissingObservation(issue));
969
- const body = section("## Results already landed, whose result file is missing", landedWithoutResult.map((id) => `- \`${id}\`: this item's edits are IN this repository — a corroborated commit landed ` +
970
- `for it and its changed files are recorded — but no result file exists at its bound ` +
971
- `\`result_path\`. This is PARTIAL PROGRESS, not absent work: write the result for the ` +
972
- `commit that is already there rather than redoing the edit.`)) +
973
- section("## Result status requiring attention", rejected.map(describe)) +
974
- section("## Results not yet written", notYetWritten.map(describe));
975
- if (body === "")
976
- return "";
977
- return (body +
978
- "\nThe workload was restored from its tool-owned digest when necessary. Repair or\n" +
979
- "complete only the named result files; do not rewrite the workload or its\n" +
980
- "baseline.\n");
962
+ function remediationIngestReportLines(ingested) {
963
+ return renderIngestReportLines({
964
+ issues: ingested.issues,
965
+ remedy: remediationIssueRemedy,
966
+ workload: "named_above",
967
+ landedWithoutResult: [...ingested.work_item_outcomes]
968
+ .filter(([, outcome]) => outcome === "missing_result_with_commit")
969
+ .map(([id]) => id)
970
+ .sort(compareCodeUnits),
971
+ });
981
972
  }
982
973
  async function buildImplementDispatchStep(ctx) {
983
974
  const { root, artifactsDir, state, store, runLogger } = ctx;
@@ -1053,17 +1044,29 @@ async function buildImplementDispatchStep(ctx) {
1053
1044
  // RENDER — a channel that survives exactly as long as the host reads this one
1054
1045
  // step, and that is reached only when nothing was accepted.
1055
1046
  //
1056
- // THREE STATES, THREE SECTIONS, because they have three different remedies and
1057
- // a reader must not have to parse prose to tell them apart (the measured
1058
- // friction: a host parser special-casing "no result file exists"):
1059
- // rejected — something was written and refused; repair it.
1060
- // missing + commit — the edits LANDED but no result exists; write the
1061
- // result FOR THE COMMIT THAT IS ALREADY THERE.
1062
- // missing, no commit — nothing yet; write it when the work is done.
1063
- // The classification is on CODES, never message text, so rewording a message
1064
- // cannot move an item between sections.
1065
- const diagnostics = remediationResultDiagnostics(ingested);
1047
+ // Each issue lands under the section its CODE's remedy names, never under one
1048
+ // its message text suggests, so rewording a message cannot move an item
1049
+ // between sections (the measured friction: a host parser special-casing "no
1050
+ // result file exists").
1051
+ const report = remediationIngestReportLines(ingested);
1066
1052
  const nextCommand = loaderCommand("next-step");
1053
+ const promptLines = [
1054
+ "# Implement the remediation work items",
1055
+ "",
1056
+ "Read the workload file:",
1057
+ "",
1058
+ `\`${handoff.workload_path}\``,
1059
+ "",
1060
+ "The file lists every work item that is ready now. Give each item's prompt to a worker. You choose the order, the groups, and how many run at the same time.",
1061
+ "",
1062
+ ...report,
1063
+ ...(report.length > 0 ? ["Change only the result files named above.", ""] : []),
1064
+ "Do not edit the workload file.",
1065
+ "",
1066
+ "When each item has its commit on HEAD and its result file, run:",
1067
+ "",
1068
+ `\`${nextCommand}\``,
1069
+ ];
1067
1070
  return {
1068
1071
  kind: "emit",
1069
1072
  step: await writeCurrentStep({
@@ -1072,23 +1075,7 @@ async function buildImplementDispatchStep(ctx) {
1072
1075
  runId,
1073
1076
  repoRoot: root,
1074
1077
  artifactsDir,
1075
- prompt: `
1076
- # Implement the Eligible Remediation Workload
1077
-
1078
- Read the generated workload at:
1079
-
1080
- \`${handoff.workload_path}\`
1081
-
1082
- It contains the complete, dependency-safe current frontier. Complete every work
1083
- item and write its exact prompt-bound result contract to its \`result_path\`.
1084
- The host owns execution choices, grouping, and concurrency; audit-tools performs
1085
- no launch, routing, or quota decision. Do not start a later dependency level.
1086
- ${diagnostics}
1087
-
1088
- After all completed changes are merged and their result files exist, run:
1089
-
1090
- \`${nextCommand}\`
1091
- `,
1078
+ prompt: `\n${promptLines.join("\n")}\n`,
1092
1079
  allowedCommands: [
1093
1080
  ...new Set(handoff.workload.work_items.flatMap((item) => item.required_tests)),
1094
1081
  nextCommand,
@@ -1490,7 +1477,7 @@ async function handleWaitingForReviewApproval(root, artifactsDir, request, refus
1490
1477
  artifactsDir,
1491
1478
  prompt: reviewApprovalPrompt(request, reviewResolutionPath(artifactsDir), refusal),
1492
1479
  allowedCommands: [loaderCommand("next-step")],
1493
- stopCondition: "Stop after presenting the findings for approval and collecting the user's approve/disapprove decision, unless the decision is already recorded and the prompt told you to continue.",
1480
+ stopCondition: "Stop after presenting the findings for approval and collecting the user's approve/decline decision, unless the decision is already recorded and the prompt told you to continue.",
1494
1481
  artifactPaths: {
1495
1482
  review_request: reviewRequestPath(artifactsDir),
1496
1483
  review_resolution: reviewResolutionPath(artifactsDir),
@@ -1498,29 +1485,25 @@ async function handleWaitingForReviewApproval(root, artifactsDir, request, refus
1498
1485
  });
1499
1486
  }
1500
1487
  /**
1501
- * Uniform id-join contract pre-screen for the review gate (both paths): a
1502
- * resolution naming an unknown finding id or tier is REFUSED whole — archived
1503
- * (never applied) and the gate re-halts with the refusal and the valid set in
1504
- * the re-prompt. The gate's default is approve, so the silent-drop alternative
1505
- * turns a typo'd decline into an approval. Returns the re-halt step, or null
1506
- * when the resolution's ids are clean. `applyReviewResolution` re-checks as the
1507
- * mechanical backstop.
1488
+ * Read the review resolution for both gate paths, through the one strict
1489
+ * parser ({@link parseReviewResolution}). A refused file (bad JSON, an unknown
1490
+ * field, a wrong type, an id or tier outside the request) is archived as
1491
+ * `.refused-<ts>` never applied and the gate re-halts with each problem
1492
+ * named. The gate's default is approve, so reading past a bad file would turn
1493
+ * the user's decline into an approval. A stale file from another run is
1494
+ * archived as `.stale-<ts>` and the gate re-halts with no refusal. Returns the
1495
+ * re-halt step, or the resolution to apply.
1508
1496
  */
1509
- async function refuseUnknownIdResolution(root, artifactsDir, request, resolution, resolutionPath, requestPath) {
1510
- const screen = screenResolutionIds(request, resolution);
1511
- if (screen.unknown_finding_ids.length === 0 && screen.unknown_tiers.length === 0) {
1512
- return null;
1513
- }
1514
- await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
1497
+ async function readReviewResolution(root, artifactsDir, request, resolutionPath, requestPath) {
1498
+ const parsed = parseReviewResolution(await readFile(resolutionPath, "utf8"), request);
1499
+ if (parsed.kind === "ok")
1500
+ return { resolution: parsed.resolution };
1501
+ const suffix = parsed.kind === "stale" ? "stale" : "refused";
1502
+ await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.${suffix}-${Date.now()}`));
1515
1503
  await writeJsonFile(requestPath, request);
1516
- const parts = [];
1517
- if (screen.unknown_finding_ids.length > 0) {
1518
- parts.push(`finding id(s) not in the request: ${screen.unknown_finding_ids.map((i) => `\`${i}\``).join(", ")}`);
1519
- }
1520
- if (screen.unknown_tiers.length > 0) {
1521
- parts.push(`unknown tier(s): ${screen.unknown_tiers.map((t) => `\`${t}\``).join(", ")}`);
1522
- }
1523
- return handleWaitingForReviewApproval(root, artifactsDir, request, parts.join("; "));
1504
+ return {
1505
+ halt: await handleWaitingForReviewApproval(root, artifactsDir, request, parsed.kind === "refused" ? parsed.reason : undefined),
1506
+ };
1524
1507
  }
1525
1508
  /**
1526
1509
  * Run the review-approval gate over the SURVIVOR finding set (already passed
@@ -1586,21 +1569,10 @@ sourceAuditRead, autonomous = false) {
1586
1569
  // Consume the resolution into a durable, reasoned decision record.
1587
1570
  // Regenerable: a stale-schema request is treated as absent and rebuilt below.
1588
1571
  const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(survivors, randomRunId("path-a-review"));
1589
- const resolution = await readOptionalJsonFile(resolutionPath);
1590
- if (!isResolutionForRequest(request, resolution)) {
1591
- // Stale cross-run resolution (plan_id mismatch): archive it and RE-HALT
1592
- // with the live request rather than applying another run's answer.
1593
- await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.stale-${Date.now()}`));
1594
- await writeJsonFile(requestPath, request);
1595
- return {
1596
- kind: "halt",
1597
- step: await handleWaitingForReviewApproval(root, artifactsDir, request),
1598
- };
1599
- }
1600
- const refusalStep = await refuseUnknownIdResolution(root, artifactsDir, request, resolution, resolutionPath, requestPath);
1601
- if (refusalStep)
1602
- return { kind: "halt", step: refusalStep };
1603
- const decision = applyReviewResolution(request, resolution);
1572
+ const read = await readReviewResolution(root, artifactsDir, request, resolutionPath, requestPath);
1573
+ if ("halt" in read)
1574
+ return { kind: "halt", step: read.halt };
1575
+ const decision = applyReviewResolution(request, read.resolution);
1604
1576
  await writeReviewDecisionRecord(decisionPath, {
1605
1577
  planId: request.plan_id,
1606
1578
  approvedIds: decision.approved_ids,
@@ -1831,9 +1803,10 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
1831
1803
  }
1832
1804
  const paths = intakePaths(artifactsDir);
1833
1805
  const sourcePaths = new Set();
1834
- if (existsSync(paths.brief)) {
1835
- sourcePaths.add(paths.brief);
1836
- }
1806
+ // The brief is the tool's render of the ready summary; write it before it
1807
+ // becomes a pipeline source, so the source is never absent or stale.
1808
+ await writeRemediationBrief(artifactsDir, intake.summary);
1809
+ sourcePaths.add(paths.brief);
1837
1810
  for (const source of manifestSources) {
1838
1811
  // Swap the raw audit-findings.json for the approved-only filtered file so a
1839
1812
  // declined finding can never re-enter the pipeline as a source input.
@@ -1950,59 +1923,128 @@ Stop after presenting this choice. Do not advance the run until the user decides
1950
1923
  },
1951
1924
  });
1952
1925
  }
1953
- const PLAN_CLARIFICATION_ACTIONS = [
1954
- "clarified",
1955
- "reject_finding",
1956
- "defer",
1957
- ];
1958
- function isPlanClarificationAction(value) {
1959
- return (typeof value === "string" &&
1960
- PLAN_CLARIFICATION_ACTIONS.includes(value));
1961
- }
1962
- /** Carry `scope_additions` through when it is an array; keep string entries. */
1963
- function normalizedScopeAdditions(entry) {
1964
- if (!Array.isArray(entry.scope_additions))
1965
- return {};
1966
- return {
1967
- scope_additions: entry.scope_additions.filter((p) => typeof p === "string"),
1968
- };
1969
- }
1970
- function normalizePlanClarificationResolutions(value) {
1971
- if (Array.isArray(value)) {
1972
- return value.filter(isRecord).flatMap((entry) => {
1973
- if (typeof entry.finding_id === "string" && isPlanClarificationAction(entry.action)) {
1974
- return [
1975
- {
1976
- finding_id: entry.finding_id,
1977
- action: entry.action,
1978
- rationale: typeof entry.rationale === "string" ? entry.rationale : undefined,
1979
- ...normalizedScopeAdditions(entry),
1980
- },
1981
- ];
1982
- }
1983
- return [];
1926
+ // Action tokens are deliberately unambiguous so a host CANNOT lose an
1927
+ // approved finding by a natural word choice at the ambiguity gate: "this
1928
+ // candidate ambiguity isn't genuine" reads as a comment on the AMBIGUITY, so it
1929
+ // must map to `clarified` (proceed with the finding), never to a drop. The
1930
+ // finding-dropping token is named `reject_finding` — it speaks about the
1931
+ // FINDING, not the ambiguity, and so can't be confused with "no ambiguity here."
1932
+ const PLAN_CLARIFICATION_ACTIONS = ["clarified", "reject_finding", "defer"];
1933
+ /**
1934
+ * One entry of a clarification resolution file. Strict: an unknown field is
1935
+ * refused, not ignored.
1936
+ *
1937
+ * - `rationale` is REQUIRED and non-empty on `clarified`: it becomes the item's
1938
+ * `clarification_context`, the answer the next worker reads. A `clarified`
1939
+ * entry without it re-opened the item with no answer attached.
1940
+ * - `scope_additions` lists the files the answer ADDS to the owning block's
1941
+ * write scope (open-bugs.md:110): every file the fix must create or edit
1942
+ * beyond the promoted scope — the test a node must write, the source a
1943
+ * generated artifact mirrors, a new shared module, a manifest. It is allowed
1944
+ * ONLY on `clarified`, the one action that re-opens the item; on
1945
+ * `reject_finding` or `defer` it used to be ignored without a word. Each path
1946
+ * is validated whole-file fail-closed BEFORE anything is applied
1947
+ * (`validateClarificationScopeAdditions`), so the host never edits the plan
1948
+ * by hand.
1949
+ */
1950
+ const PlanClarificationResolutionSchema = z
1951
+ .object({
1952
+ finding_id: z.string().min(1, "must be a non-empty finding id"),
1953
+ action: z.enum(PLAN_CLARIFICATION_ACTIONS),
1954
+ rationale: z.string().optional(),
1955
+ scope_additions: z.array(z.string()).optional(),
1956
+ })
1957
+ .strict()
1958
+ .superRefine((entry, ctx) => {
1959
+ if (entry.action === "clarified" && (entry.rationale ?? "").trim().length === 0) {
1960
+ ctx.addIssue({
1961
+ code: z.ZodIssueCode.custom,
1962
+ path: ["rationale"],
1963
+ message: 'is required and must be non-empty when action is "clarified" — it carries the answer to the next worker',
1984
1964
  });
1985
1965
  }
1986
- if (!isRecord(value))
1987
- return [];
1988
- if (Array.isArray(value.resolutions)) {
1989
- return normalizePlanClarificationResolutions(value.resolutions);
1966
+ if (entry.action !== "clarified" && entry.scope_additions !== undefined) {
1967
+ ctx.addIssue({
1968
+ code: z.ZodIssueCode.custom,
1969
+ path: ["scope_additions"],
1970
+ message: `is allowed only with action "clarified" — a ${entry.action} entry widens no write scope`,
1971
+ });
1990
1972
  }
1991
- if (Array.isArray(value.items)) {
1992
- return normalizePlanClarificationResolutions(value.items);
1973
+ });
1974
+ /**
1975
+ * Read and validate a clarification resolution file — the mid-run
1976
+ * `clarification_resolution.json` and the up-front `ambiguity_resolution.json`
1977
+ * share this one parser.
1978
+ *
1979
+ * ONE shape is accepted: a bare JSON array of entries. Every entry is checked
1980
+ * against {@link PlanClarificationResolutionSchema}, and a second entry for the
1981
+ * same finding is refused. Any bad entry refuses the WHOLE file, and the reason
1982
+ * names the entry's index and field. The parser this replaced accepted four
1983
+ * shapes and dropped a bad entry in silence: an `"action": "approve"` lost the
1984
+ * user's answer, and the item then waited forever for a question the round no
1985
+ * longer showed.
1986
+ */
1987
+ async function readPlanClarificationResolutions(path) {
1988
+ let value;
1989
+ try {
1990
+ value = JSON.parse(await readFile(path, "utf8"));
1991
+ }
1992
+ catch (error) {
1993
+ return {
1994
+ ok: false,
1995
+ reason: `the file is not valid JSON (${error instanceof Error ? error.message : String(error)})`,
1996
+ };
1997
+ }
1998
+ if (!Array.isArray(value)) {
1999
+ return {
2000
+ ok: false,
2001
+ reason: "the file must be a JSON array of entries — an object wrapper such as " +
2002
+ '`{"resolutions": [...]}` is not accepted',
2003
+ };
1993
2004
  }
1994
- return Object.entries(value).flatMap(([findingId, entry]) => {
1995
- if (!isRecord(entry))
1996
- return [];
1997
- if (!isPlanClarificationAction(entry.action))
1998
- return [];
1999
- return [{
2000
- finding_id: typeof entry.finding_id === "string" ? entry.finding_id : findingId,
2001
- action: entry.action,
2002
- rationale: typeof entry.rationale === "string" ? entry.rationale : undefined,
2003
- ...normalizedScopeAdditions(entry),
2004
- }];
2005
+ const problems = [];
2006
+ const firstEntryFor = new Map();
2007
+ const resolutions = [];
2008
+ value.forEach((entry, index) => {
2009
+ const parsed = PlanClarificationResolutionSchema.safeParse(entry);
2010
+ if (!parsed.success) {
2011
+ for (const issue of parsed.error.issues) {
2012
+ const field = issue.path.length > 0 ? ` \`${issue.path.join(".")}\`` : "";
2013
+ problems.push(`entry [${index}]${field}: ${issue.message}`);
2014
+ }
2015
+ return;
2016
+ }
2017
+ const prior = firstEntryFor.get(parsed.data.finding_id);
2018
+ if (prior !== undefined) {
2019
+ problems.push(`entry [${index}] \`finding_id\`: \`${parsed.data.finding_id}\` is already answered by entry [${prior}]`);
2020
+ return;
2021
+ }
2022
+ firstEntryFor.set(parsed.data.finding_id, index);
2023
+ resolutions.push(parsed.data);
2005
2024
  });
2025
+ return problems.length > 0
2026
+ ? { ok: false, reason: problems.join("; ") }
2027
+ : { ok: true, resolutions };
2028
+ }
2029
+ /**
2030
+ * Every whole-file refusal of a parsed resolution, in check order: the parse,
2031
+ * then the closed id set, then the scope additions. Null means the file may be
2032
+ * applied.
2033
+ */
2034
+ async function planClarificationRefusal(root, state, parsed, validIds, outsideSetLabel) {
2035
+ if (!parsed.ok)
2036
+ return parsed.reason;
2037
+ const unknownIds = parsed.resolutions
2038
+ .map((r) => r.finding_id)
2039
+ .filter((id) => !validIds.has(id));
2040
+ if (unknownIds.length > 0) {
2041
+ return `finding id(s) ${outsideSetLabel}: ${unknownIds.map((i) => `\`${i}\``).join(", ")}`;
2042
+ }
2043
+ const scopeRefusals = await validateClarificationScopeAdditions(root, state, parsed.resolutions);
2044
+ if (scopeRefusals.length > 0) {
2045
+ return `invalid scope_additions — fix and re-submit: ${scopeRefusals.join(" | ")}`;
2046
+ }
2047
+ return null;
2006
2048
  }
2007
2049
  /**
2008
2050
  * Validate every `clarified` resolution's `scope_additions` BEFORE anything is
@@ -2059,6 +2101,8 @@ function applyClarificationScopeAdditions(root, state, res) {
2059
2101
  * Never resurrects a terminal item.
2060
2102
  */
2061
2103
  function applyClarificationActionToItem(item, res, now) {
2104
+ // The question is answered (or the finding is closed): it leaves the item.
2105
+ delete item.clarification_question;
2062
2106
  if (res.action === "reject_finding") {
2063
2107
  item.status = "deemed_inappropriate";
2064
2108
  item.failure_reason = res.rationale;
@@ -2087,32 +2131,22 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
2087
2131
  if (!state.plan || !state.items)
2088
2132
  return { kind: "applied", state };
2089
2133
  const resolutionPath = join(artifactsDir, "clarification_resolution.json");
2090
- const resolutions = normalizePlanClarificationResolutions(await readOptionalJsonFile(resolutionPath));
2091
- // Uniform id-join contract: an unknown finding_id refuses the WHOLE
2092
- // resolution (archived, nothing applied) and re-halts with the unknown ids
2093
- // named the silent-continue alternative drops the user's answer on a typo'd
2094
- // id and force-closes its item as abandoned at the fall-through below.
2095
- const unknownIds = resolutions
2096
- .map((r) => r.finding_id)
2097
- .filter((id) => !state.items?.[id]);
2098
- if (unknownIds.length > 0) {
2099
- await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
2100
- return {
2101
- kind: "refused",
2102
- step: await handleWaitingForClarification(root, artifactsDir, state, `finding id(s) not in the plan: ${unknownIds.map((i) => `\`${i}\``).join(", ")}`),
2103
- };
2104
- }
2105
- // Scope deltas ride the same whole-file fail-closed contract as unknown ids:
2106
- // an invalid addition refuses everything, so a decision record never
2134
+ const parsed = await readPlanClarificationResolutions(resolutionPath);
2135
+ // Uniform whole-file fail-closed contract: a malformed entry, an id outside
2136
+ // the paused set, or an invalid scope addition refuses the WHOLE resolution
2137
+ // (archived, nothing applied) and re-halts with the reason named. The
2138
+ // silent-continue alternative drops the user's answer and leaves its item
2139
+ // waiting on a question nobody is asked, so a decision record never
2107
2140
  // half-applies (open-bugs.md:110).
2108
- const scopeRefusals = await validateClarificationScopeAdditions(root, state, resolutions);
2109
- if (scopeRefusals.length > 0) {
2141
+ const refusal = await planClarificationRefusal(root, state, parsed, new Set(pausedClarifications(state).map((q) => q.finding_id)), "not waiting for a clarification");
2142
+ if (refusal !== null || !parsed.ok) {
2110
2143
  await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
2111
2144
  return {
2112
2145
  kind: "refused",
2113
- step: await handleWaitingForClarification(root, artifactsDir, state, `invalid scope_additions fix and re-submit: ${scopeRefusals.join(" | ")}`),
2146
+ step: await handleWaitingForClarification(root, artifactsDir, state, refusal ?? "the resolution could not be read"),
2114
2147
  };
2115
2148
  }
2149
+ const resolutions = parsed.resolutions;
2116
2150
  const now = new Date().toISOString();
2117
2151
  let appliedCount = 0;
2118
2152
  for (const res of resolutions) {
@@ -2153,14 +2187,30 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
2153
2187
  : hasUnansweredClarification(state)
2154
2188
  ? "waiting_for_clarification"
2155
2189
  : "implementing";
2156
- state.clarifications = [];
2157
2190
  await store.saveState(state);
2158
2191
  return { kind: "applied", state };
2159
2192
  }
2193
+ /**
2194
+ * The open worker questions: one per item paused as `needs_clarification`,
2195
+ * read from the item itself (its `clarification_question`), in content-stable
2196
+ * finding-id order. The item is the question's one home, so this list cannot
2197
+ * disagree with the item statuses the round waits on.
2198
+ */
2199
+ function pausedClarifications(state) {
2200
+ return Object.values(state.items ?? {})
2201
+ .filter((item) => item.status === "needs_clarification")
2202
+ .sort((left, right) => compareCodeUnits(left.finding_id, right.finding_id))
2203
+ .map((item) => ({
2204
+ finding_id: item.finding_id,
2205
+ // The state store refuses a paused item without a question, so the
2206
+ // fallback is unreachable from a stored state; it keeps the render total.
2207
+ ...(item.clarification_question ?? {
2208
+ category: "scope_of_fix",
2209
+ description: item.failure_reason ?? "(no question recorded)",
2210
+ }),
2211
+ }));
2212
+ }
2160
2213
  async function handleWaitingForClarification(root, artifactsDir, state, refusal) {
2161
- const clarifications = state.clarifications ??
2162
- (await readOptionalJsonFile(join(artifactsDir, "clarification_request.json"))) ??
2163
- [];
2164
2214
  const resolutionPath = join(artifactsDir, "clarification_resolution.json");
2165
2215
  return writeCurrentStep({
2166
2216
  stepKind: "collect_clarifications",
@@ -2168,11 +2218,10 @@ async function handleWaitingForClarification(root, artifactsDir, state, refusal)
2168
2218
  runId: stateRunId(state),
2169
2219
  repoRoot: root,
2170
2220
  artifactsDir,
2171
- prompt: clarificationPrompt(clarifications, resolutionPath, refusal),
2221
+ prompt: clarificationPrompt(pausedClarifications(state), resolutionPath, refusal),
2172
2222
  allowedCommands: [loaderCommand("next-step")],
2173
2223
  stopCondition: "Stop after asking the user for clarification answers, unless the answers are already available and the prompt told you to continue.",
2174
2224
  artifactPaths: {
2175
- clarification_request: join(artifactsDir, "clarification_request.json"),
2176
2225
  clarification_resolution: resolutionPath,
2177
2226
  },
2178
2227
  });
@@ -2230,17 +2279,10 @@ async function runPlanningReviewGate(root, artifactsDir, state, store) {
2230
2279
  // Resolution present: consume it into a durable, reasoned decision record.
2231
2280
  // Regenerable: a stale-schema request is treated as absent and rebuilt below.
2232
2281
  const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(findings, reviewPlanId);
2233
- const resolution = await readOptionalJsonFile(resolutionPath);
2234
- if (!isResolutionForRequest(request, resolution)) {
2235
- // Stale cross-run resolution: archive it and re-halt with the live request.
2236
- await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.stale-${Date.now()}`));
2237
- await writeJsonFile(requestPath, request);
2238
- return handleWaitingForReviewApproval(root, artifactsDir, request);
2239
- }
2240
- const refusalStep = await refuseUnknownIdResolution(root, artifactsDir, request, resolution, resolutionPath, requestPath);
2241
- if (refusalStep)
2242
- return refusalStep;
2243
- const decision = applyReviewResolution(request, resolution);
2282
+ const read = await readReviewResolution(root, artifactsDir, request, resolutionPath, requestPath);
2283
+ if ("halt" in read)
2284
+ return read.halt;
2285
+ const decision = applyReviewResolution(request, read.resolution);
2244
2286
  await writeReviewDecisionRecord(decisionPath, {
2245
2287
  planId: request.plan_id,
2246
2288
  approvedIds: decision.approved_ids,
@@ -2287,8 +2329,8 @@ function detectPlanAmbiguities(findings, items) {
2287
2329
  out.push({
2288
2330
  finding_id: f.id,
2289
2331
  category: "scope_of_fix",
2290
- description: `"${f.title}" is a ${lens} finding with ${fileCount === 0 ? "no cited files" : `${fileCount} affected files`}; ` +
2291
- "confirm how far the fix should reach (minimal local change vs. broader restructuring).",
2332
+ description: `"${f.title}" is a finding in the ${lens} lens with ${fileCount === 0 ? "no cited files" : `${fileCount} affected files`}. ` +
2333
+ "Confirm how far the fix should reach: a minimal local change, or a broader restructure.",
2292
2334
  });
2293
2335
  continue;
2294
2336
  }
@@ -2296,7 +2338,7 @@ function detectPlanAmbiguities(findings, items) {
2296
2338
  out.push({
2297
2339
  finding_id: f.id,
2298
2340
  category: "issue_appropriateness",
2299
- description: `"${f.title}" is a low-confidence finding; confirm it is a real issue worth fixing in this run.`,
2341
+ description: `"${f.title}" is a low-confidence finding. Confirm that it is a real issue to fix in this run.`,
2300
2342
  });
2301
2343
  }
2302
2344
  }
@@ -2308,8 +2350,8 @@ function detectPlanAmbiguities(findings, items) {
2308
2350
  * ambiguity is asked as a single batched question up front rather than falling
2309
2351
  * silently to triage mid-run. Deterministic heuristics seed CANDIDATES; the host
2310
2352
  * reviews them with repo access, dismisses/adds, and batches one user round. Each
2311
- * item is resolved as `clarified` (answered → re-opened), `deemed_inappropriate`
2312
- * (not a real issue), or `defer` (the user's explicit choice to skip this run).
2353
+ * item is resolved as `clarified` (answered → re-opened), `reject_finding`
2354
+ * (not a real issue → `deemed_inappropriate`), or `defer` (the user's explicit choice to skip this run).
2313
2355
  *
2314
2356
  * Idempotent: once `ambiguity_decision.json` exists the gate is done and never
2315
2357
  * re-halts. An empty resolution proceeds (the host found nothing to ask).
@@ -2348,16 +2390,16 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
2348
2390
  }
2349
2391
  // Resolution present: apply it to items, mark the gate done, archive inputs so
2350
2392
  // it cannot re-halt.
2351
- const resolutions = normalizePlanClarificationResolutions(await readOptionalJsonFile(resolutionPath));
2352
- // Uniform id-join contract: a resolution naming a finding id outside the plan
2353
- // is REFUSED whole (archived, nothing applied) and the gate re-halts with the
2354
- // unknown ids named — the silent-continue alternative drops a host answer on a
2355
- // typo'd id, leaving its item to fall to mid-run triage unexplained.
2356
- const validIds = new Set(findings.map((f) => f.id));
2357
- const unknownIds = resolutions
2358
- .map((r) => r.finding_id)
2359
- .filter((id) => !validIds.has(id));
2360
- if (unknownIds.length > 0) {
2393
+ // The same whole-file fail-closed contract as the mid-run round: a malformed
2394
+ // entry, a finding id outside the plan, or an invalid scope addition refuses
2395
+ // the WHOLE resolution (archived, nothing applied) and the gate re-halts with
2396
+ // the reason named — the silent-continue alternative drops a host answer,
2397
+ // leaving its item to fall to mid-run triage unexplained. At this up-front
2398
+ // gate no workload binding exists yet, so an applied scope widening simply
2399
+ // flows into the first dispatch.
2400
+ const parsed = await readPlanClarificationResolutions(resolutionPath);
2401
+ const refusal = await planClarificationRefusal(root, state, parsed, new Set(findings.map((f) => f.id)), "not in the plan");
2402
+ if (refusal !== null || !parsed.ok) {
2361
2403
  await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
2362
2404
  const candidates = (await readOptionalJsonFile(requestPath)) ??
2363
2405
  detectPlanAmbiguities(findings, state.items);
@@ -2367,30 +2409,7 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
2367
2409
  runId: stateRunId(state),
2368
2410
  repoRoot: root,
2369
2411
  artifactsDir,
2370
- prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), `finding id(s) not in the plan: ${unknownIds.map((i) => `\`${i}\``).join(", ")}`),
2371
- allowedCommands: [loaderCommand("next-step")],
2372
- stopCondition: "Stop after re-submitting a corrected ambiguity resolution, unless it is already written and the prompt told you to continue.",
2373
- artifactPaths: {
2374
- ambiguity_request: requestPath,
2375
- ambiguity_resolution: resolutionPath,
2376
- },
2377
- });
2378
- }
2379
- // Scope deltas: the same whole-file fail-closed contract as unknown ids
2380
- // (open-bugs.md:110). At this up-front gate no workload binding exists yet,
2381
- // so an applied widening simply flows into the first dispatch.
2382
- const scopeRefusals = await validateClarificationScopeAdditions(root, state, resolutions);
2383
- if (scopeRefusals.length > 0) {
2384
- await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
2385
- const candidates = (await readOptionalJsonFile(requestPath)) ??
2386
- detectPlanAmbiguities(findings, state.items);
2387
- return writeCurrentStep({
2388
- stepKind: "collect_clarifications",
2389
- status: "blocked",
2390
- runId: stateRunId(state),
2391
- repoRoot: root,
2392
- artifactsDir,
2393
- prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), `invalid scope_additions — fix and re-submit: ${scopeRefusals.join(" | ")}`),
2412
+ prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), refusal ?? "the resolution could not be read"),
2394
2413
  allowedCommands: [loaderCommand("next-step")],
2395
2414
  stopCondition: "Stop after re-submitting a corrected ambiguity resolution, unless it is already written and the prompt told you to continue.",
2396
2415
  artifactPaths: {
@@ -2399,6 +2418,7 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
2399
2418
  },
2400
2419
  });
2401
2420
  }
2421
+ const resolutions = parsed.resolutions;
2402
2422
  const now = new Date().toISOString();
2403
2423
  let changed = false;
2404
2424
  for (const res of resolutions) {
@@ -2641,16 +2661,18 @@ async function emitFinalGateRedStep(ctx) {
2641
2661
  // edit to a non-ignored file outside the run's own artifacts. This is the
2642
2662
  // same carve-out `worktreeContentId` applies, said in the operator's terms.
2643
2663
  const bindingBlock = ctx.tree === undefined
2644
- ? "Binding: this gate re-runs the floor on every arrival here, so this red\n" +
2645
- "describes the tree as it stands right now."
2664
+ ? "The tool keeps no copy of this result. Each next-step runs the full build\n" +
2665
+ "and suite again, which takes minutes. So fix something before you run\n" +
2666
+ "next-step again."
2646
2667
  : ctx.tree === null
2647
- ? "Binding: no tree content id could be taken, so this red is NOT cached —\n" +
2648
- "the floor will run again on your next next-step whatever you change."
2649
- : `Binding: this red is bound to tree \`${ctx.tree}\`. Any edit to a file\n` +
2650
- `outside \`${AUDIT_TOOLS_DIRNAME}/\` and outside anything git ignores moves that\n` +
2651
- "id, which invalidates the cached verdict and makes the next next-step run\n" +
2652
- "the WHOLE floor again against what you changed. Editing nothing leaves the\n" +
2653
- "id identical, so the cached red is served back without spawning anything.";
2668
+ ? "The tool could not take a tree id, so it keeps no copy of this result.\n" +
2669
+ "Each next-step runs the full build and suite again, which takes minutes.\n" +
2670
+ "So fix something before you run next-step again."
2671
+ : `The tool keeps this result for tree \`${ctx.tree}\`. An edit to any file\n` +
2672
+ `outside \`${AUDIT_TOOLS_DIRNAME}/\` and outside git-ignored paths changes that tree.\n` +
2673
+ "The next next-step then runs the full build and suite again, which takes\n" +
2674
+ "minutes. With no edit, next-step shows this same red at once and runs\n" +
2675
+ "nothing. So fix something before you run next-step again.";
2654
2676
  const nextCommand = loaderCommand("next-step");
2655
2677
  return {
2656
2678
  kind: "emit",
@@ -2663,39 +2685,27 @@ async function emitFinalGateRedStep(ctx) {
2663
2685
  prompt: `
2664
2686
  # Remediation paused — the repository suite is red
2665
2687
 
2666
- The tool-owned gate (${scope}) ran the repository's own build/typecheck/test
2667
- floor and it FAILED. Nothing about this run has been changed: every item keeps
2668
- the status it had, the run stays in the same phase, and no work was discarded.
2669
-
2670
- Failing command:
2688
+ ${ctx.where}, the tool ran the repository's build, typecheck and tests.
2689
+ One command failed:
2671
2690
 
2672
2691
  \`${failingCommand}\`
2673
2692
 
2674
- The captured output tail is recorded at:
2693
+ The output tail is in \`${recordPath}\`.
2675
2694
 
2676
- \`${recordPath}\`
2677
-
2678
- A red here is whole-repo and says nothing about which remediation item caused
2679
- it — it may not be this run's doing at all (a commit landed alongside the run is
2680
- enough). So this is a PAUSE, not a verdict on the work.
2695
+ This pause changes nothing. Every item keeps its status, the run stays in its
2696
+ phase, and no work is lost. A red suite does not name the item that caused it.
2697
+ A commit made outside this run can also cause it.
2681
2698
 
2682
2699
  ${attributionBlock}
2683
2700
 
2684
- ${bindingBlock}
2685
-
2686
- Fix the failing command — or confirm it was already broken independently of this
2687
- run — then run:
2701
+ Do these steps:
2688
2702
 
2689
- \`${nextCommand}\`
2703
+ 1. Fix the failing command, or confirm that it was already broken before this run.
2704
+ 2. Run \`${nextCommand}\`.
2690
2705
 
2691
- The gate re-runs from scratch. The moment it is green the run continues exactly
2692
- where it left off.
2706
+ When the suite is green, the run continues from where it stopped.
2693
2707
 
2694
- Re-run it DELIBERATELY, not on a timer. The gate's verdict is cached against the
2695
- content of the tree, so a re-run with nothing changed serves the recorded verdict
2696
- without spawning anything — and the moment you fix something the tree moves, the
2697
- cache misses, and the whole floor (a full build plus the suite, minutes, holding
2698
- the run's phase lock) runs against your change. Fix something first, then re-run.
2708
+ ${bindingBlock}
2699
2709
  `,
2700
2710
  allowedCommands: [nextCommand],
2701
2711
  stopCondition: "Stop. Make the repository suite green, then re-run next-step to resume the run.",
@@ -2730,6 +2740,7 @@ async function runPhaseBoundaryGate(ctx) {
2730
2740
  if (phase == null)
2731
2741
  return null;
2732
2742
  const scope = `phase ${phase} boundary`;
2743
+ const where = `At the phase ${phase} boundary`;
2733
2744
  const disabledReason = finalGateDisabledReason(options);
2734
2745
  if (disabledReason !== null) {
2735
2746
  await recordFinalGateOutcome({
@@ -2791,6 +2802,7 @@ async function runPhaseBoundaryGate(ctx) {
2791
2802
  artifactsDir,
2792
2803
  state,
2793
2804
  scope,
2805
+ where,
2794
2806
  gate: {
2795
2807
  passed: false,
2796
2808
  results: cached.results,
@@ -2844,6 +2856,7 @@ async function runPhaseBoundaryGate(ctx) {
2844
2856
  artifactsDir,
2845
2857
  state,
2846
2858
  scope,
2859
+ where,
2847
2860
  gate,
2848
2861
  // The id taken BEFORE the floor ran, which is what writeFinalGateVerdict
2849
2862
  // just recorded — so the prompt names the binding the cache will match on.
@@ -2901,6 +2914,7 @@ async function handleAllTerminalTransition(root, artifactsDir, state, store, opt
2901
2914
  artifactsDir,
2902
2915
  state,
2903
2916
  scope,
2917
+ where: "Before the close phase",
2904
2918
  gate,
2905
2919
  runLogger,
2906
2920
  });
@@ -3201,7 +3215,7 @@ async function buildConfirmIntentStep(ctx) {
3201
3215
  const runId = stateRunId(state);
3202
3216
  const nextCommand = loaderCommand("next-step");
3203
3217
  const checkpointPath = join(artifactsDir, "intent_checkpoint.json");
3204
- // Read the pre-drafted checkpoint if one exists (confirmed_by: "draft").
3218
+ // Read a checkpoint the host already wrote, if one exists.
3205
3219
  //
3206
3220
  // This is the GATE, and it reads LENIENTLY on purpose: a checkpoint whose
3207
3221
  // `closing_action` is outside the vocabulary must reach the refusal below,
@@ -3209,14 +3223,16 @@ async function buildConfirmIntentStep(ctx) {
3209
3223
  // would replace that affordance with a zod error. Every field consumed here as
3210
3224
  // a VALUE is still checked by name (`isClosingAction`, `customCommandOf`).
3211
3225
  //
3212
- // The REFUSED values come from `rejected`, never from `draft`: the returned
3226
+ // The REFUSED values come from `rejected`, never from `existing`: the returned
3213
3227
  // checkpoint is schema-valid, so an out-of-vocabulary key is ABSENT from it
3214
3228
  // (see `parseIntentCheckpointLenient`). The value the refusal quotes is the one
3215
3229
  // the host WROTE, which is exactly what `rejected` carries.
3216
- const draftRead = await readIntentCheckpointLenient(checkpointPath);
3217
- const draft = draftRead.checkpoint;
3218
- const isDraft = draft?.confirmed_by === "draft";
3219
- const rejectedClosingAction = draftRead.rejected.find((field) => field.key === "closing_action");
3230
+ const existingRead = await readIntentCheckpointLenient(checkpointPath);
3231
+ const existing = existingRead.checkpoint;
3232
+ const rejectedClosingAction = existingRead.rejected.find((field) => field.key === "closing_action");
3233
+ // The proposal comes from the intake summary — the one file the host wrote at
3234
+ // synthesis. There is no draft checkpoint: the facts live in one place.
3235
+ const summary = (await readIntakeArtifacts(artifactsDir)).summary;
3220
3236
  // Closing action: DETECTED candidates, presented for the host to choose
3221
3237
  // from; the tool never selects one (owner decision 92b0e2dd7cfdc06d).
3222
3238
  const facts = await detectProjectFacts(root);
@@ -3226,46 +3242,42 @@ async function buildConfirmIntentStep(ctx) {
3226
3242
  const closingSection = renderClosingActionSection(facts);
3227
3243
  // A confirmed checkpoint whose closing_action is not in the vocabulary
3228
3244
  // re-enters this step by name — a refusal, never a silent default.
3229
- const rawChoice = draft?.confirmed_by === "host"
3230
- ? (draft.closing_action ?? rejectedClosingAction?.value)
3245
+ const rawChoice = existing?.confirmed_by === "host"
3246
+ ? (existing.closing_action ?? rejectedClosingAction?.value)
3231
3247
  : undefined;
3232
3248
  const refusal = rawChoice !== undefined && !isClosingAction(rawChoice)
3233
3249
  ? `> **Refused:** \`closing_action\` ${JSON.stringify(rawChoice)} is not one of ${CLOSING_ACTIONS.map((a) => `\`${a}\``).join(", ")}. Rewrite the checkpoint with a valid value, or omit the field for \`none\`.\n`
3234
- : rawChoice === "custom" && customCommandOf(draft) === null
3250
+ : rawChoice === "custom" && customCommandOf(existing) === null
3235
3251
  ? "> **Refused:** `closing_action` \"custom\" needs `closing_custom_command`, a non-empty argv array such as [\"npm\", \"run\", \"release\"]. Add it, or choose another action.\n"
3236
3252
  : "";
3237
3253
  let prompt;
3238
- if (isDraft && draft) {
3239
- // Build a consolidated single-stop proposal from the draft.
3240
- const draftRaw = draft;
3241
- const preDraftQuestions = Array.isArray(draftRaw.pre_draft_questions)
3242
- ? draftRaw.pre_draft_questions
3243
- : [];
3254
+ if (summary) {
3255
+ // Build a consolidated single-stop proposal from the intake summary.
3256
+ const questions = summary.open_questions ?? [];
3244
3257
  // INV-remediate-state-06: only explicit blocking===true is blocking.
3245
- const blockingQs = preDraftQuestions.filter((q) => q.blocking === true);
3246
- const nonBlockingQs = preDraftQuestions.filter((q) => q.blocking !== true);
3247
- const intentInterpretation = typeof draftRaw.intent_interpretation === "string" ? draftRaw.intent_interpretation : undefined;
3258
+ const blockingQs = questions.filter((q) => q.blocking === true);
3259
+ const nonBlockingQs = questions.filter((q) => q.blocking !== true);
3248
3260
  const questionLines = [
3249
3261
  ...blockingQs.map((q) => `- **[blocking] ${q.id}**: ${q.question}`),
3250
3262
  ...nonBlockingQs.map((q) => `- **[FYI] ${q.id}**: ${q.question}`),
3251
3263
  ].join("\n") || "- None";
3252
- const filtersBlock = draft.filters && Object.keys(draft.filters).length > 0
3253
- ? `\`\`\`json\n${JSON.stringify(draft.filters, null, 2)}\n\`\`\``
3264
+ const filtersBlock = Object.keys(summary.filters).length > 0
3265
+ ? `\`\`\`json\n${JSON.stringify(summary.filters, null, 2)}\n\`\`\``
3254
3266
  : "(none — remediating all findings)";
3255
3267
  prompt = `
3256
3268
  ${refusal}# Confirm Remediation Scope and Intent
3257
3269
 
3258
- The intake worker has pre-populated the following proposal. Review each section
3270
+ The tool built the following proposal from the intake summary. Review each section
3259
3271
  and adjust where needed, then confirm by writing the final \`intent_checkpoint.json\`.
3260
3272
 
3261
3273
  ## Proposed Scope
3262
3274
 
3263
- ${draft.scope_summary ?? "(not set)"}
3275
+ ${summary.scope_summary}
3264
3276
 
3265
3277
  ## Proposed Intent
3266
3278
 
3267
- ${draft.intent_summary ?? "(not set)"}
3268
- ${intentInterpretation ? `\n**How free-form intent was interpreted:** ${intentInterpretation}\n` : ""}
3279
+ ${summary.intent_summary}
3280
+ ${summary.intent_interpretation ? `\n**How free-form intent was interpreted:** ${summary.intent_interpretation}\n` : ""}
3269
3281
  ## Proposed Filters
3270
3282
 
3271
3283
  ${filtersBlock}
@@ -3287,10 +3299,10 @@ To confirm, write the final checkpoint to:
3287
3299
  "schema_version": "intent-checkpoint/v1",
3288
3300
  "confirmed_at": "<ISO-8601 timestamp>",
3289
3301
  "confirmed_by": "host",
3290
- "scope_summary": "${draft.scope_summary ?? "<the files/areas in scope>"}",
3291
- "intent_summary": "${draft.intent_summary ?? "<the goal>"}",
3302
+ "scope_summary": ${JSON.stringify(summary.scope_summary)},
3303
+ "intent_summary": ${JSON.stringify(summary.intent_summary)},
3292
3304
  "free_form_intent": "<optional: additional guidance>",
3293
- "filters": ${JSON.stringify(draft.filters ?? {}, null, 2)},
3305
+ "filters": ${JSON.stringify(summary.filters, null, 2)},
3294
3306
  "excluded_scope": [{ "path": "<path or prefix>", "reason": "<why>" }],
3295
3307
  "must_not_touch": [],
3296
3308
  "closing_action": "<one of the candidates above, or omit the field for none>",
@@ -3308,7 +3320,7 @@ Once written with \`"confirmed_by": "host"\`, run:
3308
3320
  `;
3309
3321
  }
3310
3322
  else {
3311
- // Fallback for when there is no pre-drafted checkpoint.
3323
+ // Fallback for when no intake summary exists yet.
3312
3324
  prompt = `
3313
3325
  ${refusal}# Confirm Remediation Scope and Intent
3314
3326
 
@@ -3595,11 +3607,11 @@ export function buildPreIntakeObligations(ctx, snapshot) {
3595
3607
  },
3596
3608
  {
3597
3609
  // Intent gate: fire when no confirmed checkpoint exists (no checkpoint + any
3598
- // intake artifact or an active run, or a draft checkpoint). Never for
3610
+ // intake artifact or an active run). A legacy draft checkpoint was archived
3611
+ // at the snapshot read, so it counts as no checkpoint. Never for
3599
3612
  // complete/closing — those already confirmed their checkpoint.
3600
3613
  id: "confirm_intent",
3601
3614
  derive: (state) => {
3602
- const checkpointIsDraft = existingCheckpoint?.confirmed_by === "draft";
3603
3615
  // A host-confirmed checkpoint carrying a closing_action outside the
3604
3616
  // vocabulary is not a confirmation: the step re-emits naming the value
3605
3617
  // (owner decision 92b0e2dd7cfdc06d — never a silent default).
@@ -3622,8 +3634,7 @@ export function buildPreIntakeObligations(ctx, snapshot) {
3622
3634
  state.status !== "pending" &&
3623
3635
  state.status !== "complete" &&
3624
3636
  state.status !== "closing";
3625
- const fires = checkpointIsDraft ||
3626
- invalidClosingAction ||
3637
+ const fires = invalidClosingAction ||
3627
3638
  (!existsSync(checkpointPath) &&
3628
3639
  (existsSync(ip.summary) ||
3629
3640
  existsSync(ip.extractedPlan) ||
@@ -3827,6 +3838,14 @@ export function buildMainObligations(ctx) {
3827
3838
  return { kind: "emit", step: outcome.step };
3828
3839
  return { kind: "transition", state: outcome.state };
3829
3840
  }
3841
+ // A wait with no paused item has no question to ask: a round rendered
3842
+ // from it lists nothing, and no answer can satisfy it. Return to
3843
+ // `implementing`, whose own obligations route the run onward.
3844
+ if (!hasUnansweredClarification(s)) {
3845
+ s.status = "implementing";
3846
+ await store.saveState(s);
3847
+ return { kind: "transition", state: s };
3848
+ }
3830
3849
  return {
3831
3850
  kind: "emit",
3832
3851
  step: await handleWaitingForClarification(root, artifactsDir, s),
@@ -4085,6 +4104,14 @@ async function advanceUnderPhaseLock(deps) {
4085
4104
  // asking only the parsed value reads a refusal as "no answer given, so no
4086
4105
  // question". Strict is not the alternative: it would replace the gate's named
4087
4106
  // refusal with a thrown load error.
4107
+ //
4108
+ // A legacy DRAFT checkpoint (`confirmed_by: "draft"`, written by the retired
4109
+ // synthesis prompt) is not a confirmation. It is archived here, so that every
4110
+ // presence check on the checkpoint path below reads it as absent and the
4111
+ // confirm step asks again.
4112
+ if (isLegacyDraftCheckpoint(await readOptionalJsonFile(checkpointPath))) {
4113
+ await rename(checkpointPath, `${checkpointPath}.legacy-draft-${Date.now()}`);
4114
+ }
4088
4115
  const checkpointRead = existsSync(checkpointPath)
4089
4116
  ? await readIntentCheckpointLenient(checkpointPath)
4090
4117
  : undefined;
@@ -4093,7 +4120,7 @@ async function advanceUnderPhaseLock(deps) {
4093
4120
  // Whether a supplied `--input` matches the input the existing run was built
4094
4121
  // from — so re-passing the same `--input` (the loader does this each next-step)
4095
4122
  // resumes rather than tripping the input_conflict gate.
4096
- const suppliedInputUnchanged = suppliedInputMatchesRun(inputResolution, await readOptionalJsonFile(intakePaths(artifactsDir).sourceManifest));
4123
+ const suppliedInputUnchanged = suppliedInputMatchesRun(inputResolution, await readSourceManifest(intakePaths(artifactsDir).sourceManifest));
4097
4124
  // The linear pre-intake gates run as obligations through the shared advance
4098
4125
  // loop. An emit returns to the host; a transition re-scans within this call;
4099
4126
  // exhausting them (step === null) means the run is past intake and falls