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
@@ -4,8 +4,8 @@
4
4
  // or the id-glossary write scope is reverted.
5
5
  import { mkdir } from "node:fs/promises";
6
6
  import { isAbsolute } from "node:path";
7
- import { AUDIT_TOOLS_DIRNAME, headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, enrichMissingSubmissionIssues, isMissingObservation, compareCodeUnits, contentSha256, declaredInvariantIds, deriveLaneDemand, discoverLandingGates, hasExactKeys, identityFailureDiagnostic, hostHandoffResultPath, idsAreStrictlyAscending, isCommit, isGitRepo, isRecord, isSha256, LaneDemandSchema, normalizeRepoPath, parseAllWorkloadItems, parseCommandString, parseWorkloadEnvelope, promptSha256, readSubmissionDocument, readSubmissionLedger, readTrailingSubmissionRefusals, recoveryMarkMatches, recordHostResultOutcomes, repoRelativePath, resolveContainedPath, resolveHostHandoffPaths, runTrackedAsync, sameStrings, scanBoundSubmission, SEVERITIES, severityRank, stringArray, stableStringify, TRACKED_CHILD_DEADLINE_MS, withGlossaryScope, writeJsonFile, } from "audit-tools/shared";
8
- import { REMEDIATION_STATE_CONTRACT_VERSION, StateStore, } from "../../state/store.js";
7
+ import { AUDIT_TOOLS_DIRNAME, headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_ISSUE_REMEDY, WORKLOAD_ISSUE_CODES, WORKLOAD_ISSUE_REMEDY, bindWorkerPrompt, deriveResultId, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, enrichMissingSubmissionIssues, isMissingObservation, compareCodeUnits, contentSha256, declaredInvariantIds, deriveLaneDemand, discoverLandingGates, hasExactKeys, identityFailureDiagnostic, hostHandoffResultPath, idsAreStrictlyAscending, isCommit, isGitRepo, isRecord, isSha256, LaneDemandSchema, normalizeRepoPath, parseAllWorkloadItems, parseCommandString, parseWorkloadEnvelope, readSubmissionDocument, readSubmissionLedger, readTrailingSubmissionRefusals, recoveryMarkMatches, recordHostResultOutcomes, repoRelativePath, resolveContainedPath, resolveHostHandoffPaths, runTrackedAsync, sameStrings, scanBoundSubmission, SEVERITIES, severityRank, stringArray, stableStringify, TRACKED_CHILD_DEADLINE_MS, withGlossaryScope, writeJsonFile, } from "audit-tools/shared";
8
+ import { REMEDIATION_STATE_CONTRACT_VERSION, } from "../../state/store.js";
9
9
  import { REMEDIATION_HOST_HANDOFF_RECORD_V1ALPHA1, REMEDIATION_HOST_HANDOFF_RECORD_V1ALPHA2, REMEDIATION_HOST_SCOPE_SEMANTICS, RemediationHostHandoffRecordSchema, RemediationPlanSchema, isClarificationCategory, } from "../../state/types.js";
10
10
  import { ITEM_STATUSES, isTerminalStatus, isVerifiedCompleteStatus, } from "../../state/itemStatus.js";
11
11
  import { REMEDIATION_HOST_DECISION_CONTRACT_VERSION as DECISION_CONTRACT_VERSION, REMEDIATION_HOST_RESULT_CONTRACT_VERSION as RESULT_CONTRACT_VERSION, REMEDIATION_HOST_WORKLOAD_CONTRACT_VERSION as WORKLOAD_CONTRACT_VERSION, } from "../types.js";
@@ -85,10 +85,78 @@ export const REMEDIATION_ISSUE_CODES = [
85
85
  * describe work that is no longer pending, so the whole recovery aborts.
86
86
  */
87
87
  "state_moved_between_phases",
88
+ /**
89
+ * The work item's baseline commit is not in this repository, so nothing can
90
+ * be corroborated against it. The run's recorded state is at fault, not the
91
+ * result.
92
+ */
93
+ "baseline_missing",
94
+ /**
95
+ * The work item's baseline commit exists but is no longer an ancestor of HEAD
96
+ * (history was rewritten under the run). The spawn-free `recover-ingest` verb
97
+ * is the named repair; no worker can make it.
98
+ */
99
+ "baseline_orphaned",
100
+ /**
101
+ * `landed_commit` names a commit that cannot be this item's landed work: the
102
+ * baseline itself, or a commit whose diff against the baseline is empty.
103
+ */
104
+ "landed_commit_invalid",
105
+ /**
106
+ * A result names a work item that the persisted workload binds but that is no
107
+ * longer pending (it settled, or its dependencies are not complete).
108
+ */
109
+ "work_item_not_eligible",
110
+ // `workload_stale` is shared with the audit draw: its meaning and its remedy
111
+ // live beside `WORKLOAD_ISSUE_CODES`.
112
+ ...WORKLOAD_ISSUE_CODES,
88
113
  ];
114
+ /**
115
+ * What the host must DO about each remediation ingest code (owner review of
116
+ * prompt 20, 2026-09-18). The `Record` over the whole union is the enforcement:
117
+ * a code added above with no remedy here is a type error. Read `IssueRemedy`
118
+ * for what the four remedies mean.
119
+ */
120
+ const REMEDIATION_ISSUE_REMEDY = {
121
+ ...SUBMISSION_ISSUE_REMEDY,
122
+ ...WORKLOAD_ISSUE_REMEDY,
123
+ // Here a duplicate is two result files carrying one result_id in the same
124
+ // call, and NEITHER is accepted — so the item is still pending and the fix is
125
+ // a corrected file. (The audit draw raises it only after an acceptance, so
126
+ // there it stays settled.)
127
+ duplicate_submission_id: "repair",
128
+ // The run's own documents: the next prepare re-mints the workload and its
129
+ // binding in the same call, so nothing is asked of the host.
130
+ workload_missing: "none",
131
+ workload_invalid: "none",
132
+ trusted_binding_missing: "none",
133
+ // A fault in the landed work or in the result that describes it — a worker
134
+ // can make a new commit or correct the file.
135
+ commit_missing: "repair",
136
+ commit_not_landed: "repair",
137
+ baseline_not_ancestor: "repair",
138
+ changed_files_mismatch: "repair",
139
+ run_start_dirty_overlap: "repair",
140
+ required_test_failed: "repair",
141
+ required_test_timed_out: "repair",
142
+ landed_commit_invalid: "repair",
143
+ // The plan, the run's recorded state, or the tool's own limit is at fault.
144
+ required_test_output_overflow: "operator",
145
+ dependency_missing: "operator",
146
+ block_contract_invalid: "operator",
147
+ recovery_unrecorded: "operator",
148
+ tree_moved_between_phases: "operator",
149
+ state_moved_between_phases: "operator",
150
+ baseline_missing: "operator",
151
+ baseline_orphaned: "operator",
152
+ work_item_not_eligible: "operator",
153
+ };
154
+ /** The remedy for one classified remediation ingest failure. */
155
+ export function remediationIssueRemedy(issue) {
156
+ return REMEDIATION_ISSUE_REMEDY[issue.code];
157
+ }
89
158
  const CURRENT_STATE_KEYS = new Set([
90
159
  "applied_edit_surface",
91
- "clarifications",
92
160
  "closing_context",
93
161
  "closing_plan",
94
162
  "contract_version",
@@ -104,6 +172,7 @@ const CURRENT_STATE_KEYS = new Set([
104
172
  const CURRENT_ITEM_KEYS = new Set([
105
173
  "block_id",
106
174
  "clarification_context",
175
+ "clarification_question",
107
176
  "completed_at",
108
177
  "failure_context",
109
178
  "failure_reason",
@@ -185,7 +254,7 @@ function normalizeDeclaredPath(root, candidate, label) {
185
254
  function pathIsAllowedByWriteScope(root, candidate, allowedFiles) {
186
255
  let normalized;
187
256
  try {
188
- normalized = repoRelativePath(root, candidate, "changed_files[]");
257
+ normalized = repoRelativePath(root, candidate, "landed file");
189
258
  }
190
259
  catch {
191
260
  return false;
@@ -461,36 +530,13 @@ export async function remediationSubmissionBinding(params) {
461
530
  const workItem = workload.work_items.find((item) => isRecord(item) && item.id === params.workItemId);
462
531
  if (workItem === undefined)
463
532
  return null;
464
- // Legacy directory recovery is privileged by the tool-owned state binding,
465
- // not by prompt text or current filesystem shape. Exact-scope validation
466
- // remains available when state is absent; only canonical bound state can
467
- // widen the in-memory view.
468
- let validationWorkItem = workItem;
469
- try {
470
- const storedState = await new StateStore(paths.artifactsDir).loadState();
471
- // No version is supplied here. The store reads the state from disk and
472
- // stamps the contract version it just VALIDATED, so `parseCurrentState`
473
- // tests the value that was in the file. It used to be handed
474
- // `STATE_CONTRACT_VERSION` from this line, which made the parser's version
475
- // check compare a constant to itself and accept any state the store was
476
- // willing to return.
477
- const state = storedState ? parseCurrentState(storedState) : null;
478
- const canonicalWorkload = state
479
- ? parseWorkload(read.value, paths, params.runId, state)
480
- : null;
481
- const canonicalWorkItem = canonicalWorkload?.work_items.find((item) => item.id === params.workItemId);
482
- if (state?.host_handoff && canonicalWorkItem) {
483
- validationWorkItem = deriveLegacyDirectoryScopeRecovery(state, canonicalWorkItem).workItem;
484
- }
485
- }
486
- catch {
487
- // Invalid/missing state cannot authorize widening. The raw workload's
488
- // exact scope remains the fail-closed validator for this recovery write.
489
- }
533
+ // The shape gate reads only the item's identity (id and prompt digest). The
534
+ // write scope is checked at ingest against the landed commit, so no stored
535
+ // state can widen what this validator accepts.
490
536
  return {
491
537
  submissionDir: paths.resultDir,
492
538
  validate: (value) => {
493
- const parsed = parseResult(value, params.runId, validationWorkItem, paths.root);
539
+ const parsed = parseResult(value, params.runId, workItem);
494
540
  return parsed.ok
495
541
  ? null
496
542
  : { code: "submission_contract_invalid", check: parsed.check, message: parsed.reason };
@@ -641,28 +687,82 @@ function buildPrompt(item) {
641
687
  required_tests: item.requiredTests,
642
688
  result_path: item.resultPath,
643
689
  });
690
+ // The BODY only. The result template is appended by `bindWorkerPrompt`, and
691
+ // the prompt digest covers exactly this text (owner review of prompt 20,
692
+ // 2026-09-18: plain language, rules as bullets, no word a worker cannot act
693
+ // on).
644
694
  return [
645
- "Implement the bounded remediation work item below.",
646
- "The host owns execution choices. For every assignment, apply the finding and item instructions exactly, including any clarified scope or retry context.",
647
- "Keep every edit within allowed_files, run every required test, land one attributable commit whose changed-file set is exact, and write one JSON result at result_path.",
648
- 'An allowed_files entry ending in "/" authorizes normalized descendant files; every other entry authorizes only that exact file.',
695
+ `# Implement remediation work item \`${item.blockId}\``,
696
+ "",
697
+ "Assignment:",
698
+ "",
699
+ "```json",
700
+ assignment,
701
+ "```",
702
+ "",
703
+ "Rules:",
704
+ "",
705
+ '- Edit only the files in `allowed_files`. An entry that ends in "/" allows every file below that directory. Every other entry allows only that one file.',
706
+ "- Apply each finding in `assignments` exactly, with the clarified scope or the retry context it carries.",
649
707
  ...(item.moduleContracts.length > 0
650
708
  ? [
651
- "module_contracts carries the APPROVED contract for each module this item implements. The implementation MUST conform to every declared input, output, invariant, side effect, validation boundary, failure mode, and seam adjustment. A locally plausible interface that contradicts them is a defect even when the build and the targeted tests pass — a conformance divergence propagates to every consumer of the module.",
709
+ "- Conform to each contract in `module_contracts`: every declared input, output, invariant, side effect, validation boundary, failure mode and seam adjustment. Code that breaks a contract is a defect, even when the build and the tests pass.",
652
710
  ]
653
711
  : []),
654
- ...(item.hasLandingGates
712
+ ...(item.requiredTests.length > 0
655
713
  ? [
656
- "This repository declares tree-wide LANDING GATES. They are not per-item commands and nothing in this item runs them: the CLOSE runs each of them once on the fully merged tree and reports what it finds. Keep your edits within allowed_files and conform to module_contracts — a gate refusing later is a defect in the merged tree, not a command for this item to run.",
714
+ "- Run each command in `required_tests` until it passes. The tool runs each command again before it accepts your result.",
657
715
  ]
658
716
  : []),
659
- `Assignment: ${assignment}`,
660
- `The result must use ${RESULT_CONTRACT_VERSION} and contain exactly contract_version, result_id, run_id, work_item_id, prompt_sha256, changed_files, commit_evidence, test_evidence, obligation_evidence, worktree_evidence, acceptance, and merge.`,
661
- item.obligationIds.length > 0
662
- ? `obligation_evidence must contain exactly one entry per bound obligation id — ${item.obligationIds.join(", ")} — each an object {obligation_id, evidence} whose evidence array cites at least one non-empty string (file, symbol, or test) showing the landed implementation satisfies that obligation. Ingestion refuses the result when any bound obligation is uncovered.`
663
- : "obligation_evidence must be an empty array — this item binds no contract obligations.",
664
- "Bind commit_evidence.before and worktree_evidence.baseline_commit to baseline_commit; report only passed required tests; acceptance.status must be accepted and merge.status must be merged.",
665
- `If no edit should land, write ${DECISION_CONTRACT_VERSION} instead with exactly contract_version, result_id, run_id, work_item_id, prompt_sha256, outcome. outcome must be one of: {status: resolved_no_change, evidence: [non-empty strings]}, {status: blocked, failure_reason: non-empty string}, or {status: needs_clarification, question: non-empty string, optional category}.`,
717
+ "- Make one commit on top of `baseline_commit`, with all your edits in it. Merge it so that HEAD contains it.",
718
+ ...(item.hasLandingGates
719
+ ? ["- Do not run the landing gates. The close phase runs them once, on the merged tree."]
720
+ : []),
721
+ ].join("\n");
722
+ }
723
+ /**
724
+ * The result template every remediation worker prompt ends with. The tool
725
+ * fills the identity values from the body digest; the worker changes only the
726
+ * marked values. The decision form follows for an item where no edit should
727
+ * land.
728
+ */
729
+ function renderResultTemplate(item) {
730
+ const identity = {
731
+ result_id: deriveResultId(item.blockId, item.promptDigest),
732
+ run_id: item.runId,
733
+ work_item_id: item.blockId,
734
+ prompt_sha256: item.promptDigest,
735
+ };
736
+ const landed = {
737
+ contract_version: RESULT_CONTRACT_VERSION,
738
+ ...identity,
739
+ landed_commit: "<full id of your commit>",
740
+ obligation_evidence: item.obligationIds.map((obligationId) => ({
741
+ obligation_id: obligationId,
742
+ evidence: ["<a file, symbol or test that shows this obligation holds>"],
743
+ })),
744
+ };
745
+ const decision = {
746
+ contract_version: DECISION_CONTRACT_VERSION,
747
+ ...identity,
748
+ outcome: "<one of the three forms below>",
749
+ };
750
+ return [
751
+ `When HEAD contains your commit, write this JSON to \`${item.resultPath}\`. Change only the ${item.obligationIds.length > 0 ? "marked values" : "marked value"}:`,
752
+ "",
753
+ "```json",
754
+ JSON.stringify(landed, null, 2),
755
+ "```",
756
+ "",
757
+ `If no edit should land, write this JSON to \`${item.resultPath}\` instead. Replace \`outcome\` with one of the three forms below:`,
758
+ "",
759
+ "```json",
760
+ JSON.stringify(decision, null, 2),
761
+ "```",
762
+ "",
763
+ '- `{"status": "resolved_no_change", "evidence": ["<why the code needs no change>"]}`',
764
+ '- `{"status": "blocked", "failure_reason": "<what stops the work>"}`',
765
+ '- `{"status": "needs_clarification", "question": "<your question>"}` — you can also add a `"category"`.',
666
766
  ].join("\n");
667
767
  }
668
768
  /**
@@ -740,7 +840,7 @@ function buildFindingAssignments(state, block) {
740
840
  };
741
841
  });
742
842
  }
743
- function buildWorkItem(paths, block, baselineCommit, state) {
843
+ function buildWorkItem(paths, runId, block, baselineCommit, state) {
744
844
  // The consumed-shape gate runs FIRST: a block outside the write-scope /
745
845
  // command contract must never become a work item, so nothing downstream can
746
846
  // dispatch it or execute its commands.
@@ -771,7 +871,7 @@ function buildWorkItem(paths, block, baselineCommit, state) {
771
871
  //
772
872
  // The per-item required tests are the block's OWN commands, unchanged, which
773
873
  // is what this field meant before the gates were folded in and what the
774
- // result's `test_evidence` is compared against index-for-index at ingestion.
874
+ // tool reruns at ingestion.
775
875
  //
776
876
  // ONE FACT still rides the emitted prompt: whether the target root declares
777
877
  // any landing gate at all, so the host knows a close will run them.
@@ -786,7 +886,7 @@ function buildWorkItem(paths, block, baselineCommit, state) {
786
886
  ...new Set(block.items.flatMap((findingId) => state.plan.findings.find((entry) => entry.id === findingId)
787
887
  ?.contract_obligation_ids ?? [])),
788
888
  ].sort(compareCodeUnits);
789
- const promptText = buildPrompt({
889
+ const prompt = bindWorkerPrompt(buildPrompt({
790
890
  blockId: block.block_id,
791
891
  findingIds: block.items,
792
892
  assignments,
@@ -797,14 +897,20 @@ function buildWorkItem(paths, block, baselineCommit, state) {
797
897
  hasLandingGates,
798
898
  resultPath,
799
899
  moduleContracts: block.module_contracts ?? [],
800
- });
900
+ }), (promptDigest) => renderResultTemplate({
901
+ runId,
902
+ blockId: block.block_id,
903
+ resultPath,
904
+ obligationIds,
905
+ promptDigest,
906
+ }));
801
907
  return {
802
908
  id: block.block_id,
803
909
  finding_ids: [...block.items],
804
910
  allowed_files: allowedFiles,
805
911
  baseline_commit: baselineCommit,
806
912
  obligation_ids: obligationIds,
807
- prompt: { text: promptText, sha256: promptSha256(promptText) },
913
+ prompt: { text: prompt.text, sha256: prompt.sha256 },
808
914
  required_tests: requiredTests,
809
915
  result_path: resultPath,
810
916
  demand: deriveLaneDemand({
@@ -831,10 +937,10 @@ function buildCanonicalWorkload(params) {
831
937
  return {
832
938
  contract_version: WORKLOAD_CONTRACT_VERSION,
833
939
  run_id: params.runId,
834
- work_items: blocks.map((block) => buildWorkItem(params.paths, block, params.baselineCommit, params.state)),
940
+ work_items: blocks.map((block) => buildWorkItem(params.paths, params.runId, block, params.baselineCommit, params.state)),
835
941
  };
836
942
  }
837
- function parseWorkItem(value, paths, state, expectedBaselineCommit) {
943
+ function parseWorkItem(value, paths, runId, state, expectedBaselineCommit) {
838
944
  if (!isRecord(value) ||
839
945
  !hasExactKeys(value, [
840
946
  "allowed_files",
@@ -865,8 +971,7 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
865
971
  !isRecord(value.prompt) ||
866
972
  !hasExactKeys(value.prompt, ["sha256", "text"]) ||
867
973
  typeof value.prompt.text !== "string" ||
868
- !isSha256(value.prompt.sha256) ||
869
- promptSha256(value.prompt.text) !== value.prompt.sha256) {
974
+ !isSha256(value.prompt.sha256)) {
870
975
  return null;
871
976
  }
872
977
  const block = state.plan.blocks.find((candidate) => candidate.block_id === value.id);
@@ -874,7 +979,7 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
874
979
  return null;
875
980
  let expected;
876
981
  try {
877
- expected = buildWorkItem(paths, block, expectedBaselineCommit ?? value.baseline_commit, state);
982
+ expected = buildWorkItem(paths, runId, block, expectedBaselineCommit ?? value.baseline_commit, state);
878
983
  }
879
984
  catch {
880
985
  return null;
@@ -898,7 +1003,7 @@ function parseWorkload(value, paths, runId, state) {
898
1003
  binding.workload_sha256)) {
899
1004
  return null;
900
1005
  }
901
- const workItems = parseAllWorkloadItems(envelope.rawItems, (item) => parseWorkItem(item, paths, state, binding?.baseline_commit));
1006
+ const workItems = parseAllWorkloadItems(envelope.rawItems, (item) => parseWorkItem(item, paths, runId, state, binding?.baseline_commit));
902
1007
  if (workItems === null)
903
1008
  return null;
904
1009
  const ids = workItems.map((item) => item.id);
@@ -927,7 +1032,7 @@ const remediationScanMessages = {
927
1032
  contractInvalid: (detail) => detail,
928
1033
  duplicate: (resultId) => `result_id ${resultId} is duplicated in this workload`,
929
1034
  };
930
- function parseResult(value, runId, workItem, root) {
1035
+ function parseResult(value, runId, workItem) {
931
1036
  if (isRecord(value) && value.contract_version === DECISION_CONTRACT_VERSION) {
932
1037
  // Envelope first, identity second: one message (the host repairs the same
933
1038
  // file either way), two registered checks, so the issue names which failed.
@@ -990,29 +1095,27 @@ function parseResult(value, runId, workItem, root) {
990
1095
  result: value,
991
1096
  };
992
1097
  }
1098
+ // v1alpha3 (owner review of prompt 20, 2026-09-18): the host states the
1099
+ // landed commit and the obligation evidence, and nothing it could only
1100
+ // restate. The changed files come from git, the tests from the tool's own
1101
+ // rerun, and the landing from ancestry — `corroborateHostResult` derives and
1102
+ // checks each of them, so an extra key here is a refusal, never a hint.
993
1103
  if (!isRecord(value) ||
994
1104
  !hasExactKeys(value, [
995
- "acceptance",
996
- "changed_files",
997
- "commit_evidence",
998
1105
  "contract_version",
999
- "merge",
1106
+ "landed_commit",
1000
1107
  "obligation_evidence",
1001
1108
  "prompt_sha256",
1002
1109
  "result_id",
1003
1110
  "run_id",
1004
- "test_evidence",
1005
1111
  "work_item_id",
1006
- "worktree_evidence",
1007
1112
  ]) ||
1008
1113
  value.contract_version !== RESULT_CONTRACT_VERSION) {
1009
1114
  return invalidResult("result_envelope", "result must match the exact current contract, run, work-item, and prompt binding");
1010
1115
  }
1011
1116
  // The identity walk AND its vocabulary are the CORE's, so the same broken
1012
1117
  // submission is reported with the same named component whichever half of the
1013
- // pipeline reads it. Only the framing is this draw's — and the framing is the
1014
- // part that used to differ, producing two undifferentiated sentences that
1015
- // named no component at all.
1118
+ // pipeline reads it. Only the framing is this draw's.
1016
1119
  const resultIdentityFailure = identityFailureDiagnostic(value, {
1017
1120
  runId,
1018
1121
  workItemId: workItem.id,
@@ -1021,32 +1124,8 @@ function parseResult(value, runId, workItem, root) {
1021
1124
  if (resultIdentityFailure !== null) {
1022
1125
  return invalidResult("identity_binding", `result must match the exact current contract, run, work-item, and prompt binding: ${resultIdentityFailure}`);
1023
1126
  }
1024
- const changedFiles = stringArray(value.changed_files);
1025
- if (!changedFiles ||
1026
- changedFiles.length === 0 ||
1027
- new Set(changedFiles).size !== changedFiles.length ||
1028
- !sameStrings([...changedFiles].sort(compareCodeUnits), changedFiles) ||
1029
- changedFiles.some((path) => !pathIsAllowedByWriteScope(root, path, workItem.allowed_files))) {
1030
- return invalidResult("write_scope", "changed_files must be non-empty, sorted, unique, normalized paths within allowed_files");
1031
- }
1032
- if (!isRecord(value.commit_evidence) ||
1033
- !hasExactKeys(value.commit_evidence, ["after", "before"]) ||
1034
- value.commit_evidence.before !== workItem.baseline_commit ||
1035
- !isCommit(value.commit_evidence.after) ||
1036
- value.commit_evidence.after === value.commit_evidence.before) {
1037
- return invalidResult("commit_evidence", "commit_evidence must bind the workload baseline to a distinct full commit id");
1038
- }
1039
- if (!Array.isArray(value.test_evidence) ||
1040
- value.test_evidence.length !== workItem.required_tests.length) {
1041
- return invalidResult("test_evidence", "test_evidence must contain exactly one entry for every required test");
1042
- }
1043
- for (const [index, evidence] of value.test_evidence.entries()) {
1044
- if (!isRecord(evidence) ||
1045
- !hasExactKeys(evidence, ["command", "status"]) ||
1046
- evidence.command !== workItem.required_tests[index] ||
1047
- evidence.status !== "passed") {
1048
- return invalidResult("test_evidence", `test_evidence[${index}] must echo the bound command with status passed`);
1049
- }
1127
+ if (!isCommit(value.landed_commit)) {
1128
+ return invalidResult("landed_commit", "landed_commit must be the full id of the commit that carries this item's edits");
1050
1129
  }
1051
1130
  // The evidence-coverage floor: the cited obligation set must equal the
1052
1131
  // work item's tool-owned binding exactly. Shape and coverage only — judging
@@ -1087,23 +1166,6 @@ function parseResult(value, runId, workItem, root) {
1087
1166
  if (unknown.length > 0) {
1088
1167
  return invalidResult("obligation_evidence", `obligation_evidence cites obligations the work item does not bind: ${unknown.join(", ")}`);
1089
1168
  }
1090
- if (!isRecord(value.worktree_evidence) ||
1091
- !hasExactKeys(value.worktree_evidence, ["baseline_commit", "changed_files"]) ||
1092
- value.worktree_evidence.baseline_commit !== workItem.baseline_commit) {
1093
- return invalidResult("worktree_evidence", "worktree_evidence must bind the workload baseline and changed-file list");
1094
- }
1095
- const worktreeFiles = stringArray(value.worktree_evidence.changed_files);
1096
- if (!worktreeFiles || !sameStrings(worktreeFiles, changedFiles)) {
1097
- return invalidResult("worktree_evidence", "worktree_evidence.changed_files must exactly equal changed_files");
1098
- }
1099
- if (!isRecord(value.acceptance) ||
1100
- !hasExactKeys(value.acceptance, ["status"]) ||
1101
- value.acceptance.status !== "accepted" ||
1102
- !isRecord(value.merge) ||
1103
- !hasExactKeys(value.merge, ["status"]) ||
1104
- value.merge.status !== "merged") {
1105
- return invalidResult("landing_attestation", "acceptance and merge must both attest a completed landing");
1106
- }
1107
1169
  return {
1108
1170
  ok: true,
1109
1171
  kind: "landed",
@@ -1625,7 +1687,7 @@ function requiredTestIssue(workItem, failures) {
1625
1687
  : failures.some((failure) => failure.outcome === "output_overflow")
1626
1688
  ? "required_test_output_overflow"
1627
1689
  : "required_test_failed",
1628
- check: "test_evidence",
1690
+ check: "required_tests",
1629
1691
  work_item_id: workItem.id,
1630
1692
  result_path: workItem.result_path,
1631
1693
  message: boundRequiredTestMessage(body),
@@ -1689,23 +1751,50 @@ export async function precomputeRecoveryTestVerdicts(params) {
1689
1751
  async function corroborateHostResult(params) {
1690
1752
  const { root, state, workItem, result, verdicts } = params;
1691
1753
  const baseline = workItem.baseline_commit;
1692
- const landed = result.commit_evidence.after;
1754
+ const landed = result.landed_commit;
1693
1755
  let usedRecovery = false;
1694
- if (!(await gitCommitExists(root, baseline)) ||
1695
- !(await gitCommitExists(root, landed))) {
1756
+ if (!(await gitCommitExists(root, baseline))) {
1757
+ return {
1758
+ ok: false,
1759
+ code: "baseline_missing",
1760
+ check: "landed_commit",
1761
+ message: "the work item's baseline_commit is not in this repository",
1762
+ };
1763
+ }
1764
+ if (!(await gitCommitExists(root, landed))) {
1696
1765
  return {
1697
1766
  ok: false,
1698
1767
  code: "commit_missing",
1699
- check: "commit_evidence",
1700
- message: "baseline_commit and commit_evidence.after must both resolve to real commits",
1768
+ check: "landed_commit",
1769
+ message: "landed_commit is not a commit in this repository",
1770
+ };
1771
+ }
1772
+ if (landed === baseline) {
1773
+ return {
1774
+ ok: false,
1775
+ code: "landed_commit_invalid",
1776
+ check: "landed_commit",
1777
+ message: "landed_commit is the baseline commit; name the commit that carries this item's edits",
1701
1778
  };
1702
1779
  }
1703
1780
  if (!(await gitCommitIsAncestor(root, baseline, landed))) {
1704
1781
  if (!params.recovery) {
1782
+ // An ORPHANED baseline (no ref contains it, HEAD cannot reach it) is a
1783
+ // fault in the run, not in the work: no commit could ever descend from
1784
+ // it. The spawn-free recovery verb is its named repair.
1785
+ if (await gitCommitIsOrphaned(root, baseline)) {
1786
+ return {
1787
+ ok: false,
1788
+ code: "baseline_orphaned",
1789
+ check: "landed_commit",
1790
+ message: "the work item's baseline_commit is orphaned (history was rewritten under the run); " +
1791
+ "the operator can run `recover-ingest` to accept the landed work",
1792
+ };
1793
+ }
1705
1794
  return {
1706
1795
  ok: false,
1707
1796
  code: "baseline_not_ancestor",
1708
- check: "commit_evidence",
1797
+ check: "landed_commit",
1709
1798
  message: "the trusted workload baseline is not an ancestor of the claimed landed commit",
1710
1799
  };
1711
1800
  }
@@ -1722,7 +1811,7 @@ async function corroborateHostResult(params) {
1722
1811
  return {
1723
1812
  ok: false,
1724
1813
  code: "baseline_not_ancestor",
1725
- check: "commit_evidence",
1814
+ check: "landed_commit",
1726
1815
  message: "the trusted workload baseline is not an ancestor of the claimed landed commit, " +
1727
1816
  "and the baseline is NOT orphaned (a ref still contains it, or it is reachable " +
1728
1817
  "from HEAD), so the stale-worker protection stands and recovery cannot waive it",
@@ -1734,25 +1823,27 @@ async function corroborateHostResult(params) {
1734
1823
  return {
1735
1824
  ok: false,
1736
1825
  code: "commit_not_landed",
1737
- check: "commit_evidence",
1738
- message: "commit_evidence.after is not reachable from the repository HEAD",
1826
+ check: "landed_commit",
1827
+ message: "landed_commit is not reachable from the repository HEAD",
1739
1828
  };
1740
1829
  }
1741
1830
  const actualFiles = await gitChangedFilesOfCommit(root, landed);
1742
- if (!actualFiles || !sameStrings(actualFiles, result.changed_files)) {
1831
+ if (!actualFiles || actualFiles.length === 0) {
1743
1832
  return {
1744
1833
  ok: false,
1745
- code: "changed_files_mismatch",
1746
- check: "write_scope",
1747
- message: "the landed commit's mechanically derived changed files do not exactly match changed_files",
1834
+ code: "landed_commit_invalid",
1835
+ check: "landed_commit",
1836
+ message: "landed_commit changes no file; name the commit that carries this item's edits",
1748
1837
  };
1749
1838
  }
1750
- if (actualFiles.some((path) => !pathIsAllowedByWriteScope(root, path, workItem.allowed_files))) {
1839
+ const outOfScope = actualFiles.filter((path) => !pathIsAllowedByWriteScope(root, path, workItem.allowed_files));
1840
+ if (outOfScope.length > 0) {
1751
1841
  return {
1752
1842
  ok: false,
1753
1843
  code: "changed_files_mismatch",
1754
1844
  check: "write_scope",
1755
- message: "the landed commit changed a file outside the prompt-bound allowed_files",
1845
+ message: "the landed commit changed files outside the prompt-bound allowed_files: " +
1846
+ outOfScope.join(", "),
1756
1847
  };
1757
1848
  }
1758
1849
  const runStartDirty = new Set((state.run_start_dirty ?? []).map(normalizeRepoPath));
@@ -1761,17 +1852,31 @@ async function corroborateHostResult(params) {
1761
1852
  return {
1762
1853
  ok: false,
1763
1854
  code: "run_start_dirty_overlap",
1764
- check: "worktree_evidence",
1855
+ check: "run_start_dirt",
1765
1856
  message: `landed files overlap pre-existing run-start dirt: ${dirtyOverlap.join(", ")}`,
1766
1857
  };
1767
1858
  }
1768
1859
  const failedTests = await rerunRequiredTests(root, workItem.required_tests, verdicts);
1769
1860
  if (failedTests.length > 0) {
1770
1861
  const issue = requiredTestIssue(workItem, failedTests);
1771
- return { ok: false, code: issue.code, check: "test_evidence", message: issue.message };
1862
+ return { ok: false, code: issue.code, check: "required_tests", message: issue.message };
1772
1863
  }
1773
1864
  return { ok: true, changedFiles: actualFiles, usedRecovery };
1774
1865
  }
1866
+ /**
1867
+ * Was this persisted workload written under an earlier workload contract
1868
+ * version? Only a document that NAMES a different version counts: a document
1869
+ * with no version, or not an object at all, is invalid, not stale.
1870
+ */
1871
+ function persistedWorkloadIsStale(value) {
1872
+ return (isRecord(value) &&
1873
+ typeof value.contract_version === "string" &&
1874
+ value.contract_version !== WORKLOAD_CONTRACT_VERSION);
1875
+ }
1876
+ async function persistedWorkloadFileIsStale(workloadPath) {
1877
+ const read = await readSubmissionDocument(workloadPath);
1878
+ return read.kind === "value" && persistedWorkloadIsStale(read.value);
1879
+ }
1775
1880
  export async function prepareRemediationHostHandoff(params) {
1776
1881
  const state = parseCurrentState(params.state);
1777
1882
  if (!state)
@@ -1826,18 +1931,29 @@ export async function prepareRemediationHostHandoff(params) {
1826
1931
  .join("; ")}`);
1827
1932
  }
1828
1933
  const workloadDigest = contentSha256(workload);
1934
+ // A binding whose workload file an earlier contract version wrote is
1935
+ // re-minted in place: same run, baseline and work item ids, new digest. Only
1936
+ // the DIGEST moves — anything else that no longer matches is still a refusal.
1937
+ const remintStaleDigest = existingRecord !== undefined &&
1938
+ existingRecord.workload_sha256 !== workloadDigest &&
1939
+ (await persistedWorkloadFileIsStale(paths.workloadPath));
1829
1940
  if (existingRecord &&
1830
- existingRecord.workload_sha256 !== workloadDigest) {
1941
+ existingRecord.workload_sha256 !== workloadDigest &&
1942
+ !remintStaleDigest) {
1831
1943
  throw new Error("Trusted remediation host workload no longer matches the persisted state binding");
1832
1944
  }
1833
- const handoffRecord = existingRecord ?? {
1834
- contract_version: REMEDIATION_HOST_HANDOFF_RECORD_V1ALPHA2,
1835
- scope_semantics: REMEDIATION_HOST_SCOPE_SEMANTICS,
1836
- run_id: params.runId,
1837
- baseline_commit: baselineCommit,
1838
- workload_sha256: workloadDigest,
1839
- work_item_ids: workload.work_items.map((item) => item.id),
1840
- };
1945
+ const handoffRecord = existingRecord
1946
+ ? remintStaleDigest
1947
+ ? { ...existingRecord, workload_sha256: workloadDigest }
1948
+ : existingRecord
1949
+ : {
1950
+ contract_version: REMEDIATION_HOST_HANDOFF_RECORD_V1ALPHA2,
1951
+ scope_semantics: REMEDIATION_HOST_SCOPE_SEMANTICS,
1952
+ run_id: params.runId,
1953
+ baseline_commit: baselineCommit,
1954
+ workload_sha256: workloadDigest,
1955
+ work_item_ids: workload.work_items.map((item) => item.id),
1956
+ };
1841
1957
  await mkdir(paths.resultDir, { recursive: true });
1842
1958
  await writeJsonFile(paths.workloadPath, workload);
1843
1959
  return {
@@ -1867,8 +1983,8 @@ export async function prepareRemediationHostHandoff(params) {
1867
1983
  * remote ref) also fails the ancestry test when work lands elsewhere, and that
1868
1984
  * is the ordinary stale-worker case — recovery refuses it. Every other
1869
1985
  * corroboration check runs unchanged (the landed commit exists and is reachable
1870
- * from HEAD; its mechanically derived changed files exactly equal
1871
- * `changed_files` and lie within the prompt-bound `allowed_files`; no overlap
1986
+ * from HEAD; its mechanically derived changed files lie within the
1987
+ * prompt-bound `allowed_files`; no overlap
1872
1988
  * with run-start dirt; the required tests rerun green), `parseResult` stays
1873
1989
  * fully strict, and dependency/phase eligibility is enforced exactly as on the
1874
1990
  * normal lane.
@@ -2014,6 +2130,29 @@ async function validateHostResultBundle(input) {
2014
2130
  },
2015
2131
  };
2016
2132
  }
2133
+ // A workload an EARLIER contract version wrote is not a defect in it: the
2134
+ // tool changed underneath a live binding. It is reported as stale — no host
2135
+ // action — and `prepareRemediationHostHandoff` re-mints the digest and
2136
+ // rewrites the file on the same step, under the same work item ids.
2137
+ if (state.host_handoff && persistedWorkloadIsStale(workloadRead.value)) {
2138
+ issues.push({
2139
+ code: "workload_stale",
2140
+ check: "workload_binding",
2141
+ message: "the workload file was written under an earlier contract version; the tool writes it again",
2142
+ });
2143
+ return {
2144
+ kind: "summary",
2145
+ summary: {
2146
+ accepted_count: 0,
2147
+ completed_work_item_ids: [],
2148
+ pending_work_item_ids: state.host_handoff.work_item_ids,
2149
+ issues,
2150
+ work_item_outcomes: new Map(),
2151
+ state_changed: false,
2152
+ state: nextState,
2153
+ },
2154
+ };
2155
+ }
2017
2156
  const workload = parseWorkload(workloadRead.value, paths, input.runId, state);
2018
2157
  if (!workload) {
2019
2158
  // Accumulated, not replaced: when a block-contract defect is WHY the
@@ -2075,8 +2214,8 @@ async function validateHostResultBundle(input) {
2075
2214
  *
2076
2215
  * Ground truth, not a claim: `host_landed_commit` is written only after
2077
2216
  * `corroborateHostResult` verified the commit resolves, is reachable from HEAD,
2078
- * and that its mechanically derived diff equals the result's `changed_files`
2079
- * within the item's write scope. Anything less is not a landing.
2217
+ * and that its mechanically derived diff lies within the item's write scope.
2218
+ * Anything less is not a landing.
2080
2219
  *
2081
2220
  * ANY finding of the item counts, because the commit is attributed per FINDING
2082
2221
  * and a work item is a bundle of them: a commit for one of its findings is work
@@ -2096,7 +2235,7 @@ async function executeHostVerificationReruns(ctx, acc) {
2096
2235
  continue;
2097
2236
  if (!ctx.eligibleIds.has(workItem.id)) {
2098
2237
  acc.issues.push({
2099
- code: "submission_contract_invalid",
2238
+ code: "work_item_not_eligible",
2100
2239
  work_item_id: workItem.id,
2101
2240
  result_path: workItem.result_path,
2102
2241
  message: "the work item is no longer dependency/phase eligible",
@@ -2109,7 +2248,7 @@ async function executeHostVerificationReruns(ctx, acc) {
2109
2248
  workItemId: workItem.id,
2110
2249
  resultPath: workItem.result_path,
2111
2250
  parse: (value) => {
2112
- const result = parseResult(value, ctx.runId, workItem, paths.root);
2251
+ const result = parseResult(value, ctx.runId, workItem);
2113
2252
  return result.ok
2114
2253
  ? { ok: true, parsed: result }
2115
2254
  : { ok: false, check: result.check, detail: result.reason };
@@ -2239,7 +2378,7 @@ async function executeHostVerificationReruns(ctx, acc) {
2239
2378
  // landed commit is carried on the event as `landed_commit` and read
2240
2379
  // back through `recoveryMarkMatches` — prose is not an identity, and a
2241
2380
  // message reworded by any later edit silently un-deduped the mark.
2242
- const landedCommit = result.commit_evidence.after;
2381
+ const landedCommit = result.landed_commit;
2243
2382
  const alreadyMarked = acc.recordedRecoveryMarks.some((event) => event.run_id === ctx.runId &&
2244
2383
  recoveryMarkMatches(event, workItem.id, landedCommit));
2245
2384
  if (!alreadyMarked) {
@@ -2283,7 +2422,7 @@ async function executeHostVerificationReruns(ctx, acc) {
2283
2422
  workItem,
2284
2423
  pendingItems,
2285
2424
  at: new Date().toISOString(),
2286
- landedCommit: result.commit_evidence.after,
2425
+ landedCommit: result.landed_commit,
2287
2426
  changedFiles: corroborated.changedFiles,
2288
2427
  });
2289
2428
  for (const changedFile of corroborated.changedFiles) {
@@ -2324,17 +2463,14 @@ function commitRemediationStateUpdates(ctx, acc, verdicts, issues) {
2324
2463
  item.status = "needs_clarification";
2325
2464
  delete item.completed_at;
2326
2465
  item.failure_reason = outcome.question;
2327
- const clarifications = nextState.clarifications ?? [];
2328
- if (!clarifications.some((entry) => entry.finding_id === findingId)) {
2329
- clarifications.push({
2330
- finding_id: findingId,
2331
- category: isClarificationCategory(outcome.category)
2332
- ? outcome.category
2333
- : "scope_of_fix",
2334
- description: outcome.question,
2335
- });
2336
- }
2337
- nextState.clarifications = clarifications;
2466
+ // The question lives on the item (its one home): the clarification
2467
+ // round lists every paused item, so no answer can erase another's.
2468
+ item.clarification_question = {
2469
+ category: isClarificationCategory(outcome.category)
2470
+ ? outcome.category
2471
+ : "scope_of_fix",
2472
+ description: outcome.question,
2473
+ };
2338
2474
  }
2339
2475
  }
2340
2476
  continue;