audit-tools 0.45.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +51 -18
  2. package/dispatch/merge-results.mjs +3 -3
  3. package/dispatch/validate-result.mjs +3 -3
  4. package/dist/audit/cli/charterExtractionPrompt.d.ts.map +1 -1
  5. package/dist/audit/cli/charterExtractionPrompt.js +11 -1
  6. package/dist/audit/cli/charterExtractionPrompt.js.map +1 -1
  7. package/dist/audit/cli/cleanupCommand.d.ts +25 -0
  8. package/dist/audit/cli/cleanupCommand.d.ts.map +1 -1
  9. package/dist/audit/cli/cleanupCommand.js +76 -1
  10. package/dist/audit/cli/cleanupCommand.js.map +1 -1
  11. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  12. package/dist/audit/cli/dispatch/hostHandoff.js +52 -76
  13. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  14. package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
  15. package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
  16. package/dist/audit/cli/fanoutLanes.js +6 -1
  17. package/dist/audit/cli/fanoutLanes.js.map +1 -1
  18. package/dist/audit/cli/laneSubmissions.d.ts +13 -0
  19. package/dist/audit/cli/laneSubmissions.d.ts.map +1 -1
  20. package/dist/audit/cli/laneSubmissions.js +38 -27
  21. package/dist/audit/cli/laneSubmissions.js.map +1 -1
  22. package/dist/audit/cli/nextStepCommand.d.ts +12 -0
  23. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  24. package/dist/audit/cli/nextStepCommand.js +23 -12
  25. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  26. package/dist/audit/cli/nextStepHelpers.d.ts +3 -2
  27. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  28. package/dist/audit/cli/nextStepHelpers.js +65 -37
  29. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  30. package/dist/audit/cli/reviewRun.d.ts +1 -1
  31. package/dist/audit/cli/reviewRun.d.ts.map +1 -1
  32. package/dist/audit/cli/reviewRun.js +2 -9
  33. package/dist/audit/cli/reviewRun.js.map +1 -1
  34. package/dist/audit/cli/sampleRunCommand.d.ts +215 -0
  35. package/dist/audit/cli/sampleRunCommand.d.ts.map +1 -1
  36. package/dist/audit/cli/sampleRunCommand.js +60 -3
  37. package/dist/audit/cli/sampleRunCommand.js.map +1 -1
  38. package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
  39. package/dist/audit/cli/semanticReviewStep.js +9 -1
  40. package/dist/audit/cli/semanticReviewStep.js.map +1 -1
  41. package/dist/audit/cli/steps.d.ts +6 -6
  42. package/dist/audit/contracts/workerSchemas.d.ts +36 -26
  43. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  44. package/dist/audit/contracts/workerSchemas.js +11 -1
  45. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  46. package/dist/audit/contracts/wrapperResponse.d.ts +52 -28
  47. package/dist/audit/contracts/wrapperResponse.d.ts.map +1 -1
  48. package/dist/audit/contracts/wrapperResponse.js +8 -5
  49. package/dist/audit/contracts/wrapperResponse.js.map +1 -1
  50. package/dist/audit/decompose/sources.d.ts.map +1 -1
  51. package/dist/audit/decompose/sources.js +21 -5
  52. package/dist/audit/decompose/sources.js.map +1 -1
  53. package/dist/audit/extractors/analyzers/registry.d.ts +8 -5
  54. package/dist/audit/extractors/analyzers/registry.d.ts.map +1 -1
  55. package/dist/audit/extractors/analyzers/registry.js +28 -7
  56. package/dist/audit/extractors/analyzers/registry.js.map +1 -1
  57. package/dist/audit/extractors/designAssessment.js +1 -1
  58. package/dist/audit/extractors/gitHistory.d.ts +1 -1
  59. package/dist/audit/extractors/gitHistory.js +1 -1
  60. package/dist/audit/extractors/graph.d.ts +1 -1
  61. package/dist/audit/extractors/graph.js +1 -1
  62. package/dist/audit/extractors/risk.d.ts +1 -1
  63. package/dist/audit/extractors/risk.js +1 -1
  64. package/dist/audit/io/artifacts.d.ts.map +1 -1
  65. package/dist/audit/io/artifacts.js +5 -5
  66. package/dist/audit/io/artifacts.js.map +1 -1
  67. package/dist/audit/orchestrator/acquisitionExecutor.d.ts +12 -7
  68. package/dist/audit/orchestrator/acquisitionExecutor.d.ts.map +1 -1
  69. package/dist/audit/orchestrator/acquisitionExecutor.js +1 -1
  70. package/dist/audit/orchestrator/acquisitionExecutor.js.map +1 -1
  71. package/dist/audit/orchestrator/advance.d.ts +14 -10
  72. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  73. package/dist/audit/orchestrator/advance.js +49 -24
  74. package/dist/audit/orchestrator/advance.js.map +1 -1
  75. package/dist/audit/orchestrator/advanceTypes.d.ts +14 -0
  76. package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
  77. package/dist/audit/orchestrator/autoFixExecutor.d.ts +20 -1
  78. package/dist/audit/orchestrator/autoFixExecutor.d.ts.map +1 -1
  79. package/dist/audit/orchestrator/autoFixExecutor.js +77 -15
  80. package/dist/audit/orchestrator/autoFixExecutor.js.map +1 -1
  81. package/dist/audit/orchestrator/dependencyMap.js +1 -1
  82. package/dist/audit/orchestrator/dependencyMap.js.map +1 -1
  83. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  84. package/dist/audit/orchestrator/designReviewPrompt.js +7 -0
  85. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  86. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  87. package/dist/audit/orchestrator/executorRunners.js +18 -4
  88. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  89. package/dist/audit/orchestrator/executors.d.ts +54 -0
  90. package/dist/audit/orchestrator/executors.d.ts.map +1 -1
  91. package/dist/audit/orchestrator/executors.js +352 -0
  92. package/dist/audit/orchestrator/executors.js.map +1 -1
  93. package/dist/audit/orchestrator/hostInputPause.d.ts +28 -13
  94. package/dist/audit/orchestrator/hostInputPause.d.ts.map +1 -1
  95. package/dist/audit/orchestrator/hostInputPause.js +17 -11
  96. package/dist/audit/orchestrator/hostInputPause.js.map +1 -1
  97. package/dist/audit/orchestrator/intentInterpreter.d.ts +15 -18
  98. package/dist/audit/orchestrator/intentInterpreter.d.ts.map +1 -1
  99. package/dist/audit/orchestrator/intentInterpreter.js +12 -26
  100. package/dist/audit/orchestrator/intentInterpreter.js.map +1 -1
  101. package/dist/audit/orchestrator/localCommands.d.ts +24 -1
  102. package/dist/audit/orchestrator/localCommands.d.ts.map +1 -1
  103. package/dist/audit/orchestrator/localCommands.js +44 -9
  104. package/dist/audit/orchestrator/localCommands.js.map +1 -1
  105. package/dist/audit/orchestrator/nextStep.d.ts +13 -0
  106. package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
  107. package/dist/audit/orchestrator/nextStep.js +13 -0
  108. package/dist/audit/orchestrator/nextStep.js.map +1 -1
  109. package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
  110. package/dist/audit/orchestrator/runtimeCommand.d.ts +32 -1
  111. package/dist/audit/orchestrator/runtimeCommand.d.ts.map +1 -1
  112. package/dist/audit/orchestrator/runtimeCommand.js +90 -3
  113. package/dist/audit/orchestrator/runtimeCommand.js.map +1 -1
  114. package/dist/audit/orchestrator/structureExecutors.js +1 -1
  115. package/dist/audit/orchestrator/structureExecutors.js.map +1 -1
  116. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +11 -1
  117. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
  118. package/dist/audit/orchestrator/syntaxResolutionExecutor.js +25 -9
  119. package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
  120. package/dist/audit/supervisor/operatorHandoff.d.ts +0 -1
  121. package/dist/audit/supervisor/operatorHandoff.d.ts.map +1 -1
  122. package/dist/audit/supervisor/operatorHandoff.js +2 -1
  123. package/dist/audit/supervisor/operatorHandoff.js.map +1 -1
  124. package/dist/audit/types.d.ts +18 -18
  125. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  126. package/dist/remediate/contractPipeline/derive.js +1 -0
  127. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  128. package/dist/remediate/contractPipeline/semanticProjection.d.ts +5 -4
  129. package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +1 -1
  130. package/dist/remediate/contractPipeline/semanticProjection.js +34 -19
  131. package/dist/remediate/contractPipeline/semanticProjection.js.map +1 -1
  132. package/dist/remediate/index.d.ts.map +1 -1
  133. package/dist/remediate/index.js +3 -2
  134. package/dist/remediate/index.js.map +1 -1
  135. package/dist/remediate/intake.js +1 -1
  136. package/dist/remediate/intake.js.map +1 -1
  137. package/dist/remediate/phases/close.d.ts +45 -2
  138. package/dist/remediate/phases/close.d.ts.map +1 -1
  139. package/dist/remediate/phases/close.js +111 -43
  140. package/dist/remediate/phases/close.js.map +1 -1
  141. package/dist/remediate/phases/closeVerifyAnalyzerLeads.js +2 -2
  142. package/dist/remediate/phases/closeVerifyAnalyzerLeads.js.map +1 -1
  143. package/dist/remediate/phases/triage.d.ts.map +1 -1
  144. package/dist/remediate/phases/triage.js +61 -19
  145. package/dist/remediate/phases/triage.js.map +1 -1
  146. package/dist/remediate/review/autonomousGate.d.ts +3 -7
  147. package/dist/remediate/review/autonomousGate.d.ts.map +1 -1
  148. package/dist/remediate/review/autonomousGate.js +10 -24
  149. package/dist/remediate/review/autonomousGate.js.map +1 -1
  150. package/dist/remediate/riskSignal.d.ts +8 -20
  151. package/dist/remediate/riskSignal.d.ts.map +1 -1
  152. package/dist/remediate/riskSignal.js +21 -76
  153. package/dist/remediate/riskSignal.js.map +1 -1
  154. package/dist/remediate/state/accessMemory.d.ts +2 -3
  155. package/dist/remediate/state/accessMemory.d.ts.map +1 -1
  156. package/dist/remediate/state/accessMemory.js +3 -6
  157. package/dist/remediate/state/accessMemory.js.map +1 -1
  158. package/dist/remediate/state/store.d.ts +6 -7
  159. package/dist/remediate/state/store.d.ts.map +1 -1
  160. package/dist/remediate/state/store.js.map +1 -1
  161. package/dist/remediate/state/types.d.ts +2 -84
  162. package/dist/remediate/state/types.d.ts.map +1 -1
  163. package/dist/remediate/state/types.js +1 -39
  164. package/dist/remediate/state/types.js.map +1 -1
  165. package/dist/remediate/steps/contractPipeline.d.ts +13 -19
  166. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  167. package/dist/remediate/steps/contractPipeline.js +157 -66
  168. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  169. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +1 -1
  170. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  171. package/dist/remediate/steps/dispatch/hostHandoff.js +297 -141
  172. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  173. package/dist/remediate/steps/nextStep.d.ts +16 -7
  174. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  175. package/dist/remediate/steps/nextStep.js +207 -216
  176. package/dist/remediate/steps/nextStep.js.map +1 -1
  177. package/dist/remediate/steps/stepUtils.d.ts +10 -2
  178. package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
  179. package/dist/remediate/steps/stepUtils.js +13 -3
  180. package/dist/remediate/steps/stepUtils.js.map +1 -1
  181. package/dist/remediate/steps/types.d.ts +1 -1
  182. package/dist/remediate/steps/types.d.ts.map +1 -1
  183. package/dist/remediate/steps/types.js.map +1 -1
  184. package/dist/remediate/types/options.d.ts +1 -1
  185. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  186. package/dist/remediate/validation/artifacts.js +1 -16
  187. package/dist/remediate/validation/artifacts.js.map +1 -1
  188. package/dist/remediate/validation/remediationState.d.ts +0 -1
  189. package/dist/remediate/validation/remediationState.d.ts.map +1 -1
  190. package/dist/remediate/validation/remediationState.js +0 -51
  191. package/dist/remediate/validation/remediationState.js.map +1 -1
  192. package/dist/shared/analyzerPolicy.d.ts +18 -6
  193. package/dist/shared/analyzerPolicy.d.ts.map +1 -1
  194. package/dist/shared/analyzerPolicy.js +15 -3
  195. package/dist/shared/analyzerPolicy.js.map +1 -1
  196. package/dist/shared/analyzers/acquisitionEngine.d.ts +71 -32
  197. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  198. package/dist/shared/analyzers/acquisitionEngine.js +86 -28
  199. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  200. package/dist/shared/analyzers/binaryAcquisition.d.ts +8 -2
  201. package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
  202. package/dist/shared/analyzers/binaryAcquisition.js +4 -4
  203. package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
  204. package/dist/shared/constitutionalDocPaths.d.ts.map +1 -1
  205. package/dist/shared/constitutionalDocPaths.js +7 -0
  206. package/dist/shared/constitutionalDocPaths.js.map +1 -1
  207. package/dist/shared/engine/obligationEngine.d.ts +44 -3
  208. package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
  209. package/dist/shared/engine/obligationEngine.js +53 -7
  210. package/dist/shared/engine/obligationEngine.js.map +1 -1
  211. package/dist/shared/findings/dedupe.d.ts.map +1 -1
  212. package/dist/shared/findings/dedupe.js +8 -0
  213. package/dist/shared/findings/dedupe.js.map +1 -1
  214. package/dist/shared/friction/frictionRecord.d.ts +42 -2
  215. package/dist/shared/friction/frictionRecord.d.ts.map +1 -1
  216. package/dist/shared/friction/frictionRecord.js +111 -3
  217. package/dist/shared/friction/frictionRecord.js.map +1 -1
  218. package/dist/shared/friction/triage.d.ts +15 -0
  219. package/dist/shared/friction/triage.d.ts.map +1 -1
  220. package/dist/shared/friction/triage.js +33 -2
  221. package/dist/shared/friction/triage.js.map +1 -1
  222. package/dist/shared/index.d.ts +15 -9
  223. package/dist/shared/index.d.ts.map +1 -1
  224. package/dist/shared/index.js +15 -7
  225. package/dist/shared/index.js.map +1 -1
  226. package/dist/shared/intent/constraintClauses.d.ts +31 -0
  227. package/dist/shared/intent/constraintClauses.d.ts.map +1 -0
  228. package/dist/shared/intent/constraintClauses.js +37 -0
  229. package/dist/shared/intent/constraintClauses.js.map +1 -0
  230. package/dist/shared/io/fileLock.d.ts.map +1 -1
  231. package/dist/shared/io/fileLock.js +22 -3
  232. package/dist/shared/io/fileLock.js.map +1 -1
  233. package/dist/shared/io/frictionCapture.d.ts +73 -19
  234. package/dist/shared/io/frictionCapture.d.ts.map +1 -1
  235. package/dist/shared/io/frictionCapture.js +21 -30
  236. package/dist/shared/io/frictionCapture.js.map +1 -1
  237. package/dist/shared/io/lockedJsonStore.d.ts +24 -3
  238. package/dist/shared/io/lockedJsonStore.d.ts.map +1 -1
  239. package/dist/shared/io/lockedJsonStore.js +35 -7
  240. package/dist/shared/io/lockedJsonStore.js.map +1 -1
  241. package/dist/shared/io/stepContractWriter.d.ts +3 -3
  242. package/dist/shared/io/stepContractWriter.d.ts.map +1 -1
  243. package/dist/shared/io/stepContractWriter.js +97 -13
  244. package/dist/shared/io/stepContractWriter.js.map +1 -1
  245. package/dist/shared/submission/expectedSubmissions.d.ts +18 -0
  246. package/dist/shared/submission/expectedSubmissions.d.ts.map +1 -1
  247. package/dist/shared/submission/expectedSubmissions.js +17 -0
  248. package/dist/shared/submission/expectedSubmissions.js.map +1 -1
  249. package/dist/shared/submission/handRecovery.d.ts.map +1 -1
  250. package/dist/shared/submission/handRecovery.js +9 -11
  251. package/dist/shared/submission/handRecovery.js.map +1 -1
  252. package/dist/shared/submission/hostHandoffCore.d.ts +155 -0
  253. package/dist/shared/submission/hostHandoffCore.d.ts.map +1 -0
  254. package/dist/shared/submission/hostHandoffCore.js +210 -0
  255. package/dist/shared/submission/hostHandoffCore.js.map +1 -0
  256. package/dist/shared/submission/submissionLedger.d.ts +5 -1
  257. package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
  258. package/dist/shared/submission/submissionLedger.js +33 -3
  259. package/dist/shared/submission/submissionLedger.js.map +1 -1
  260. package/dist/shared/tooling/commandShape.d.ts +29 -0
  261. package/dist/shared/tooling/commandShape.d.ts.map +1 -1
  262. package/dist/shared/tooling/commandShape.js +82 -10
  263. package/dist/shared/tooling/commandShape.js.map +1 -1
  264. package/dist/shared/tooling/exec.d.ts +32 -0
  265. package/dist/shared/tooling/exec.d.ts.map +1 -1
  266. package/dist/shared/tooling/exec.js +189 -0
  267. package/dist/shared/tooling/exec.js.map +1 -1
  268. package/docs/audit-pkg/contracts.md +2 -1
  269. package/docs/audit-pkg/operator-guide.md +12 -6
  270. package/docs/audit-pkg/product.md +22 -6
  271. package/package.json +13 -6
  272. package/schemas/audit-code-v1alpha1.schema.json +297 -0
  273. package/schemas/audit_result.schema.json +4 -0
  274. package/schemas/audit_results.schema.json +4 -0
  275. package/scripts/audit/postinstall.mjs +4 -2
  276. package/scripts/postinstall.mjs +1 -1
  277. package/scripts/remediate/postinstall.mjs +3 -1
  278. package/scripts/shared/install-host-assets.mjs +15 -8
  279. package/skills/audit-code/SKILL.md +4 -2
  280. package/skills/audit-code/audit-code.prompt.md +5 -2
  281. package/skills/audit-code/opencode-command-template.txt +1 -1
  282. package/wrapper/audit-code-wrapper-build.mjs +3 -3
  283. package/wrapper/audit-code-wrapper-install-hosts.mjs +3 -3
  284. package/wrapper/audit-code-wrapper-lib.mjs +82 -16
  285. package/wrapper/installer-verb-help.mjs +14 -4
  286. package/wrapper/remediate-code-wrapper-install-hosts.mjs +3 -3
  287. package/docs/audit-pkg/development.md +0 -113
  288. package/docs/audit-pkg/release.md +0 -140
@@ -1,7 +1,6 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { mkdir } from "node:fs/promises";
3
- import { isAbsolute, join, resolve } from "node:path";
4
- import { headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, assertSubmissionRunId, hashContent, isGitRepo, normalizeRepoPath, readSubmissionDocument, readSubmissionLedger, repoRelativePath, resolveContainedPath, spawnSyncHidden, stableStringify, submissionPathFor, writeJsonFile, } from "audit-tools/shared";
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, stringArray, spawnSyncHidden, stableStringify, writeJsonFile, } from "audit-tools/shared";
5
4
  import { RemediationHostHandoffRecordSchema, RemediationPlanSchema, isClarificationCategory, } from "../../state/types.js";
6
5
  import { ITEM_STATUSES, isTerminalStatus, isVerifiedCompleteStatus, } from "../../state/itemStatus.js";
7
6
  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";
@@ -95,31 +94,12 @@ const CURRENT_ITEM_KEYS = new Set([
95
94
  "host_result_evidence",
96
95
  "finding_id",
97
96
  "incomplete_coverage_attempts",
98
- "item_spec",
99
97
  "last_successful_step",
100
98
  "mechanical_verification",
101
99
  "rework_count",
102
100
  "started_at",
103
101
  "status",
104
102
  ]);
105
- function compareCodeUnits(left, right) {
106
- return left < right ? -1 : left > right ? 1 : 0;
107
- }
108
- function isRecord(value) {
109
- return value !== null && typeof value === "object" && !Array.isArray(value);
110
- }
111
- function hasExactKeys(value, expected) {
112
- const actual = Object.keys(value).sort(compareCodeUnits);
113
- const canonicalExpected = [...expected].sort(compareCodeUnits);
114
- return (actual.length === canonicalExpected.length &&
115
- actual.every((key, index) => key === canonicalExpected[index]));
116
- }
117
- function isCommit(value) {
118
- return typeof value === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value);
119
- }
120
- function isSha256(value) {
121
- return typeof value === "string" && /^[0-9a-f]{64}$/u.test(value);
122
- }
123
103
  function hasOnlyKnownKeys(value, known) {
124
104
  return Object.keys(value).every((key) => known.has(key));
125
105
  }
@@ -325,15 +305,19 @@ function cannotPrepareMessage(root, state, raised) {
325
305
  return `Cannot prepare a remediation host workload: ${messages.join("; ")}`;
326
306
  }
327
307
  function resolveBoundaryPaths(params) {
328
- assertSubmissionRunId(params.runId, "remediation host run id");
329
- const root = resolve(params.root);
330
- const artifactsDir = resolveContainedPath(root, params.artifactsDir, "artifactsDir");
331
- const runDir = resolveContainedPath(artifactsDir, join("runs", params.runId, "implement"), "remediation host run directory");
308
+ // The remediate draw's run dir carries the `implement` lane segment — its
309
+ // runs directory also holds triage/closing lanes — which the core takes as a
310
+ // parameter rather than a fork.
311
+ const core = resolveHostHandoffPaths({
312
+ ...params,
313
+ runDirSegments: ["implement"],
314
+ runIdLabel: "remediation host run id",
315
+ });
332
316
  return {
333
- root,
334
- artifactsDir,
335
- workloadPath: join(runDir, "host-workload.json"),
336
- resultDir: join(runDir, "host-results"),
317
+ root: core.root,
318
+ artifactsDir: core.artifactsDir,
319
+ workloadPath: core.workloadPath,
320
+ resultDir: core.resultDir,
337
321
  };
338
322
  }
339
323
  /**
@@ -342,7 +326,13 @@ function resolveBoundaryPaths(params) {
342
326
  * divergence between them would have been silent on both sides.
343
327
  */
344
328
  function resultPathFor(paths, workItemId) {
345
- return submissionPathFor({ root: paths.root, submissionDir: paths.resultDir }, workItemId);
329
+ return hostHandoffResultPath({
330
+ root: paths.root,
331
+ artifactsDir: paths.artifactsDir,
332
+ runDir: paths.workloadPath.slice(0, paths.workloadPath.length - "host-workload.json".length - 1),
333
+ resultDir: paths.resultDir,
334
+ workloadPath: paths.workloadPath,
335
+ }, workItemId);
346
336
  }
347
337
  /**
348
338
  * Resolve the submission validator the INGEST applies to one work item, plus
@@ -478,7 +468,6 @@ function buildFindingAssignments(state, block) {
478
468
  }
479
469
  return {
480
470
  finding: FindingSchema.parse(finding),
481
- ...(item.item_spec ? { item_spec: item.item_spec } : {}),
482
471
  ...(item.clarification_context
483
472
  ? { clarification_context: item.clarification_context }
484
473
  : {}),
@@ -511,15 +500,12 @@ function buildWorkItem(paths, block, baselineCommit, state) {
511
500
  finding_ids: [...block.items],
512
501
  allowed_files: allowedFiles,
513
502
  baseline_commit: baselineCommit,
514
- prompt: { text: promptText, sha256: hashContent(promptText) },
503
+ prompt: { text: promptText, sha256: promptSha256(promptText) },
515
504
  required_tests: requiredTests,
516
505
  result_path: resultPath,
517
506
  token_estimate: block.token_estimate ?? 0,
518
507
  };
519
508
  }
520
- function hostWorkloadSha256(workload) {
521
- return hashContent(stableStringify(workload));
522
- }
523
509
  function buildCanonicalWorkload(params) {
524
510
  const requestedIds = params.workItemIds
525
511
  ? new Set(params.workItemIds)
@@ -566,7 +552,7 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
566
552
  !hasExactKeys(value.prompt, ["sha256", "text"]) ||
567
553
  typeof value.prompt.text !== "string" ||
568
554
  !isSha256(value.prompt.sha256) ||
569
- hashContent(value.prompt.text) !== value.prompt.sha256) {
555
+ promptSha256(value.prompt.text) !== value.prompt.sha256) {
570
556
  return null;
571
557
  }
572
558
  const block = state.plan.blocks.find((candidate) => candidate.block_id === value.id);
@@ -584,40 +570,32 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
584
570
  : null;
585
571
  }
586
572
  function parseWorkload(value, paths, runId, state) {
587
- if (!isRecord(value) ||
588
- !hasExactKeys(value, ["contract_version", "run_id", "work_items"]) ||
589
- value.contract_version !== WORKLOAD_CONTRACT_VERSION ||
590
- value.run_id !== runId ||
591
- !Array.isArray(value.work_items)) {
573
+ // Envelope (keys, contract version, run id, items array) is the CORE's check.
574
+ const envelope = parseWorkloadEnvelope(value, {
575
+ contractVersion: WORKLOAD_CONTRACT_VERSION,
576
+ runId,
577
+ });
578
+ if (!envelope.ok)
592
579
  return null;
593
- }
594
580
  const binding = state.host_handoff;
595
581
  if (binding &&
596
582
  (binding.run_id !== runId ||
597
- hostWorkloadSha256(value) !==
583
+ contentSha256(value) !==
598
584
  binding.workload_sha256)) {
599
585
  return null;
600
586
  }
601
- const workItems = value.work_items.map((item) => parseWorkItem(item, paths, state, binding?.baseline_commit));
602
- if (workItems.some((item) => item === null))
587
+ const workItems = parseAllWorkloadItems(envelope.rawItems, (item) => parseWorkItem(item, paths, state, binding?.baseline_commit));
588
+ if (workItems === null)
603
589
  return null;
604
590
  const ids = workItems.map((item) => item.id);
605
- if (new Set(ids).size !== ids.length ||
606
- ids.some((id, index) => index > 0 && compareCodeUnits(ids[index - 1], id) >= 0) ||
591
+ // Strictly ascending covers BOTH "sorted" and "duplicate-free" — the two
592
+ // properties the byte-for-byte re-derivation comparison needs.
593
+ if (!idsAreStrictlyAscending(ids) ||
607
594
  (binding !== undefined && !sameStrings(ids, binding.work_item_ids))) {
608
595
  return null;
609
596
  }
610
597
  return value;
611
598
  }
612
- function stringArray(value) {
613
- return Array.isArray(value) && value.every((entry) => typeof entry === "string")
614
- ? value
615
- : null;
616
- }
617
- function sameStrings(left, right) {
618
- return (left.length === right.length &&
619
- left.every((entry, index) => entry === right[index]));
620
- }
621
599
  function invalidResult(reason) {
622
600
  return { ok: false, reason };
623
601
  }
@@ -631,11 +609,11 @@ function parseResult(value, runId, workItem) {
631
609
  "prompt_sha256",
632
610
  "outcome",
633
611
  ]) ||
634
- typeof value.result_id !== "string" ||
635
- value.result_id.length === 0 ||
636
- value.run_id !== runId ||
637
- value.work_item_id !== workItem.id ||
638
- value.prompt_sha256 !== workItem.prompt.sha256 ||
612
+ !resultIdentityIsBound(value, {
613
+ runId,
614
+ workItemId: workItem.id,
615
+ promptSha256: workItem.prompt.sha256,
616
+ }) ||
639
617
  !isRecord(value.outcome) ||
640
618
  typeof value.outcome.status !== "string") {
641
619
  return invalidResult("decision must match the exact current run, work-item, and prompt binding");
@@ -690,11 +668,11 @@ function parseResult(value, runId, workItem) {
690
668
  "worktree_evidence",
691
669
  ]) ||
692
670
  value.contract_version !== RESULT_CONTRACT_VERSION ||
693
- typeof value.result_id !== "string" ||
694
- value.result_id.length === 0 ||
695
- value.run_id !== runId ||
696
- value.work_item_id !== workItem.id ||
697
- value.prompt_sha256 !== workItem.prompt.sha256) {
671
+ !resultIdentityIsBound(value, {
672
+ runId,
673
+ workItemId: workItem.id,
674
+ promptSha256: workItem.prompt.sha256,
675
+ })) {
698
676
  return invalidResult("result must match the exact current contract, run, work-item, and prompt binding");
699
677
  }
700
678
  const changedFiles = stringArray(value.changed_files);
@@ -797,6 +775,134 @@ function gitChangedFilesOfCommit(root, commit) {
797
775
  return null;
798
776
  return [...new Set((result.stdout ?? "").split("\0").filter(Boolean))].sort(compareCodeUnits);
799
777
  }
778
+ /**
779
+ * Every repo-relative path the tree shows as touched since `baseline` — the
780
+ * commits baseline→HEAD, the working tree's own deviation from HEAD (staged and
781
+ * unstaged alike), and the untracked files git considers repository content.
782
+ *
783
+ * All three legs are needed to falsify a no-change claim, and they enumerate the
784
+ * three ways a host can have edited: committed (leg 1), edited a TRACKED file
785
+ * and left it uncommitted (leg 2), and CREATED a file (leg 3). A new `src/*.ts`
786
+ * is a real edit and the most natural shape a remediation takes; without leg 3
787
+ * the cheapest way to smuggle one past a no-change claim was simply never to
788
+ * `git add` it. `null` means git could not answer, which callers must treat as
789
+ * "cannot corroborate" rather than as "nothing changed".
790
+ *
791
+ * The untracked leg honours `--exclude-standard`, so it enumerates only what git
792
+ * itself treats as content — a repo's `.gitignore`d build and coverage output is
793
+ * already invisible to it. Two exemptions cover the remainder, both ground
794
+ * truth rather than the host's word:
795
+ *
796
+ * - THIS TOOL'S OWN ARTIFACT TREE ({@link AUDIT_TOOLS_DIRNAME}) is subtracted
797
+ * here, explicitly. In a real repository the tool writes a managed
798
+ * `.gitignore` block covering it, so it never reaches this probe at all; the
799
+ * explicit subtraction is what makes that independent of whether the block
800
+ * has been written yet, so a bare root (a fixture, a first run) cannot
801
+ * manufacture a false refusal out of the tool's own workload, prompt and
802
+ * result documents.
803
+ * - PRE-EXISTING untracked strays are excused by the caller's `excusedPaths`,
804
+ * for free: `run_start_dirty` is captured from `stagedAndUntracked` before
805
+ * any remediation edit exists, so it already enumerates untracked files.
806
+ * What survives both is an untracked file that appeared DURING the run — the
807
+ * only untracked class that can be this host's edit.
808
+ */
809
+ function gitChangedFilesSince(root, baseline) {
810
+ const files = new Set();
811
+ for (const args of [
812
+ // baseline → HEAD: what the host committed.
813
+ ["diff", "--name-only", "-z", baseline, "HEAD"],
814
+ // HEAD → working tree: what the host edited and did not commit.
815
+ ["diff", "--name-only", "-z", "HEAD"],
816
+ // Never added: what the host CREATED. `--exclude-standard` keeps git's own
817
+ // ignore rules authoritative.
818
+ ["ls-files", "--others", "--exclude-standard", "-z"],
819
+ ]) {
820
+ const probe = spawnSyncHidden("git", args, {
821
+ cwd: root,
822
+ encoding: "utf8",
823
+ shell: false,
824
+ });
825
+ if (probe.error || probe.status !== 0)
826
+ return null;
827
+ for (const file of (probe.stdout ?? "").split("\0").filter(Boolean)) {
828
+ if (isAuditToolsArtifactPath(file))
829
+ continue;
830
+ files.add(file);
831
+ }
832
+ }
833
+ return [...files].sort(compareCodeUnits);
834
+ }
835
+ /** Whether a repo-relative path lives inside this tool's own artifact tree. */
836
+ function isAuditToolsArtifactPath(path) {
837
+ const normalized = normalizeRepoPath(path);
838
+ return (normalized === AUDIT_TOOLS_DIRNAME ||
839
+ normalized.startsWith(`${AUDIT_TOOLS_DIRNAME}/`));
840
+ }
841
+ /**
842
+ * Corroborate an explicit `resolved_no_change` decision against the repository.
843
+ *
844
+ * A no-change decision used to be accepted on its evidence STRINGS alone, with
845
+ * only the required tests re-run — so a host that had in fact edited and then
846
+ * declared "nothing to do" was recorded as verified-no-change, and the edit
847
+ * rode into the run unattributed. The claim is mechanically falsifiable, so it
848
+ * is FALSIFIED.
849
+ *
850
+ * The scope of the falsification is the FULL write-scope corroboration every
851
+ * other acceptance path gets, NOT a narrowing to this item's `allowed_files`.
852
+ * `corroborateHostResult` refuses a landed commit that touched anything outside
853
+ * `allowed_files`; a no-change decision that narrowed the check to files INSIDE
854
+ * `allowed_files` would be the inverse rule — the out-of-scope edit, the more
855
+ * serious of the two, would be the one silently admitted. So EVERY path the
856
+ * tree shows as moved since the workload baseline refuses the claim.
857
+ *
858
+ * `excusedPaths` is the only exemption, and it is ground truth rather than the
859
+ * host's word: `run_start_dirty` (already dirty before the run began, so not
860
+ * evidence that this host edited anything — and because it is captured from
861
+ * `stagedAndUntracked`, it excuses pre-existing UNTRACKED strays too) unioned
862
+ * with the accepted edit surface — `applied_edit_surface` plus whatever this
863
+ * same ingest has already corroborated and accepted, so a sibling work item's
864
+ * legitimately landed files do not falsify this item's claim.
865
+ *
866
+ * Fails CLOSED, like every other corroboration here: a git that cannot answer
867
+ * refuses the claim rather than admitting it.
868
+ */
869
+ function corroborateNoChangeClaim(params) {
870
+ const { root, workItem, excusedPaths } = params;
871
+ if (!isGitRepo(root))
872
+ return { ok: true };
873
+ const baseline = workItem.baseline_commit;
874
+ if (!gitCommitExists(root, baseline)) {
875
+ return {
876
+ ok: false,
877
+ code: "commit_missing",
878
+ message: "resolved_no_change cannot be corroborated: baseline_commit does not resolve to a real commit",
879
+ };
880
+ }
881
+ const changed = gitChangedFilesSince(root, baseline);
882
+ if (changed === null) {
883
+ return {
884
+ ok: false,
885
+ code: "commit_missing",
886
+ message: "resolved_no_change cannot be corroborated: git could not enumerate the changes since the workload baseline",
887
+ };
888
+ }
889
+ const violating = changed.filter((path) => !excusedPaths.has(normalizeRepoPath(path)));
890
+ if (violating.length > 0) {
891
+ const inScope = violating.filter((path) => workItem.allowed_files.includes(path));
892
+ const outOfScope = violating.filter((path) => !workItem.allowed_files.includes(path));
893
+ return {
894
+ ok: false,
895
+ code: "changed_files_mismatch",
896
+ message: "resolved_no_change is contradicted by the tree — these files changed since the " +
897
+ `workload baseline: ${violating.join(", ")}` +
898
+ (outOfScope.length > 0
899
+ ? ` (outside the prompt-bound allowed_files: ${outOfScope.join(", ")}` +
900
+ (inScope.length > 0 ? `; inside: ${inScope.join(", ")})` : ")")
901
+ : ""),
902
+ };
903
+ }
904
+ return { ok: true };
905
+ }
800
906
  /**
801
907
  * Per-command deadline. A required test is host-authored and may legitimately be
802
908
  * a full suite, so the bound is generous; what changed is that hitting it is now
@@ -865,10 +971,19 @@ function requiredTestVerdictKey(root, command) {
865
971
  * first-class outcome of this function, and an outcome that can only be reached
866
972
  * by waiting ten real minutes is an outcome nothing ever tests.
867
973
  */
868
- export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT_MS) {
869
- const result = spawnSync(command, {
974
+ export async function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT_MS) {
975
+ // AWAITED, never `spawnSync`: ingestion runs with the remediation state lock
976
+ // held, and a synchronous child blocks the event loop for the whole suite —
977
+ // starving the lock's mtime heartbeat until a LIVE lock is classified stale
978
+ // and stolen mid-ingest.
979
+ //
980
+ // argv + `shell: false`, never a shell string. A required test is a workload
981
+ // command that already cleared the declared-shape gate at the producer, so
982
+ // splitting it is unambiguous, and dropping the shell removes the last place
983
+ // an ingest hands a declared string to `sh`/`cmd.exe`. `resolveExecArgv`
984
+ // inside the runner is what keeps the npm/npx shims resolvable on win32.
985
+ const result = await runTrackedAsync(parseCommandString(command), {
870
986
  cwd: root,
871
- shell: true,
872
987
  // Captured, not discarded: without it a red ingest reports that something
873
988
  // failed and nothing about why.
874
989
  encoding: "utf8",
@@ -920,7 +1035,7 @@ export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT
920
1035
  // `status` with `signal` null — so this branch is unreachable on the platform
921
1036
  // this repo runs its suites on, and no test exercises it. It is kept because
922
1037
  // the runner is OS-agnostic by contract, not because it has been observed.
923
- if (result.signal !== null) {
1038
+ if (result.signal !== null && result.signal !== undefined) {
924
1039
  return {
925
1040
  command,
926
1041
  outcome: "failed",
@@ -941,7 +1056,7 @@ export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT
941
1056
  }
942
1057
  return null;
943
1058
  }
944
- function rerunRequiredTests(root, commands,
1059
+ async function rerunRequiredTests(root, commands,
945
1060
  /** `null` on the normal lane — see {@link RemediationRequiredTestVerdicts}. */
946
1061
  verdicts) {
947
1062
  const failures = [];
@@ -962,7 +1077,7 @@ verdicts) {
962
1077
  }
963
1078
  continue;
964
1079
  }
965
- const failure = runRequiredTest(root, command);
1080
+ const failure = await runRequiredTest(root, command);
966
1081
  if (failure !== null)
967
1082
  failures.push(failure);
968
1083
  }
@@ -1028,11 +1143,11 @@ export async function precomputeRecoveryTestVerdicts(params) {
1028
1143
  }
1029
1144
  }
1030
1145
  for (const command of commands) {
1031
- verdicts.set(requiredTestVerdictKey(paths.root, command), runRequiredTest(paths.root, command));
1146
+ verdicts.set(requiredTestVerdictKey(paths.root, command), await runRequiredTest(paths.root, command));
1032
1147
  }
1033
1148
  return verdicts;
1034
1149
  }
1035
- function corroborateHostResult(params) {
1150
+ async function corroborateHostResult(params) {
1036
1151
  const { root, state, workItem, result, verdicts } = params;
1037
1152
  const baseline = workItem.baseline_commit;
1038
1153
  const landed = result.commit_evidence.after;
@@ -1103,7 +1218,7 @@ function corroborateHostResult(params) {
1103
1218
  message: `landed files overlap pre-existing run-start dirt: ${dirtyOverlap.join(", ")}`,
1104
1219
  };
1105
1220
  }
1106
- const failedTests = rerunRequiredTests(root, workItem.required_tests, verdicts);
1221
+ const failedTests = await rerunRequiredTests(root, workItem.required_tests, verdicts);
1107
1222
  if (failedTests.length > 0) {
1108
1223
  const issue = requiredTestIssue(workItem, failedTests);
1109
1224
  return { ok: false, code: issue.code, message: issue.message };
@@ -1163,7 +1278,7 @@ export async function prepareRemediationHostHandoff(params) {
1163
1278
  .map((issue) => issue.message)
1164
1279
  .join("; ")}`);
1165
1280
  }
1166
- const workloadDigest = hostWorkloadSha256(workload);
1281
+ const workloadDigest = contentSha256(workload);
1167
1282
  if (existingRecord &&
1168
1283
  existingRecord.workload_sha256 !== workloadDigest) {
1169
1284
  throw new Error("Trusted remediation host workload no longer matches the persisted state binding");
@@ -1312,7 +1427,13 @@ export async function ingestRemediationHostResults(params) {
1312
1427
  // the same acceptance.
1313
1428
  let recordedRecoveryMarks = null;
1314
1429
  const landedFiles = new Set(nextState.applied_edit_surface ?? []);
1315
- const requireRepositoryCorroboration = state.host_handoff !== undefined || isGitRepo(paths.root);
1430
+ // Can this ingest be corroborated against ground truth AT ALL? A git root
1431
+ // supplies the tree; a persisted `host_handoff` supplies the trusted binding.
1432
+ // With NEITHER there is nothing to check a host's claim against, and the
1433
+ // remaining evidence is the host's own attestation — which is exactly the
1434
+ // claim under test. That branch is REFUSED for both result and decision
1435
+ // documents rather than admitted on the attestation alone.
1436
+ const canCorroborate = state.host_handoff !== undefined || isGitRepo(paths.root);
1316
1437
  for (const workItem of workload.work_items) {
1317
1438
  const pendingItems = workItem.finding_ids.filter((findingId) => nextState.items[findingId]?.status === "pending");
1318
1439
  if (pendingItems.length === 0)
@@ -1372,7 +1493,36 @@ export async function ingestRemediationHostResults(params) {
1372
1493
  const result = parsed.result;
1373
1494
  const outcome = result.outcome;
1374
1495
  if (outcome.status === "resolved_no_change") {
1375
- const failedTests = rerunRequiredTests(paths.root, workItem.required_tests, requiredTestVerdicts);
1496
+ if (!canCorroborate) {
1497
+ issues.push({
1498
+ code: "trusted_binding_missing",
1499
+ work_item_id: workItem.id,
1500
+ result_path: workItem.result_path,
1501
+ 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",
1502
+ });
1503
+ continue;
1504
+ }
1505
+ const noChange = corroborateNoChangeClaim({
1506
+ root: paths.root,
1507
+ workItem,
1508
+ // Ground truth only: pre-existing dirt plus the edit surface this run
1509
+ // has already corroborated and accepted (`landedFiles` starts from
1510
+ // `applied_edit_surface` and grows as this same ingest accepts).
1511
+ excusedPaths: new Set([
1512
+ ...(state.run_start_dirty ?? []).map(normalizeRepoPath),
1513
+ ...[...landedFiles].map(normalizeRepoPath),
1514
+ ]),
1515
+ });
1516
+ if (!noChange.ok) {
1517
+ issues.push({
1518
+ code: noChange.code,
1519
+ work_item_id: workItem.id,
1520
+ result_path: workItem.result_path,
1521
+ message: noChange.message,
1522
+ });
1523
+ continue;
1524
+ }
1525
+ const failedTests = await rerunRequiredTests(paths.root, workItem.required_tests, requiredTestVerdicts);
1376
1526
  if (failedTests.length > 0) {
1377
1527
  issues.push(requiredTestIssue(workItem, failedTests));
1378
1528
  continue;
@@ -1414,76 +1564,82 @@ export async function ingestRemediationHostResults(params) {
1414
1564
  continue;
1415
1565
  }
1416
1566
  const result = parsed.result;
1417
- let acceptedFiles = result.changed_files;
1418
- if (requireRepositoryCorroboration) {
1419
- const corroborated = corroborateHostResult({
1420
- root: paths.root,
1421
- state,
1422
- workItem,
1423
- result,
1424
- verdicts: requiredTestVerdicts,
1425
- recovery: params.recovery !== undefined,
1567
+ if (!canCorroborate) {
1568
+ issues.push({
1569
+ code: "trusted_binding_missing",
1570
+ work_item_id: workItem.id,
1571
+ result_path: workItem.result_path,
1572
+ 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",
1426
1573
  });
1427
- if (!corroborated.ok) {
1574
+ continue;
1575
+ }
1576
+ const corroborated = await corroborateHostResult({
1577
+ root: paths.root,
1578
+ state,
1579
+ workItem,
1580
+ result,
1581
+ verdicts: requiredTestVerdicts,
1582
+ recovery: params.recovery !== undefined,
1583
+ });
1584
+ if (!corroborated.ok) {
1585
+ issues.push({
1586
+ code: corroborated.code,
1587
+ work_item_id: workItem.id,
1588
+ result_path: workItem.result_path,
1589
+ message: corroborated.message,
1590
+ });
1591
+ continue;
1592
+ }
1593
+ if (corroborated.usedRecovery) {
1594
+ // No acceptance without a record. The mark goes down BEFORE the item is
1595
+ // marked resolved, and an append that throws refuses this item rather
1596
+ // than landing an acceptance the ledger cannot account for — the run
1597
+ // must never read as one that never drifted. The refusal is per item:
1598
+ // an unwritable ledger is not a reason to discard the whole ingest.
1599
+ try {
1600
+ recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
1601
+ // The mark's identity is (run, item, LANDED COMMIT), not just
1602
+ // (run, item): an item re-opened and later re-accepted from a
1603
+ // DIFFERENT landing is a different relaxed acceptance and earns its
1604
+ // own record. Only a retry of the SAME landing is a duplicate. The
1605
+ // landed sha is matched inside the message because the shared event
1606
+ // contract carries no commit field, and a 40-hex sha this writer
1607
+ // itself emitted is an unambiguous token to match on.
1608
+ const landedCommit = result.commit_evidence.after;
1609
+ const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
1610
+ event.submission_id === workItem.id &&
1611
+ (event.message ?? "").includes(landedCommit));
1612
+ if (!alreadyMarked) {
1613
+ const event = {
1614
+ contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
1615
+ run_id: params.runId,
1616
+ submission_id: workItem.id,
1617
+ lane: workItem.id,
1618
+ kind: "accepted_via_recovery",
1619
+ // Derived from what was actually probed, never asserted: the
1620
+ // baseline was found in no ref and unreachable from HEAD.
1621
+ message: `accepted under recovery: the trusted baseline ${workItem.baseline_commit} is ` +
1622
+ "contained by no ref and unreachable from HEAD, so landed commit " +
1623
+ `${landedCommit} was corroborated against HEAD and this ` +
1624
+ "item's bound scope instead of against baseline ancestry",
1625
+ recorded_at: new Date().toISOString(),
1626
+ };
1627
+ await appendSubmissionEvent(paths.artifactsDir, event);
1628
+ recordedRecoveryMarks.push(event);
1629
+ }
1630
+ }
1631
+ catch (error) {
1428
1632
  issues.push({
1429
- code: corroborated.code,
1633
+ code: "recovery_unrecorded",
1430
1634
  work_item_id: workItem.id,
1431
1635
  result_path: workItem.result_path,
1432
- message: corroborated.message,
1636
+ message: "the recovery acceptance could not be recorded on the submission ledger, so it " +
1637
+ `was refused: ${error instanceof Error ? error.message : String(error)}`,
1433
1638
  });
1434
1639
  continue;
1435
1640
  }
1436
- if (corroborated.usedRecovery) {
1437
- // No acceptance without a record. The mark goes down BEFORE the item is
1438
- // marked resolved, and an append that throws refuses this item rather
1439
- // than landing an acceptance the ledger cannot account for — the run
1440
- // must never read as one that never drifted. The refusal is per item:
1441
- // an unwritable ledger is not a reason to discard the whole ingest.
1442
- try {
1443
- recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
1444
- // The mark's identity is (run, item, LANDED COMMIT), not just
1445
- // (run, item): an item re-opened and later re-accepted from a
1446
- // DIFFERENT landing is a different relaxed acceptance and earns its
1447
- // own record. Only a retry of the SAME landing is a duplicate. The
1448
- // landed sha is matched inside the message because the shared event
1449
- // contract carries no commit field, and a 40-hex sha this writer
1450
- // itself emitted is an unambiguous token to match on.
1451
- const landedCommit = result.commit_evidence.after;
1452
- const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
1453
- event.submission_id === workItem.id &&
1454
- (event.message ?? "").includes(landedCommit));
1455
- if (!alreadyMarked) {
1456
- const event = {
1457
- contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
1458
- run_id: params.runId,
1459
- submission_id: workItem.id,
1460
- lane: workItem.id,
1461
- kind: "accepted_via_recovery",
1462
- // Derived from what was actually probed, never asserted: the
1463
- // baseline was found in no ref and unreachable from HEAD.
1464
- message: `accepted under recovery: the trusted baseline ${workItem.baseline_commit} is ` +
1465
- "contained by no ref and unreachable from HEAD, so landed commit " +
1466
- `${landedCommit} was corroborated against HEAD and this ` +
1467
- "item's bound scope instead of against baseline ancestry",
1468
- recorded_at: new Date().toISOString(),
1469
- };
1470
- await appendSubmissionEvent(paths.artifactsDir, event);
1471
- recordedRecoveryMarks.push(event);
1472
- }
1473
- }
1474
- catch (error) {
1475
- issues.push({
1476
- code: "recovery_unrecorded",
1477
- work_item_id: workItem.id,
1478
- result_path: workItem.result_path,
1479
- message: "the recovery acceptance could not be recorded on the submission ledger, so it " +
1480
- `was refused: ${error instanceof Error ? error.message : String(error)}`,
1481
- });
1482
- continue;
1483
- }
1484
- }
1485
- acceptedFiles = corroborated.changedFiles;
1486
1641
  }
1642
+ const acceptedFiles = corroborated.changedFiles;
1487
1643
  const completedAt = new Date().toISOString();
1488
1644
  for (const findingId of pendingItems) {
1489
1645
  const item = nextState.items[findingId];