audit-tools 0.51.0 → 0.51.2

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 (254) hide show
  1. package/README.md +1 -1
  2. package/dist/audit/cli/cleanup.d.ts +0 -1
  3. package/dist/audit/cli/cleanup.d.ts.map +1 -1
  4. package/dist/audit/cli/cleanup.js +32 -20
  5. package/dist/audit/cli/cleanup.js.map +1 -1
  6. package/dist/audit/cli/dispatch/hostHandoff.d.ts +2 -0
  7. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  8. package/dist/audit/cli/dispatch/hostHandoff.js +38 -20
  9. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  10. package/dist/audit/cli/forceSynthesisCommand.d.ts.map +1 -1
  11. package/dist/audit/cli/forceSynthesisCommand.js +2 -14
  12. package/dist/audit/cli/forceSynthesisCommand.js.map +1 -1
  13. package/dist/audit/cli/intakeCommand.d.ts.map +1 -1
  14. package/dist/audit/cli/intakeCommand.js +3 -13
  15. package/dist/audit/cli/intakeCommand.js.map +1 -1
  16. package/dist/audit/cli/laneSubmissions.d.ts +4 -30
  17. package/dist/audit/cli/laneSubmissions.d.ts.map +1 -1
  18. package/dist/audit/cli/laneSubmissions.js +2 -95
  19. package/dist/audit/cli/laneSubmissions.js.map +1 -1
  20. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  21. package/dist/audit/cli/nextStepCommand.js +105 -92
  22. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  23. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  24. package/dist/audit/cli/nextStepHelpers.js +6 -43
  25. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  26. package/dist/audit/cli/resynthesizeCommand.d.ts.map +1 -1
  27. package/dist/audit/cli/resynthesizeCommand.js +5 -2
  28. package/dist/audit/cli/resynthesizeCommand.js.map +1 -1
  29. package/dist/audit/cli/sampleRunCommand.d.ts +1 -1
  30. package/dist/audit/cli/stepScaffold.d.ts +21 -0
  31. package/dist/audit/cli/stepScaffold.d.ts.map +1 -0
  32. package/dist/audit/cli/stepScaffold.js +50 -0
  33. package/dist/audit/cli/stepScaffold.js.map +1 -0
  34. package/dist/audit/cli/steps.d.ts +6 -6
  35. package/dist/audit/cli/synthesizeCommand.d.ts.map +1 -1
  36. package/dist/audit/cli/synthesizeCommand.js +2 -14
  37. package/dist/audit/cli/synthesizeCommand.js.map +1 -1
  38. package/dist/audit/contracts/workerSchemas.d.ts +4 -4
  39. package/dist/audit/extractors/analyzers/merge.d.ts.map +1 -1
  40. package/dist/audit/extractors/analyzers/merge.js +2 -7
  41. package/dist/audit/extractors/analyzers/merge.js.map +1 -1
  42. package/dist/audit/extractors/commentDecomposition.d.ts.map +1 -1
  43. package/dist/audit/extractors/commentDecomposition.js +34 -22
  44. package/dist/audit/extractors/commentDecomposition.js.map +1 -1
  45. package/dist/audit/extractors/graph.d.ts +1 -1
  46. package/dist/audit/extractors/graph.d.ts.map +1 -1
  47. package/dist/audit/extractors/graph.js +2 -6
  48. package/dist/audit/extractors/graph.js.map +1 -1
  49. package/dist/audit/extractors/graphManifestEdges/cargo.d.ts.map +1 -1
  50. package/dist/audit/extractors/graphManifestEdges/cargo.js +4 -4
  51. package/dist/audit/extractors/graphManifestEdges/cargo.js.map +1 -1
  52. package/dist/audit/extractors/graphManifestEdges/pnpm.d.ts.map +1 -1
  53. package/dist/audit/extractors/graphManifestEdges/pnpm.js +3 -3
  54. package/dist/audit/extractors/graphManifestEdges/pnpm.js.map +1 -1
  55. package/dist/audit/extractors/graphManifestEdges/pyproject.d.ts.map +1 -1
  56. package/dist/audit/extractors/graphManifestEdges/pyproject.js +3 -2
  57. package/dist/audit/extractors/graphManifestEdges/pyproject.js.map +1 -1
  58. package/dist/audit/extractors/graphManifestEdges/toml.d.ts +0 -6
  59. package/dist/audit/extractors/graphManifestEdges/toml.d.ts.map +1 -1
  60. package/dist/audit/extractors/graphManifestEdges/toml.js +0 -13
  61. package/dist/audit/extractors/graphManifestEdges/toml.js.map +1 -1
  62. package/dist/audit/extractors/graphManifestEdges/workspace.d.ts +9 -0
  63. package/dist/audit/extractors/graphManifestEdges/workspace.d.ts.map +1 -1
  64. package/dist/audit/extractors/graphManifestEdges/workspace.js +16 -0
  65. package/dist/audit/extractors/graphManifestEdges/workspace.js.map +1 -1
  66. package/dist/audit/extractors/graphManifestEdges/yaml.d.ts +0 -2
  67. package/dist/audit/extractors/graphManifestEdges/yaml.d.ts.map +1 -1
  68. package/dist/audit/extractors/graphManifestEdges/yaml.js +0 -9
  69. package/dist/audit/extractors/graphManifestEdges/yaml.js.map +1 -1
  70. package/dist/audit/extractors/graphSignals.d.ts.map +1 -1
  71. package/dist/audit/extractors/graphSignals.js +16 -2
  72. package/dist/audit/extractors/graphSignals.js.map +1 -1
  73. package/dist/audit/io/artifacts.d.ts +19 -0
  74. package/dist/audit/io/artifacts.d.ts.map +1 -1
  75. package/dist/audit/io/artifacts.js +44 -2
  76. package/dist/audit/io/artifacts.js.map +1 -1
  77. package/dist/audit/orchestrator/advance.d.ts +0 -22
  78. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  79. package/dist/audit/orchestrator/advance.js +1 -43
  80. package/dist/audit/orchestrator/advance.js.map +1 -1
  81. package/dist/audit/orchestrator/charterPackets.d.ts +2 -2
  82. package/dist/audit/orchestrator/charterPackets.d.ts.map +1 -1
  83. package/dist/audit/orchestrator/charterPackets.js +79 -230
  84. package/dist/audit/orchestrator/charterPackets.js.map +1 -1
  85. package/dist/audit/orchestrator/edgeReasoning.d.ts.map +1 -1
  86. package/dist/audit/orchestrator/edgeReasoning.js +4 -9
  87. package/dist/audit/orchestrator/edgeReasoning.js.map +1 -1
  88. package/dist/audit/orchestrator/intentCheckpointGate.d.ts.map +1 -1
  89. package/dist/audit/orchestrator/intentCheckpointGate.js +11 -2
  90. package/dist/audit/orchestrator/intentCheckpointGate.js.map +1 -1
  91. package/dist/audit/orchestrator/lineCounts.d.ts +44 -0
  92. package/dist/audit/orchestrator/lineCounts.d.ts.map +1 -0
  93. package/dist/audit/orchestrator/lineCounts.js +61 -0
  94. package/dist/audit/orchestrator/lineCounts.js.map +1 -0
  95. package/dist/audit/orchestrator/obligationDerive.d.ts +35 -0
  96. package/dist/audit/orchestrator/obligationDerive.d.ts.map +1 -0
  97. package/dist/audit/orchestrator/obligationDerive.js +50 -0
  98. package/dist/audit/orchestrator/obligationDerive.js.map +1 -0
  99. package/dist/audit/orchestrator/reviewPacketMetrics.d.ts.map +1 -1
  100. package/dist/audit/orchestrator/reviewPacketMetrics.js +3 -2
  101. package/dist/audit/orchestrator/reviewPacketMetrics.js.map +1 -1
  102. package/dist/audit/orchestrator/reviewPacketShared.d.ts +0 -2
  103. package/dist/audit/orchestrator/reviewPacketShared.d.ts.map +1 -1
  104. package/dist/audit/orchestrator/reviewPacketShared.js +0 -4
  105. package/dist/audit/orchestrator/reviewPacketShared.js.map +1 -1
  106. package/dist/audit/orchestrator/reviewPackets.d.ts.map +1 -1
  107. package/dist/audit/orchestrator/reviewPackets.js +3 -2
  108. package/dist/audit/orchestrator/reviewPackets.js.map +1 -1
  109. package/dist/audit/orchestrator/selectiveDeepening/conflict.d.ts.map +1 -1
  110. package/dist/audit/orchestrator/selectiveDeepening/conflict.js +3 -2
  111. package/dist/audit/orchestrator/selectiveDeepening/conflict.js.map +1 -1
  112. package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.d.ts.map +1 -1
  113. package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js +3 -2
  114. package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js.map +1 -1
  115. package/dist/audit/orchestrator/selectiveDeepening/highRiskClean.d.ts.map +1 -1
  116. package/dist/audit/orchestrator/selectiveDeepening/highRiskClean.js +3 -2
  117. package/dist/audit/orchestrator/selectiveDeepening/highRiskClean.js.map +1 -1
  118. package/dist/audit/orchestrator/selectiveDeepening/lensVerification.d.ts.map +1 -1
  119. package/dist/audit/orchestrator/selectiveDeepening/lensVerification.js +4 -3
  120. package/dist/audit/orchestrator/selectiveDeepening/lensVerification.js.map +1 -1
  121. package/dist/audit/orchestrator/selectiveDeepening/runtimeValidation.d.ts.map +1 -1
  122. package/dist/audit/orchestrator/selectiveDeepening/runtimeValidation.js +2 -1
  123. package/dist/audit/orchestrator/selectiveDeepening/runtimeValidation.js.map +1 -1
  124. package/dist/audit/orchestrator/selectiveDeepening/shared.d.ts +0 -2
  125. package/dist/audit/orchestrator/selectiveDeepening/shared.d.ts.map +1 -1
  126. package/dist/audit/orchestrator/selectiveDeepening/shared.js +0 -27
  127. package/dist/audit/orchestrator/selectiveDeepening/shared.js.map +1 -1
  128. package/dist/audit/orchestrator/selectiveDeepening/stewardFollowup.d.ts.map +1 -1
  129. package/dist/audit/orchestrator/selectiveDeepening/stewardFollowup.js +10 -1
  130. package/dist/audit/orchestrator/selectiveDeepening/stewardFollowup.js.map +1 -1
  131. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  132. package/dist/audit/reporting/synthesis.js +1 -2
  133. package/dist/audit/reporting/synthesis.js.map +1 -1
  134. package/dist/audit/systemic/secondOrderAdversaryPrompt.d.ts.map +1 -1
  135. package/dist/audit/systemic/secondOrderAdversaryPrompt.js +7 -0
  136. package/dist/audit/systemic/secondOrderAdversaryPrompt.js.map +1 -1
  137. package/dist/audit/types/conceptualAdjudication.d.ts +12 -12
  138. package/dist/audit/types.d.ts +2 -2
  139. package/dist/remediate/droppedFindingsRecord.d.ts +13 -0
  140. package/dist/remediate/droppedFindingsRecord.d.ts.map +1 -0
  141. package/dist/remediate/droppedFindingsRecord.js +113 -0
  142. package/dist/remediate/droppedFindingsRecord.js.map +1 -0
  143. package/dist/remediate/intake.d.ts +17 -0
  144. package/dist/remediate/intake.d.ts.map +1 -1
  145. package/dist/remediate/intake.js +23 -0
  146. package/dist/remediate/intake.js.map +1 -1
  147. package/dist/remediate/phases/close.d.ts +1 -1
  148. package/dist/remediate/phases/close.d.ts.map +1 -1
  149. package/dist/remediate/phases/close.js +68 -6
  150. package/dist/remediate/phases/close.js.map +1 -1
  151. package/dist/remediate/state/types.d.ts +12 -6
  152. package/dist/remediate/state/types.d.ts.map +1 -1
  153. package/dist/remediate/state/types.js +3 -1
  154. package/dist/remediate/state/types.js.map +1 -1
  155. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  156. package/dist/remediate/steps/contractPipeline.js +10 -4
  157. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  158. package/dist/remediate/steps/contractPipelinePrompts.d.ts +10 -2
  159. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  160. package/dist/remediate/steps/contractPipelinePrompts.js +56 -12
  161. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  162. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  163. package/dist/remediate/steps/dispatch/hostHandoff.js +277 -146
  164. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  165. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  166. package/dist/remediate/steps/nextStep.js +253 -75
  167. package/dist/remediate/steps/nextStep.js.map +1 -1
  168. package/dist/remediate/steps/prompts.d.ts +14 -0
  169. package/dist/remediate/steps/prompts.d.ts.map +1 -1
  170. package/dist/remediate/steps/prompts.js +52 -4
  171. package/dist/remediate/steps/prompts.js.map +1 -1
  172. package/dist/remediate/steps/types.d.ts +1 -1
  173. package/dist/remediate/steps/types.d.ts.map +1 -1
  174. package/dist/remediate/steps/types.js.map +1 -1
  175. package/dist/remediate/validation/contractPipelineGates.d.ts +0 -34
  176. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  177. package/dist/remediate/validation/contractPipelineGates.js +181 -143
  178. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  179. package/dist/shared/agentReflections.d.ts +44 -3
  180. package/dist/shared/agentReflections.d.ts.map +1 -1
  181. package/dist/shared/agentReflections.js +63 -4
  182. package/dist/shared/agentReflections.js.map +1 -1
  183. package/dist/shared/analyzers/acquisitionEngine.d.ts +20 -1
  184. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  185. package/dist/shared/analyzers/acquisitionEngine.js +17 -3
  186. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  187. package/dist/shared/analyzers/candidates.d.ts.map +1 -1
  188. package/dist/shared/analyzers/candidates.js +13 -4
  189. package/dist/shared/analyzers/candidates.js.map +1 -1
  190. package/dist/shared/decompose/systemicChallenge.d.ts +111 -9
  191. package/dist/shared/decompose/systemicChallenge.d.ts.map +1 -1
  192. package/dist/shared/decompose/systemicChallenge.js +32 -1
  193. package/dist/shared/decompose/systemicChallenge.js.map +1 -1
  194. package/dist/shared/friction/triage.d.ts.map +1 -1
  195. package/dist/shared/friction/triage.js +5 -1
  196. package/dist/shared/friction/triage.js.map +1 -1
  197. package/dist/shared/git.d.ts +6 -0
  198. package/dist/shared/git.d.ts.map +1 -1
  199. package/dist/shared/git.js +8 -0
  200. package/dist/shared/git.js.map +1 -1
  201. package/dist/shared/graph/edgeConfidence.d.ts +12 -0
  202. package/dist/shared/graph/edgeConfidence.d.ts.map +1 -0
  203. package/dist/shared/graph/edgeConfidence.js +15 -0
  204. package/dist/shared/graph/edgeConfidence.js.map +1 -0
  205. package/dist/shared/index.d.ts +12 -3
  206. package/dist/shared/index.d.ts.map +1 -1
  207. package/dist/shared/index.js +8 -3
  208. package/dist/shared/index.js.map +1 -1
  209. package/dist/shared/loopCorePaths.d.ts.map +1 -1
  210. package/dist/shared/loopCorePaths.js +4 -0
  211. package/dist/shared/loopCorePaths.js.map +1 -1
  212. package/dist/shared/submission/hostResultOutcomes.d.ts +42 -0
  213. package/dist/shared/submission/hostResultOutcomes.d.ts.map +1 -0
  214. package/dist/shared/submission/hostResultOutcomes.js +129 -0
  215. package/dist/shared/submission/hostResultOutcomes.js.map +1 -0
  216. package/dist/shared/submission/ingestionChecks.d.ts +146 -0
  217. package/dist/shared/submission/ingestionChecks.d.ts.map +1 -0
  218. package/dist/shared/submission/ingestionChecks.js +157 -0
  219. package/dist/shared/submission/ingestionChecks.js.map +1 -0
  220. package/dist/shared/submission/submissionClassifier.d.ts +8 -0
  221. package/dist/shared/submission/submissionClassifier.d.ts.map +1 -1
  222. package/dist/shared/submission/submissionClassifier.js.map +1 -1
  223. package/dist/shared/submission/submissionScan.d.ts +2 -0
  224. package/dist/shared/submission/submissionScan.d.ts.map +1 -1
  225. package/dist/shared/submission/submissionScan.js +4 -0
  226. package/dist/shared/submission/submissionScan.js.map +1 -1
  227. package/dist/shared/tooling/projectFacts.d.ts +71 -0
  228. package/dist/shared/tooling/projectFacts.d.ts.map +1 -0
  229. package/dist/shared/tooling/projectFacts.js +153 -0
  230. package/dist/shared/tooling/projectFacts.js.map +1 -0
  231. package/dist/shared/tooling/projectTestAdmission.d.ts +5 -15
  232. package/dist/shared/tooling/projectTestAdmission.d.ts.map +1 -1
  233. package/dist/shared/tooling/projectTestAdmission.js +14 -5
  234. package/dist/shared/tooling/projectTestAdmission.js.map +1 -1
  235. package/dist/shared/types/charterPacket.d.ts +18 -17
  236. package/dist/shared/types/charterPacket.d.ts.map +1 -1
  237. package/dist/shared/types/charterPacket.js.map +1 -1
  238. package/dist/shared/types/closingActions.d.ts +16 -0
  239. package/dist/shared/types/closingActions.d.ts.map +1 -0
  240. package/dist/shared/types/closingActions.js +25 -0
  241. package/dist/shared/types/closingActions.js.map +1 -0
  242. package/dist/shared/types/intentCheckpoint.d.ts +50 -0
  243. package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
  244. package/dist/shared/types/intentCheckpoint.js +31 -0
  245. package/dist/shared/types/intentCheckpoint.js.map +1 -1
  246. package/docs/audit-pkg/contracts.md +34 -12
  247. package/docs/audit-pkg/operator-guide.md +10 -7
  248. package/package.json +6 -2
  249. package/scripts/postinstall.mjs +42 -0
  250. package/skills/audit-code/audit-code.prompt.md +5 -3
  251. package/dist/remediate/state/closingActions.d.ts +0 -3
  252. package/dist/remediate/state/closingActions.d.ts.map +0 -1
  253. package/dist/remediate/state/closingActions.js +0 -10
  254. package/dist/remediate/state/closingActions.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { isAbsolute } from "node:path";
3
- import { AUDIT_TOOLS_DIRNAME, headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, compareCodeUnits, contentSha256, hasExactKeys, hostHandoffResultPath, idsAreStrictlyAscending, isCommit, isGitRepo, isRecord, isSha256, normalizeRepoPath, parseAllWorkloadItems, parseCommandString, parseWorkloadEnvelope, promptSha256, readSubmissionDocument, readSubmissionLedger, repoRelativePath, resolveContainedPath, resolveHostHandoffPaths, resultIdentityIsBound, runTrackedAsync, sameStrings, scanBoundSubmission, stringArray, stableStringify, TRACKED_CHILD_DEADLINE_MS, writeJsonFile, } from "audit-tools/shared";
3
+ import { AUDIT_TOOLS_DIRNAME, headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, enrichMissingSubmissionIssues, compareCodeUnits, contentSha256, hasExactKeys, hostHandoffResultPath, idsAreStrictlyAscending, isCommit, isGitRepo, isRecord, isSha256, normalizeRepoPath, parseAllWorkloadItems, parseCommandString, parseWorkloadEnvelope, promptSha256, readSubmissionDocument, readSubmissionLedger, readTrailingSubmissionRefusals, recordHostResultOutcomes, repoRelativePath, resolveContainedPath, resolveHostHandoffPaths, resultIdentityIsBound, runTrackedAsync, sameStrings, scanBoundSubmission, stringArray, stableStringify, TRACKED_CHILD_DEADLINE_MS, writeJsonFile, } from "audit-tools/shared";
4
4
  import { StateStore } from "../../state/store.js";
5
5
  import { REMEDIATION_HOST_HANDOFF_RECORD_V1ALPHA1, REMEDIATION_HOST_HANDOFF_RECORD_V1ALPHA2, REMEDIATION_HOST_SCOPE_SEMANTICS, RemediationHostHandoffRecordSchema, RemediationPlanSchema, isClarificationCategory, } from "../../state/types.js";
6
6
  import { ITEM_STATUSES, isTerminalStatus, isVerifiedCompleteStatus, } from "../../state/itemStatus.js";
@@ -481,7 +481,7 @@ export async function remediationSubmissionBinding(params) {
481
481
  const parsed = parseResult(value, params.runId, validationWorkItem, paths.root);
482
482
  return parsed.ok
483
483
  ? null
484
- : { code: "submission_contract_invalid", message: parsed.reason };
484
+ : { code: "submission_contract_invalid", check: parsed.check, message: parsed.reason };
485
485
  },
486
486
  };
487
487
  }
@@ -799,8 +799,9 @@ function parseWorkload(value, paths, runId, state) {
799
799
  }
800
800
  return value;
801
801
  }
802
- function invalidResult(reason) {
803
- return { ok: false, reason };
802
+ /** `check` names the registered ingestion check that failed; `reason` is its prose. */
803
+ function invalidResult(check, reason) {
804
+ return { ok: false, check, reason };
804
805
  }
805
806
  /**
806
807
  * This draw's refusal vocabulary for {@link scanBoundSubmission}. The scan owns
@@ -817,6 +818,8 @@ const remediationScanMessages = {
817
818
  };
818
819
  function parseResult(value, runId, workItem, root) {
819
820
  if (isRecord(value) && value.contract_version === DECISION_CONTRACT_VERSION) {
821
+ // Envelope first, identity second: one message (the host repairs the same
822
+ // file either way), two registered checks, so the issue names which failed.
820
823
  if (!hasExactKeys(value, [
821
824
  "contract_version",
822
825
  "result_id",
@@ -825,14 +828,16 @@ function parseResult(value, runId, workItem, root) {
825
828
  "prompt_sha256",
826
829
  "outcome",
827
830
  ]) ||
828
- !resultIdentityIsBound(value, {
829
- runId,
830
- workItemId: workItem.id,
831
- promptSha256: workItem.prompt.sha256,
832
- }) ||
833
831
  !isRecord(value.outcome) ||
834
832
  typeof value.outcome.status !== "string") {
835
- return invalidResult("decision must match the exact current run, work-item, and prompt binding");
833
+ return invalidResult("result_envelope", "decision must match the exact current run, work-item, and prompt binding");
834
+ }
835
+ if (!resultIdentityIsBound(value, {
836
+ runId,
837
+ workItemId: workItem.id,
838
+ promptSha256: workItem.prompt.sha256,
839
+ })) {
840
+ return invalidResult("identity_binding", "decision must match the exact current run, work-item, and prompt binding");
836
841
  }
837
842
  const outcome = value.outcome;
838
843
  if (outcome.status === "resolved_no_change") {
@@ -841,14 +846,14 @@ function parseResult(value, runId, workItem, root) {
841
846
  !evidence ||
842
847
  evidence.length === 0 ||
843
848
  evidence.some((entry) => entry.trim().length === 0)) {
844
- return invalidResult("resolved_no_change requires a non-empty evidence string array");
849
+ return invalidResult("outcome_shape", "resolved_no_change requires a non-empty evidence string array");
845
850
  }
846
851
  }
847
852
  else if (outcome.status === "blocked") {
848
853
  if (!hasExactKeys(outcome, ["status", "failure_reason"]) ||
849
854
  typeof outcome.failure_reason !== "string" ||
850
855
  outcome.failure_reason.trim().length === 0) {
851
- return invalidResult("blocked requires a non-empty failure_reason");
856
+ return invalidResult("outcome_shape", "blocked requires a non-empty failure_reason");
852
857
  }
853
858
  }
854
859
  else if (outcome.status === "needs_clarification") {
@@ -857,11 +862,11 @@ function parseResult(value, runId, workItem, root) {
857
862
  outcome.question.trim().length === 0 ||
858
863
  (outcome.category !== undefined &&
859
864
  !isClarificationCategory(outcome.category))) {
860
- return invalidResult("needs_clarification requires a non-empty question and optional canonical category");
865
+ return invalidResult("outcome_shape", "needs_clarification requires a non-empty question and optional canonical category");
861
866
  }
862
867
  }
863
868
  else {
864
- return invalidResult("decision outcome.status must be resolved_no_change, blocked, or needs_clarification");
869
+ return invalidResult("outcome_shape", "decision outcome.status must be resolved_no_change, blocked, or needs_clarification");
865
870
  }
866
871
  return {
867
872
  ok: true,
@@ -884,13 +889,15 @@ function parseResult(value, runId, workItem, root) {
884
889
  "work_item_id",
885
890
  "worktree_evidence",
886
891
  ]) ||
887
- value.contract_version !== RESULT_CONTRACT_VERSION ||
888
- !resultIdentityIsBound(value, {
889
- runId,
890
- workItemId: workItem.id,
891
- promptSha256: workItem.prompt.sha256,
892
- })) {
893
- return invalidResult("result must match the exact current contract, run, work-item, and prompt binding");
892
+ value.contract_version !== RESULT_CONTRACT_VERSION) {
893
+ return invalidResult("result_envelope", "result must match the exact current contract, run, work-item, and prompt binding");
894
+ }
895
+ if (!resultIdentityIsBound(value, {
896
+ runId,
897
+ workItemId: workItem.id,
898
+ promptSha256: workItem.prompt.sha256,
899
+ })) {
900
+ return invalidResult("identity_binding", "result must match the exact current contract, run, work-item, and prompt binding");
894
901
  }
895
902
  const changedFiles = stringArray(value.changed_files);
896
903
  if (!changedFiles ||
@@ -898,25 +905,25 @@ function parseResult(value, runId, workItem, root) {
898
905
  new Set(changedFiles).size !== changedFiles.length ||
899
906
  !sameStrings([...changedFiles].sort(compareCodeUnits), changedFiles) ||
900
907
  changedFiles.some((path) => !pathIsAllowedByWriteScope(root, path, workItem.allowed_files))) {
901
- return invalidResult("changed_files must be non-empty, sorted, unique, normalized paths within allowed_files");
908
+ return invalidResult("write_scope", "changed_files must be non-empty, sorted, unique, normalized paths within allowed_files");
902
909
  }
903
910
  if (!isRecord(value.commit_evidence) ||
904
911
  !hasExactKeys(value.commit_evidence, ["after", "before"]) ||
905
912
  value.commit_evidence.before !== workItem.baseline_commit ||
906
913
  !isCommit(value.commit_evidence.after) ||
907
914
  value.commit_evidence.after === value.commit_evidence.before) {
908
- return invalidResult("commit_evidence must bind the workload baseline to a distinct full commit id");
915
+ return invalidResult("commit_evidence", "commit_evidence must bind the workload baseline to a distinct full commit id");
909
916
  }
910
917
  if (!Array.isArray(value.test_evidence) ||
911
918
  value.test_evidence.length !== workItem.required_tests.length) {
912
- return invalidResult("test_evidence must contain exactly one entry for every required test");
919
+ return invalidResult("test_evidence", "test_evidence must contain exactly one entry for every required test");
913
920
  }
914
921
  for (const [index, evidence] of value.test_evidence.entries()) {
915
922
  if (!isRecord(evidence) ||
916
923
  !hasExactKeys(evidence, ["command", "status"]) ||
917
924
  evidence.command !== workItem.required_tests[index] ||
918
925
  evidence.status !== "passed") {
919
- return invalidResult(`test_evidence[${index}] must echo the bound command with status passed`);
926
+ return invalidResult("test_evidence", `test_evidence[${index}] must echo the bound command with status passed`);
920
927
  }
921
928
  }
922
929
  // The evidence-coverage floor: the cited obligation set must equal the
@@ -924,7 +931,7 @@ function parseResult(value, runId, workItem, root) {
924
931
  // whether a citation SUFFICES is the per-run conformance review's job.
925
932
  const obligationEvidence = value.obligation_evidence;
926
933
  if (!Array.isArray(obligationEvidence)) {
927
- return invalidResult("obligation_evidence must be an array with one entry per bound obligation");
934
+ return invalidResult("obligation_evidence", "obligation_evidence must be an array with one entry per bound obligation");
928
935
  }
929
936
  const citedIds = [];
930
937
  for (const [index, entry] of obligationEvidence.entries()) {
@@ -939,7 +946,7 @@ function parseResult(value, runId, workItem, root) {
939
946
  const named = isRecord(entry) && typeof entry.obligation_id === "string"
940
947
  ? ` (obligation_id: ${entry.obligation_id})`
941
948
  : "";
942
- return invalidResult(`obligation_evidence[${index}] must cite at least one non-empty evidence string for a named obligation${named}`);
949
+ return invalidResult("obligation_evidence", `obligation_evidence[${index}] must cite at least one non-empty evidence string for a named obligation${named}`);
943
950
  }
944
951
  citedIds.push(entry.obligation_id);
945
952
  }
@@ -948,24 +955,24 @@ function parseResult(value, runId, workItem, root) {
948
955
  const duplicates = [
949
956
  ...new Set(citedIds.filter((id, i) => citedIds.indexOf(id) !== i)),
950
957
  ];
951
- return invalidResult(`obligation_evidence cites an obligation more than once: ${duplicates.join(", ")}`);
958
+ return invalidResult("obligation_evidence", `obligation_evidence cites an obligation more than once: ${duplicates.join(", ")}`);
952
959
  }
953
960
  const uncovered = workItem.obligation_ids.filter((id) => !cited.has(id));
954
961
  if (uncovered.length > 0) {
955
- return invalidResult(`obligation_evidence must cover every prompt-bound obligation; uncovered: ${uncovered.join(", ")}`);
962
+ return invalidResult("obligation_evidence", `obligation_evidence must cover every prompt-bound obligation; uncovered: ${uncovered.join(", ")}`);
956
963
  }
957
964
  const unknown = citedIds.filter((id) => !workItem.obligation_ids.includes(id));
958
965
  if (unknown.length > 0) {
959
- return invalidResult(`obligation_evidence cites obligations the work item does not bind: ${unknown.join(", ")}`);
966
+ return invalidResult("obligation_evidence", `obligation_evidence cites obligations the work item does not bind: ${unknown.join(", ")}`);
960
967
  }
961
968
  if (!isRecord(value.worktree_evidence) ||
962
969
  !hasExactKeys(value.worktree_evidence, ["baseline_commit", "changed_files"]) ||
963
970
  value.worktree_evidence.baseline_commit !== workItem.baseline_commit) {
964
- return invalidResult("worktree_evidence must bind the workload baseline and changed-file list");
971
+ return invalidResult("worktree_evidence", "worktree_evidence must bind the workload baseline and changed-file list");
965
972
  }
966
973
  const worktreeFiles = stringArray(value.worktree_evidence.changed_files);
967
974
  if (!worktreeFiles || !sameStrings(worktreeFiles, changedFiles)) {
968
- return invalidResult("worktree_evidence.changed_files must exactly equal changed_files");
975
+ return invalidResult("worktree_evidence", "worktree_evidence.changed_files must exactly equal changed_files");
969
976
  }
970
977
  if (!isRecord(value.acceptance) ||
971
978
  !hasExactKeys(value.acceptance, ["status"]) ||
@@ -973,7 +980,7 @@ function parseResult(value, runId, workItem, root) {
973
980
  !isRecord(value.merge) ||
974
981
  !hasExactKeys(value.merge, ["status"]) ||
975
982
  value.merge.status !== "merged") {
976
- return invalidResult("acceptance and merge must both attest a completed landing");
983
+ return invalidResult("landing_attestation", "acceptance and merge must both attest a completed landing");
977
984
  }
978
985
  return {
979
986
  ok: true,
@@ -1136,6 +1143,7 @@ async function corroborateNoChangeClaim(params) {
1136
1143
  return {
1137
1144
  ok: false,
1138
1145
  code: "commit_missing",
1146
+ check: "no_change_corroboration",
1139
1147
  message: "resolved_no_change cannot be corroborated: baseline_commit does not resolve to a real commit",
1140
1148
  };
1141
1149
  }
@@ -1144,6 +1152,7 @@ async function corroborateNoChangeClaim(params) {
1144
1152
  return {
1145
1153
  ok: false,
1146
1154
  code: "commit_missing",
1155
+ check: "no_change_corroboration",
1147
1156
  message: "resolved_no_change cannot be corroborated: git could not enumerate the changes since the workload baseline",
1148
1157
  };
1149
1158
  }
@@ -1154,6 +1163,7 @@ async function corroborateNoChangeClaim(params) {
1154
1163
  return {
1155
1164
  ok: false,
1156
1165
  code: "changed_files_mismatch",
1166
+ check: "no_change_corroboration",
1157
1167
  message: "resolved_no_change is contradicted by the tree — these files changed since the " +
1158
1168
  `workload baseline: ${violating.join(", ")}` +
1159
1169
  (outOfScope.length > 0
@@ -1359,6 +1369,7 @@ function requiredTestIssue(workItem, failures) {
1359
1369
  : failures.some((failure) => failure.outcome === "output_overflow")
1360
1370
  ? "required_test_output_overflow"
1361
1371
  : "required_test_failed",
1372
+ check: "test_evidence",
1362
1373
  work_item_id: workItem.id,
1363
1374
  result_path: workItem.result_path,
1364
1375
  message: `mechanical required-test rerun failed: ${failures
@@ -1418,6 +1429,7 @@ async function corroborateHostResult(params) {
1418
1429
  return {
1419
1430
  ok: false,
1420
1431
  code: "commit_missing",
1432
+ check: "commit_evidence",
1421
1433
  message: "baseline_commit and commit_evidence.after must both resolve to real commits",
1422
1434
  };
1423
1435
  }
@@ -1426,6 +1438,7 @@ async function corroborateHostResult(params) {
1426
1438
  return {
1427
1439
  ok: false,
1428
1440
  code: "baseline_not_ancestor",
1441
+ check: "commit_evidence",
1429
1442
  message: "the trusted workload baseline is not an ancestor of the claimed landed commit",
1430
1443
  };
1431
1444
  }
@@ -1442,6 +1455,7 @@ async function corroborateHostResult(params) {
1442
1455
  return {
1443
1456
  ok: false,
1444
1457
  code: "baseline_not_ancestor",
1458
+ check: "commit_evidence",
1445
1459
  message: "the trusted workload baseline is not an ancestor of the claimed landed commit, " +
1446
1460
  "and the baseline is NOT orphaned (a ref still contains it, or it is reachable " +
1447
1461
  "from HEAD), so the stale-worker protection stands and recovery cannot waive it",
@@ -1453,6 +1467,7 @@ async function corroborateHostResult(params) {
1453
1467
  return {
1454
1468
  ok: false,
1455
1469
  code: "commit_not_landed",
1470
+ check: "commit_evidence",
1456
1471
  message: "commit_evidence.after is not reachable from the repository HEAD",
1457
1472
  };
1458
1473
  }
@@ -1461,6 +1476,7 @@ async function corroborateHostResult(params) {
1461
1476
  return {
1462
1477
  ok: false,
1463
1478
  code: "changed_files_mismatch",
1479
+ check: "write_scope",
1464
1480
  message: "the landed commit's mechanically derived changed files do not exactly match changed_files",
1465
1481
  };
1466
1482
  }
@@ -1468,6 +1484,7 @@ async function corroborateHostResult(params) {
1468
1484
  return {
1469
1485
  ok: false,
1470
1486
  code: "changed_files_mismatch",
1487
+ check: "write_scope",
1471
1488
  message: "the landed commit changed a file outside the prompt-bound allowed_files",
1472
1489
  };
1473
1490
  }
@@ -1477,13 +1494,14 @@ async function corroborateHostResult(params) {
1477
1494
  return {
1478
1495
  ok: false,
1479
1496
  code: "run_start_dirty_overlap",
1497
+ check: "worktree_evidence",
1480
1498
  message: `landed files overlap pre-existing run-start dirt: ${dirtyOverlap.join(", ")}`,
1481
1499
  };
1482
1500
  }
1483
1501
  const failedTests = await rerunRequiredTests(root, workItem.required_tests, verdicts);
1484
1502
  if (failedTests.length > 0) {
1485
1503
  const issue = requiredTestIssue(workItem, failedTests);
1486
- return { ok: false, code: issue.code, message: issue.message };
1504
+ return { ok: false, code: issue.code, check: "test_evidence", message: issue.message };
1487
1505
  }
1488
1506
  return { ok: true, changedFiles: actualFiles, usedRecovery };
1489
1507
  }
@@ -1609,6 +1627,55 @@ export async function ingestRemediationHostResults(params) {
1609
1627
  return "unsupported_retired_state";
1610
1628
  const paths = resolveBoundaryPaths(params);
1611
1629
  const nextState = structuredClone(state);
1630
+ const validated = await validateHostResultBundle({
1631
+ state,
1632
+ nextState,
1633
+ paths,
1634
+ runId: params.runId,
1635
+ recovery: params.recovery,
1636
+ });
1637
+ if (validated.kind === "summary") {
1638
+ const issues = await recordAndEnrichHostIssues(params.artifactsDir, params.runId, validated.summary.issues, validated.summary.completed_work_item_ids);
1639
+ return { ...validated.summary, issues };
1640
+ }
1641
+ const acc = {
1642
+ issues: validated.issues,
1643
+ completed: [],
1644
+ resultIds: new Set(),
1645
+ landedFiles: new Set(nextState.applied_edit_surface ?? []),
1646
+ settledFindingIds: new Set(),
1647
+ recordedRecoveryMarks: null,
1648
+ };
1649
+ const verdicts = await executeHostVerificationReruns(validated.ctx, acc);
1650
+ const issues = await recordAndEnrichHostIssues(params.artifactsDir, params.runId, acc.issues, acc.completed);
1651
+ return commitRemediationStateUpdates(validated.ctx, acc, verdicts, issues);
1652
+ }
1653
+ /** Record raw observations first, then decorate only the returned diagnostics. */
1654
+ async function recordAndEnrichHostIssues(artifactsDir, runId, issues, acceptedIds) {
1655
+ const refusals = await readTrailingSubmissionRefusals(artifactsDir, issues
1656
+ .map((issue) => issue.work_item_id ?? issue.submission_id)
1657
+ .filter((id) => id !== undefined), { runId });
1658
+ await recordHostResultOutcomes(artifactsDir, runId, {
1659
+ // This refusal already reports that the ledger write failed. Retrying
1660
+ // that same item would replace its diagnostic with an ingest exception;
1661
+ // other items retain normal recording and error propagation.
1662
+ issues: issues.filter((issue) => issue.code !== "recovery_unrecorded"),
1663
+ acceptedIds,
1664
+ }, { scopeToRunId: runId });
1665
+ return enrichMissingSubmissionIssues(issues, refusals, "submission_rejected");
1666
+ }
1667
+ /**
1668
+ * Phase 1 — the whole-bundle gates, up to the schedulable frontier.
1669
+ *
1670
+ * Returns either a populated context or ONE OF THREE finished summaries. The
1671
+ * three do not agree on `pending_work_item_ids` — the read failure and the
1672
+ * canonical parse failure return the binding's own item ids, the trusted-binding
1673
+ * refusal returns the empty list — so folding any two together is a silent
1674
+ * behavior change. Each is pinned by a test that asserts the pending list, not
1675
+ * just the code.
1676
+ */
1677
+ async function validateHostResultBundle(input) {
1678
+ const { state, nextState, paths } = input;
1612
1679
  const issues = [];
1613
1680
  const workloadRead = await readSubmissionDocument(paths.workloadPath);
1614
1681
  if (workloadRead.kind !== "value") {
@@ -1617,18 +1684,22 @@ export async function ingestRemediationHostResults(params) {
1617
1684
  code: workloadRead.kind === "missing"
1618
1685
  ? "workload_missing"
1619
1686
  : "workload_invalid",
1687
+ check: "workload_binding",
1620
1688
  message: workloadRead.kind === "missing"
1621
1689
  ? "the persisted trusted handoff has no workload file"
1622
1690
  : `the workload file is not valid JSON: ${workloadRead.detail}`,
1623
1691
  });
1624
1692
  }
1625
1693
  return {
1626
- accepted_count: 0,
1627
- completed_work_item_ids: [],
1628
- pending_work_item_ids: state.host_handoff?.work_item_ids ?? [],
1629
- issues,
1630
- state_changed: false,
1631
- state: nextState,
1694
+ kind: "summary",
1695
+ summary: {
1696
+ accepted_count: 0,
1697
+ completed_work_item_ids: [],
1698
+ pending_work_item_ids: state.host_handoff?.work_item_ids ?? [],
1699
+ issues,
1700
+ state_changed: false,
1701
+ state: nextState,
1702
+ },
1632
1703
  };
1633
1704
  }
1634
1705
  // Producer-side plan defects are reported BEFORE the workload is parsed: a
@@ -1648,62 +1719,85 @@ export async function ingestRemediationHostResults(params) {
1648
1719
  if ((await isGitRepo(paths.root)) && !state.host_handoff) {
1649
1720
  issues.push({
1650
1721
  code: "trusted_binding_missing",
1722
+ check: "workload_binding",
1651
1723
  message: "a git-backed remediation workload requires the tool-owned host_handoff state binding",
1652
1724
  });
1653
1725
  return {
1654
- accepted_count: 0,
1655
- completed_work_item_ids: [],
1656
- pending_work_item_ids: [],
1657
- issues,
1658
- state_changed: false,
1659
- state: nextState,
1726
+ kind: "summary",
1727
+ summary: {
1728
+ accepted_count: 0,
1729
+ completed_work_item_ids: [],
1730
+ pending_work_item_ids: [],
1731
+ issues,
1732
+ state_changed: false,
1733
+ state: nextState,
1734
+ },
1660
1735
  };
1661
1736
  }
1662
- const workload = parseWorkload(workloadRead.value, paths, params.runId, state);
1737
+ const workload = parseWorkload(workloadRead.value, paths, input.runId, state);
1663
1738
  if (!workload) {
1664
1739
  // Accumulated, not replaced: when a block-contract defect is WHY the
1665
1740
  // canonical re-derivation failed, the block-attributed issue is the only
1666
1741
  // thing that names the cause.
1667
1742
  issues.push({
1668
1743
  code: "workload_invalid",
1744
+ check: "workload_binding",
1669
1745
  message: "the workload does not match its canonical state shape and persisted digest binding",
1670
1746
  });
1671
1747
  return {
1672
- accepted_count: 0,
1673
- completed_work_item_ids: [],
1674
- pending_work_item_ids: state.host_handoff?.work_item_ids ?? [],
1675
- issues,
1676
- state_changed: false,
1677
- state: nextState,
1748
+ kind: "summary",
1749
+ summary: {
1750
+ accepted_count: 0,
1751
+ completed_work_item_ids: [],
1752
+ pending_work_item_ids: state.host_handoff?.work_item_ids ?? [],
1753
+ issues,
1754
+ state_changed: false,
1755
+ state: nextState,
1756
+ },
1678
1757
  };
1679
1758
  }
1680
- const effectiveWorkload = effectiveBoundWorkload(state, workload);
1681
- const eligibleIds = new Set((hostDependencyLevels(state)[0] ?? []).map((block) => block.block_id));
1682
- const resultIds = new Set();
1683
- const completed = [];
1684
- // Recovery-only answer table; the normal lane gets null and spawns exactly as
1685
- // it always has. See RemediationRequiredTestVerdicts.
1686
- const requiredTestVerdicts = params.recovery?.requiredTestVerdicts ?? null;
1687
- // Lazily loaded on the first recovery-marked acceptance: the recovery marks
1688
- // the ledger ALREADY carries. A crash between the append and the state write
1689
- // leaves a mark whose item is still pending, and the natural response is to
1690
- // re-run the verb which must converge, not accumulate a second record of
1691
- // the same acceptance.
1692
- let recordedRecoveryMarks = null;
1693
- const landedFiles = new Set(nextState.applied_edit_surface ?? []);
1694
- // Can this ingest be corroborated against ground truth AT ALL? A git root
1695
- // supplies the tree; a persisted `host_handoff` supplies the trusted binding.
1696
- // With NEITHER there is nothing to check a host's claim against, and the
1697
- // remaining evidence is the host's own attestation — which is exactly the
1698
- // claim under test. That branch is REFUSED for both result and decision
1699
- // documents rather than admitted on the attestation alone.
1700
- const canCorroborate = state.host_handoff !== undefined || (await isGitRepo(paths.root));
1701
- for (const workItem of effectiveWorkload.work_items) {
1702
- const pendingItems = workItem.finding_ids.filter((findingId) => nextState.items[findingId]?.status === "pending");
1759
+ return {
1760
+ kind: "context",
1761
+ issues,
1762
+ ctx: {
1763
+ paths,
1764
+ runId: input.runId,
1765
+ recovery: input.recovery !== undefined,
1766
+ state,
1767
+ nextState,
1768
+ effectiveWorkload: effectiveBoundWorkload(state, workload),
1769
+ eligibleIds: new Set((hostDependencyLevels(state)[0] ?? []).map((block) => block.block_id)),
1770
+ // Can this ingest be corroborated against ground truth AT ALL? A git root
1771
+ // supplies the tree; a persisted `host_handoff` supplies the trusted
1772
+ // binding. With NEITHER there is nothing to check a host's claim against,
1773
+ // and the remaining evidence is the host's own attestation which is
1774
+ // exactly the claim under test. That branch is REFUSED for both result
1775
+ // and decision documents rather than admitted on the attestation alone.
1776
+ canCorroborate: state.host_handoff !== undefined || (await isGitRepo(paths.root)),
1777
+ // Recovery-only answer table; the normal lane gets null and spawns
1778
+ // exactly as it always has. See RemediationRequiredTestVerdicts.
1779
+ requiredTestVerdicts: input.recovery?.requiredTestVerdicts ?? null,
1780
+ },
1781
+ };
1782
+ }
1783
+ /**
1784
+ * Phase 2 — per-item verification. Writes NOTHING to `ctx.nextState`.
1785
+ *
1786
+ * Iterates the effective workload in order and emits one verdict per ACCEPTED
1787
+ * item; a refusal pushes its classified issue and continues. The ledger mark
1788
+ * for a recovery acceptance still goes down before that item's verdict is
1789
+ * emitted, so no acceptance can outrun its record.
1790
+ */
1791
+ async function executeHostVerificationReruns(ctx, acc) {
1792
+ const { paths, nextState, state } = ctx;
1793
+ const verdicts = [];
1794
+ for (const workItem of ctx.effectiveWorkload.work_items) {
1795
+ const pendingItems = workItem.finding_ids.filter((findingId) => nextState.items[findingId]?.status === "pending" &&
1796
+ !acc.settledFindingIds.has(findingId));
1703
1797
  if (pendingItems.length === 0)
1704
1798
  continue;
1705
- if (!eligibleIds.has(workItem.id)) {
1706
- issues.push({
1799
+ if (!ctx.eligibleIds.has(workItem.id)) {
1800
+ acc.issues.push({
1707
1801
  code: "submission_contract_invalid",
1708
1802
  work_item_id: workItem.id,
1709
1803
  result_path: workItem.result_path,
@@ -1717,29 +1811,30 @@ export async function ingestRemediationHostResults(params) {
1717
1811
  workItemId: workItem.id,
1718
1812
  resultPath: workItem.result_path,
1719
1813
  parse: (value) => {
1720
- const result = parseResult(value, params.runId, workItem, paths.root);
1814
+ const result = parseResult(value, ctx.runId, workItem, paths.root);
1721
1815
  return result.ok
1722
1816
  ? { ok: true, parsed: result }
1723
- : { ok: false, detail: result.reason };
1817
+ : { ok: false, check: result.check, detail: result.reason };
1724
1818
  },
1725
1819
  resultId: (result) => result.result.result_id,
1726
- seen: (resultId) => resultIds.has(resultId),
1820
+ seen: (resultId) => acc.resultIds.has(resultId),
1727
1821
  messages: remediationScanMessages,
1728
1822
  });
1729
1823
  if (!scan.ok) {
1730
- issues.push(scan.issue);
1824
+ acc.issues.push(scan.issue);
1731
1825
  continue;
1732
1826
  }
1733
1827
  const parsed = scan.parsed;
1734
1828
  const resultId = parsed.result.result_id;
1735
- resultIds.add(resultId);
1829
+ acc.resultIds.add(resultId);
1736
1830
  if (parsed.kind === "decision") {
1737
1831
  const result = parsed.result;
1738
1832
  const outcome = result.outcome;
1739
1833
  if (outcome.status === "resolved_no_change") {
1740
- if (!canCorroborate) {
1741
- issues.push({
1834
+ if (!ctx.canCorroborate) {
1835
+ acc.issues.push({
1742
1836
  code: "trusted_binding_missing",
1837
+ check: "no_change_corroboration",
1743
1838
  work_item_id: workItem.id,
1744
1839
  result_path: workItem.result_path,
1745
1840
  message: "resolved_no_change needs a git root or a persisted host_handoff binding to corroborate the write scope against; attestation-only acceptance is refused",
@@ -1754,63 +1849,42 @@ export async function ingestRemediationHostResults(params) {
1754
1849
  // `applied_edit_surface` and grows as this same ingest accepts).
1755
1850
  excusedPaths: new Set([
1756
1851
  ...(state.run_start_dirty ?? []).map(normalizeRepoPath),
1757
- ...[...landedFiles].map(normalizeRepoPath),
1852
+ ...[...acc.landedFiles].map(normalizeRepoPath),
1758
1853
  ]),
1759
1854
  });
1760
1855
  if (!noChange.ok) {
1761
- issues.push({
1856
+ acc.issues.push({
1762
1857
  code: noChange.code,
1858
+ check: noChange.check,
1763
1859
  work_item_id: workItem.id,
1764
1860
  result_path: workItem.result_path,
1765
1861
  message: noChange.message,
1766
1862
  });
1767
1863
  continue;
1768
1864
  }
1769
- const failedTests = await rerunRequiredTests(paths.root, workItem.required_tests, requiredTestVerdicts);
1865
+ const failedTests = await rerunRequiredTests(paths.root, workItem.required_tests, ctx.requiredTestVerdicts);
1770
1866
  if (failedTests.length > 0) {
1771
- issues.push(requiredTestIssue(workItem, failedTests));
1867
+ acc.issues.push(requiredTestIssue(workItem, failedTests));
1772
1868
  continue;
1773
1869
  }
1774
1870
  }
1775
- const now = new Date().toISOString();
1776
- for (const findingId of pendingItems) {
1777
- const item = nextState.items[findingId];
1778
- item.started_at ??= now;
1779
- if (outcome.status === "resolved_no_change") {
1780
- item.status = "resolved_no_change";
1781
- item.completed_at = now;
1782
- item.host_result_evidence = [...outcome.evidence];
1783
- delete item.failure_reason;
1784
- }
1785
- else if (outcome.status === "blocked") {
1786
- item.status = "blocked";
1787
- item.completed_at = now;
1788
- item.failure_reason = outcome.failure_reason;
1789
- }
1790
- else {
1791
- item.status = "needs_clarification";
1792
- delete item.completed_at;
1793
- item.failure_reason = outcome.question;
1794
- const clarifications = nextState.clarifications ?? [];
1795
- if (!clarifications.some((entry) => entry.finding_id === findingId)) {
1796
- clarifications.push({
1797
- finding_id: findingId,
1798
- category: isClarificationCategory(outcome.category)
1799
- ? outcome.category
1800
- : "scope_of_fix",
1801
- description: outcome.question,
1802
- });
1803
- }
1804
- nextState.clarifications = clarifications;
1805
- }
1806
- }
1807
- completed.push(workItem.id);
1871
+ for (const findingId of pendingItems)
1872
+ acc.settledFindingIds.add(findingId);
1873
+ verdicts.push({
1874
+ kind: "decision",
1875
+ workItem,
1876
+ pendingItems,
1877
+ at: new Date().toISOString(),
1878
+ outcome,
1879
+ });
1880
+ acc.completed.push(workItem.id);
1808
1881
  continue;
1809
1882
  }
1810
1883
  const result = parsed.result;
1811
- if (!canCorroborate) {
1812
- issues.push({
1884
+ if (!ctx.canCorroborate) {
1885
+ acc.issues.push({
1813
1886
  code: "trusted_binding_missing",
1887
+ check: "workload_binding",
1814
1888
  work_item_id: workItem.id,
1815
1889
  result_path: workItem.result_path,
1816
1890
  message: "a landed result needs a git root or a persisted host_handoff binding to corroborate the write scope against; attestation-only acceptance is refused",
@@ -1822,12 +1896,13 @@ export async function ingestRemediationHostResults(params) {
1822
1896
  state,
1823
1897
  workItem,
1824
1898
  result,
1825
- verdicts: requiredTestVerdicts,
1826
- recovery: params.recovery !== undefined,
1899
+ verdicts: ctx.requiredTestVerdicts,
1900
+ recovery: ctx.recovery,
1827
1901
  });
1828
1902
  if (!corroborated.ok) {
1829
- issues.push({
1903
+ acc.issues.push({
1830
1904
  code: corroborated.code,
1905
+ check: corroborated.check,
1831
1906
  work_item_id: workItem.id,
1832
1907
  result_path: workItem.result_path,
1833
1908
  message: corroborated.message,
@@ -1841,7 +1916,7 @@ export async function ingestRemediationHostResults(params) {
1841
1916
  // must never read as one that never drifted. The refusal is per item:
1842
1917
  // an unwritable ledger is not a reason to discard the whole ingest.
1843
1918
  try {
1844
- recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
1919
+ acc.recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
1845
1920
  // The mark's identity is (run, item, LANDED COMMIT), not just
1846
1921
  // (run, item): an item re-opened and later re-accepted from a
1847
1922
  // DIFFERENT landing is a different relaxed acceptance and earns its
@@ -1850,13 +1925,13 @@ export async function ingestRemediationHostResults(params) {
1850
1925
  // contract carries no commit field, and a 40-hex sha this writer
1851
1926
  // itself emitted is an unambiguous token to match on.
1852
1927
  const landedCommit = result.commit_evidence.after;
1853
- const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
1928
+ const alreadyMarked = acc.recordedRecoveryMarks.some((event) => event.run_id === ctx.runId &&
1854
1929
  event.submission_id === workItem.id &&
1855
1930
  (event.message ?? "").includes(landedCommit));
1856
1931
  if (!alreadyMarked) {
1857
1932
  const event = {
1858
1933
  contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
1859
- run_id: params.runId,
1934
+ run_id: ctx.runId,
1860
1935
  submission_id: workItem.id,
1861
1936
  lane: workItem.id,
1862
1937
  kind: "accepted_via_recovery",
@@ -1869,11 +1944,11 @@ export async function ingestRemediationHostResults(params) {
1869
1944
  recorded_at: new Date().toISOString(),
1870
1945
  };
1871
1946
  await appendSubmissionEvent(paths.artifactsDir, event);
1872
- recordedRecoveryMarks.push(event);
1947
+ acc.recordedRecoveryMarks.push(event);
1873
1948
  }
1874
1949
  }
1875
1950
  catch (error) {
1876
- issues.push({
1951
+ acc.issues.push({
1877
1952
  code: "recovery_unrecorded",
1878
1953
  work_item_id: workItem.id,
1879
1954
  result_path: workItem.result_path,
@@ -1883,35 +1958,91 @@ export async function ingestRemediationHostResults(params) {
1883
1958
  continue;
1884
1959
  }
1885
1960
  }
1886
- const acceptedFiles = corroborated.changedFiles;
1887
- const completedAt = new Date().toISOString();
1888
- for (const findingId of pendingItems) {
1961
+ for (const findingId of pendingItems)
1962
+ acc.settledFindingIds.add(findingId);
1963
+ verdicts.push({
1964
+ kind: "landed",
1965
+ workItem,
1966
+ pendingItems,
1967
+ at: new Date().toISOString(),
1968
+ });
1969
+ for (const changedFile of corroborated.changedFiles) {
1970
+ acc.landedFiles.add(changedFile);
1971
+ }
1972
+ acc.completed.push(workItem.id);
1973
+ }
1974
+ return verdicts;
1975
+ }
1976
+ /**
1977
+ * Phase 3 — apply the accepted verdicts to the clone and assemble the summary.
1978
+ *
1979
+ * Infallible given the verdicts: every refusal was already classified in
1980
+ * phase 2, so nothing here can reject. Verdicts apply in workload order, which
1981
+ * is the order they were emitted, so a finding two items share is written the
1982
+ * same way it was before the phases were separated.
1983
+ */
1984
+ function commitRemediationStateUpdates(ctx, acc, verdicts, issues) {
1985
+ const { nextState } = ctx;
1986
+ for (const verdict of verdicts) {
1987
+ if (verdict.kind === "decision") {
1988
+ const outcome = verdict.outcome;
1989
+ for (const findingId of verdict.pendingItems) {
1990
+ const item = nextState.items[findingId];
1991
+ item.started_at ??= verdict.at;
1992
+ if (outcome.status === "resolved_no_change") {
1993
+ item.status = "resolved_no_change";
1994
+ item.completed_at = verdict.at;
1995
+ item.host_result_evidence = [...outcome.evidence];
1996
+ delete item.failure_reason;
1997
+ }
1998
+ else if (outcome.status === "blocked") {
1999
+ item.status = "blocked";
2000
+ item.completed_at = verdict.at;
2001
+ item.failure_reason = outcome.failure_reason;
2002
+ }
2003
+ else {
2004
+ item.status = "needs_clarification";
2005
+ delete item.completed_at;
2006
+ item.failure_reason = outcome.question;
2007
+ const clarifications = nextState.clarifications ?? [];
2008
+ if (!clarifications.some((entry) => entry.finding_id === findingId)) {
2009
+ clarifications.push({
2010
+ finding_id: findingId,
2011
+ category: isClarificationCategory(outcome.category)
2012
+ ? outcome.category
2013
+ : "scope_of_fix",
2014
+ description: outcome.question,
2015
+ });
2016
+ }
2017
+ nextState.clarifications = clarifications;
2018
+ }
2019
+ }
2020
+ continue;
2021
+ }
2022
+ for (const findingId of verdict.pendingItems) {
1889
2023
  const item = nextState.items[findingId];
1890
2024
  item.status = "resolved";
1891
- item.started_at ??= completedAt;
1892
- item.completed_at = completedAt;
2025
+ item.started_at ??= verdict.at;
2026
+ item.completed_at = verdict.at;
1893
2027
  delete item.failure_reason;
1894
2028
  delete item.host_result_evidence;
1895
2029
  }
1896
- for (const changedFile of acceptedFiles)
1897
- landedFiles.add(changedFile);
1898
- completed.push(workItem.id);
1899
2030
  }
1900
- if (completed.length > 0) {
1901
- nextState.applied_edit_surface = [...landedFiles].sort(compareCodeUnits);
2031
+ if (acc.completed.length > 0) {
2032
+ nextState.applied_edit_surface = [...acc.landedFiles].sort(compareCodeUnits);
1902
2033
  }
1903
- const pendingWorkItemIds = effectiveWorkload.work_items
2034
+ const pendingWorkItemIds = ctx.effectiveWorkload.work_items
1904
2035
  .filter((workItem) => workItem.finding_ids.some((findingId) => nextState.items[findingId]?.status === "pending"))
1905
2036
  .map((workItem) => workItem.id);
1906
- let stateChanged = completed.length > 0;
2037
+ let stateChanged = acc.completed.length > 0;
1907
2038
  if (nextState.host_handoff && pendingWorkItemIds.length === 0) {
1908
2039
  migrateLegacyDirectoryScopesAfterFinalDrain(nextState);
1909
2040
  delete nextState.host_handoff;
1910
2041
  stateChanged = true;
1911
2042
  }
1912
2043
  return {
1913
- accepted_count: completed.length,
1914
- completed_work_item_ids: completed,
2044
+ accepted_count: acc.completed.length,
2045
+ completed_work_item_ids: acc.completed,
1915
2046
  pending_work_item_ids: pendingWorkItemIds,
1916
2047
  issues,
1917
2048
  state_changed: stateChanged,