@fitlab-ai/agent-infra 0.9.14 → 0.10.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 (354) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +9 -4
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  8. package/dist/lib/defaults.json +103 -103
  9. package/dist/lib/fs/durable-write.js +39 -0
  10. package/dist/lib/fs/file-lock.js +111 -0
  11. package/dist/lib/host-control/audit.js +19 -0
  12. package/dist/lib/host-control/client.js +142 -0
  13. package/dist/lib/host-control/command.js +49 -0
  14. package/dist/lib/host-control/path.js +160 -0
  15. package/dist/lib/host-control/secure-fs.js +140 -0
  16. package/dist/lib/host-control/server.js +159 -0
  17. package/dist/lib/host-control/service.js +112 -0
  18. package/dist/lib/init.js +11 -2
  19. package/dist/lib/internal/cli-route-inventory.js +4 -2
  20. package/dist/lib/internal/codex-lifecycle.js +6 -6
  21. package/dist/lib/internal/demo-promotion.js +322 -0
  22. package/dist/lib/internal/demo-transcript.js +454 -0
  23. package/dist/lib/internal/host-control.js +28 -0
  24. package/dist/lib/internal/manual-validation.js +331 -0
  25. package/dist/lib/internal/platform-pr.js +18 -4
  26. package/dist/lib/internal/release-workflow.js +63 -84
  27. package/dist/lib/internal/sandbox-control.js +149 -2
  28. package/dist/lib/internal/task-artifact.js +16 -263
  29. package/dist/lib/internal/task-create.js +6 -5
  30. package/dist/lib/internal/task-event.js +37 -37
  31. package/dist/lib/internal/task-lifecycle.js +2 -2
  32. package/dist/lib/internal/task-operation-registry.js +101 -4
  33. package/dist/lib/internal/task-review.js +11 -56
  34. package/dist/lib/internal/task-validate.js +30 -10
  35. package/dist/lib/owned-path.js +33 -0
  36. package/dist/lib/platform/check-status.js +11 -0
  37. package/dist/lib/platform/comment-safety.js +4 -1
  38. package/dist/lib/platform/completion-backfill.js +1 -1
  39. package/dist/lib/platform/github-data.js +241 -0
  40. package/dist/lib/platform/github-provider.js +15 -20
  41. package/dist/lib/platform/issue-comments.js +18 -12
  42. package/dist/lib/platform/issues.js +2 -180
  43. package/dist/lib/platform/pr-change-report.js +5 -4
  44. package/dist/lib/platform/pr-checks.js +12 -65
  45. package/dist/lib/platform/pr-summary.js +117 -33
  46. package/dist/lib/platform/pull-requests.js +5 -108
  47. package/dist/lib/platform/snapshots.js +2 -0
  48. package/dist/lib/platform/task-identities.js +2 -9
  49. package/dist/lib/platform/verification-sync.js +132 -143
  50. package/dist/lib/pr-review/evidence-grading.js +4 -8
  51. package/dist/lib/release/workflow.js +1 -1
  52. package/dist/lib/sandbox/commands/create.js +7 -2
  53. package/dist/lib/sandbox/commands/rm.js +19 -1511
  54. package/dist/lib/sandbox/control/audit.js +203 -0
  55. package/dist/lib/sandbox/control/client.js +93 -54
  56. package/dist/lib/sandbox/control/executor.js +129 -65
  57. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  58. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  59. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  60. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  61. package/dist/lib/sandbox/control/protocol.js +55 -4
  62. package/dist/lib/sandbox/control/server.js +518 -127
  63. package/dist/lib/sandbox/control/state.js +64 -0
  64. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  65. package/dist/lib/sandbox/control/workflow-executor.js +89 -0
  66. package/dist/lib/sandbox/index.js +1 -2
  67. package/dist/lib/sandbox/recovery.js +28 -4
  68. package/dist/lib/sandbox/removal.js +822 -0
  69. package/dist/lib/sandbox/task-cleanup.js +217 -0
  70. package/dist/lib/sandbox/workspace-identity.js +17 -1
  71. package/dist/lib/sandbox/workspace-view.js +13 -14
  72. package/dist/lib/task/activity-intent.js +2 -1
  73. package/dist/lib/task/activity-log.js +30 -24
  74. package/dist/lib/task/artifact-command.js +79 -0
  75. package/dist/lib/task/artifact-lifecycle.js +30 -28
  76. package/dist/lib/task/artifact-name.js +36 -0
  77. package/dist/lib/task/artifact-operations.js +29 -289
  78. package/dist/lib/task/artifact-receipts.js +3 -15
  79. package/dist/lib/task/artifact-recovery.js +430 -0
  80. package/dist/lib/task/artifact-repair-intent.js +134 -29
  81. package/dist/lib/task/artifact-schema.js +6 -14
  82. package/dist/lib/task/capabilities.js +12 -19
  83. package/dist/lib/task/codex-orchestration.js +71 -25
  84. package/dist/lib/task/commit-identity.js +28 -0
  85. package/dist/lib/task/commit-operation.js +33 -20
  86. package/dist/lib/task/control-authority.js +403 -6
  87. package/dist/lib/task/control-recovery.js +140 -0
  88. package/dist/lib/task/create-service.js +1 -8
  89. package/dist/lib/task/create.js +62 -9
  90. package/dist/lib/task/decision-details.js +2 -46
  91. package/dist/lib/task/delegation-receipts.js +64 -3
  92. package/dist/lib/task/events.js +275 -22
  93. package/dist/lib/task/finalization-state.js +9 -0
  94. package/dist/lib/task/human-override.js +8 -14
  95. package/dist/lib/task/implementation-inputs.js +3 -3
  96. package/dist/lib/task/intermediate-cleanup.js +441 -0
  97. package/dist/lib/task/invalidation.js +18 -65
  98. package/dist/lib/task/ledger-intents.js +7 -22
  99. package/dist/lib/task/ledger.js +1 -3
  100. package/dist/lib/task/lifecycle-recovery.js +549 -0
  101. package/dist/lib/task/lifecycle.js +31 -4
  102. package/dist/lib/task/local-artifact-finalization.js +197 -197
  103. package/dist/lib/task/manual-validation-completion.js +26 -0
  104. package/dist/lib/task/manual-validation-evidence.js +130 -0
  105. package/dist/lib/task/manual-validation-receipt.js +105 -0
  106. package/dist/lib/task/manual-validation-shared.js +40 -0
  107. package/dist/lib/task/manual-validation-transaction.js +243 -0
  108. package/dist/lib/task/markdown.js +47 -0
  109. package/dist/lib/task/orchestration.js +66 -19
  110. package/dist/lib/task/pr-delivery-fact.js +12 -56
  111. package/dist/lib/task/qualification-audit.js +20 -34
  112. package/dist/lib/task/recovery-warning.js +19 -0
  113. package/dist/lib/task/review-artifacts.js +2 -18
  114. package/dist/lib/task/review-command.js +26 -0
  115. package/dist/lib/task/review-finalization.js +175 -216
  116. package/dist/lib/task/review-fingerprint.js +2 -2
  117. package/dist/lib/task/sections.js +34 -56
  118. package/dist/lib/task/short-id.js +15 -7
  119. package/dist/lib/task/task-execution-lock.js +1 -1
  120. package/dist/lib/task/verification-engine.js +119 -337
  121. package/dist/lib/task/verification.js +1 -1
  122. package/dist/lib/task/workflow-command.js +16 -0
  123. package/dist/lib/task/workflow-warning-intents.js +0 -16
  124. package/dist/lib/task/workflow-warnings.js +28 -2
  125. package/dist/lib/update.js +2 -0
  126. package/dist/lib/version.js +1 -11
  127. package/dist/lifecycle-build-manifest.json +3 -3
  128. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  129. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +10 -4
  130. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  131. package/lib/fs/durable-write.ts +30 -0
  132. package/lib/fs/file-lock.ts +113 -0
  133. package/lib/host-control/audit.ts +22 -0
  134. package/lib/host-control/client.ts +172 -0
  135. package/lib/host-control/command.ts +60 -0
  136. package/lib/host-control/path.ts +167 -0
  137. package/lib/host-control/secure-fs.ts +135 -0
  138. package/lib/host-control/server.ts +180 -0
  139. package/lib/host-control/service.ts +133 -0
  140. package/lib/init.ts +15 -2
  141. package/lib/internal/cli-route-inventory.ts +4 -2
  142. package/lib/internal/codex-lifecycle.ts +6 -6
  143. package/lib/internal/demo-promotion.ts +354 -0
  144. package/lib/internal/demo-transcript.ts +419 -0
  145. package/lib/internal/host-control.ts +28 -0
  146. package/lib/internal/manual-validation.ts +315 -0
  147. package/lib/internal/platform-pr.ts +15 -4
  148. package/lib/internal/release-workflow.ts +74 -82
  149. package/lib/internal/sandbox-control.ts +162 -1
  150. package/lib/internal/task-artifact.ts +14 -217
  151. package/lib/internal/task-create.ts +5 -5
  152. package/lib/internal/task-event.ts +20 -11
  153. package/lib/internal/task-lifecycle.ts +4 -3
  154. package/lib/internal/task-operation-registry.ts +111 -4
  155. package/lib/internal/task-review.ts +9 -54
  156. package/lib/internal/task-validate.ts +26 -10
  157. package/lib/owned-path.ts +26 -0
  158. package/lib/platform/check-status.ts +9 -0
  159. package/lib/platform/comment-safety.ts +5 -0
  160. package/lib/platform/completion-backfill.ts +1 -1
  161. package/lib/platform/github-data.ts +322 -0
  162. package/lib/platform/github-provider.ts +15 -20
  163. package/lib/platform/issue-comments.ts +21 -14
  164. package/lib/platform/issues.ts +5 -246
  165. package/lib/platform/pr-change-report.ts +7 -5
  166. package/lib/platform/pr-checks.ts +13 -77
  167. package/lib/platform/pr-summary.ts +123 -35
  168. package/lib/platform/pull-requests.ts +6 -173
  169. package/lib/platform/snapshots.ts +53 -0
  170. package/lib/platform/task-identities.ts +3 -14
  171. package/lib/platform/verification-sync.ts +150 -156
  172. package/lib/pr-review/evidence-grading.ts +4 -9
  173. package/lib/release/workflow.ts +3 -3
  174. package/lib/sandbox/commands/create.ts +7 -6
  175. package/lib/sandbox/commands/rm.ts +21 -1895
  176. package/lib/sandbox/control/audit.ts +253 -0
  177. package/lib/sandbox/control/client.ts +109 -66
  178. package/lib/sandbox/control/executor.ts +134 -74
  179. package/lib/sandbox/control/finalization-response.ts +29 -0
  180. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  181. package/lib/sandbox/control/lifecycle.ts +45 -3
  182. package/lib/sandbox/control/native-file-lock.ts +6 -107
  183. package/lib/sandbox/control/protocol.ts +80 -6
  184. package/lib/sandbox/control/server.ts +567 -132
  185. package/lib/sandbox/control/state.ts +86 -0
  186. package/lib/sandbox/control/task-workflow.ts +95 -0
  187. package/lib/sandbox/control/workflow-executor.ts +92 -0
  188. package/lib/sandbox/index.ts +1 -2
  189. package/lib/sandbox/recovery.ts +28 -5
  190. package/lib/sandbox/removal.ts +1142 -0
  191. package/lib/sandbox/task-cleanup.ts +209 -0
  192. package/lib/sandbox/workspace-identity.ts +19 -3
  193. package/lib/sandbox/workspace-view.ts +17 -15
  194. package/lib/task/activity-intent.ts +2 -2
  195. package/lib/task/activity-log.ts +27 -19
  196. package/lib/task/artifact-command.ts +85 -0
  197. package/lib/task/artifact-lifecycle.ts +31 -26
  198. package/lib/task/artifact-name.ts +55 -0
  199. package/lib/task/artifact-operations.ts +34 -310
  200. package/lib/task/artifact-receipts.ts +3 -14
  201. package/lib/task/artifact-recovery.ts +519 -0
  202. package/lib/task/artifact-repair-intent.ts +162 -33
  203. package/lib/task/artifact-schema.ts +5 -34
  204. package/lib/task/capabilities.ts +13 -22
  205. package/lib/task/codex-orchestration.ts +85 -24
  206. package/lib/task/commit-identity.ts +31 -0
  207. package/lib/task/commit-operation.ts +37 -20
  208. package/lib/task/control-authority.ts +540 -10
  209. package/lib/task/control-recovery.ts +186 -0
  210. package/lib/task/create-service.ts +1 -10
  211. package/lib/task/create.ts +63 -8
  212. package/lib/task/decision-details.ts +3 -71
  213. package/lib/task/delegation-receipts.ts +73 -2
  214. package/lib/task/events.ts +309 -24
  215. package/lib/task/finalization-state.ts +11 -0
  216. package/lib/task/human-override.ts +8 -16
  217. package/lib/task/implementation-inputs.ts +3 -3
  218. package/lib/task/intermediate-cleanup.ts +493 -0
  219. package/lib/task/invalidation.ts +18 -62
  220. package/lib/task/ledger-intents.ts +7 -24
  221. package/lib/task/ledger.ts +1 -3
  222. package/lib/task/lifecycle-recovery.ts +691 -0
  223. package/lib/task/lifecycle.ts +30 -5
  224. package/lib/task/local-artifact-finalization.ts +244 -234
  225. package/lib/task/manual-validation-completion.ts +40 -0
  226. package/lib/task/manual-validation-evidence.ts +209 -0
  227. package/lib/task/manual-validation-receipt.ts +156 -0
  228. package/lib/task/manual-validation-shared.ts +50 -0
  229. package/lib/task/manual-validation-transaction.ts +304 -0
  230. package/lib/task/markdown.ts +70 -0
  231. package/lib/task/orchestration.ts +84 -19
  232. package/lib/task/pr-delivery-fact.ts +15 -62
  233. package/lib/task/qualification-audit.ts +22 -33
  234. package/lib/task/recovery-warning.ts +21 -0
  235. package/lib/task/review-artifacts.ts +1 -20
  236. package/lib/task/review-command.ts +24 -0
  237. package/lib/task/review-finalization.ts +183 -292
  238. package/lib/task/review-fingerprint.ts +2 -2
  239. package/lib/task/sections.ts +33 -54
  240. package/lib/task/short-id.ts +27 -5
  241. package/lib/task/task-execution-lock.ts +1 -1
  242. package/lib/task/verification-engine.ts +113 -383
  243. package/lib/task/verification-types.ts +15 -0
  244. package/lib/task/verification.ts +1 -1
  245. package/lib/task/workflow-command.ts +17 -0
  246. package/lib/task/workflow-warning-intents.ts +0 -13
  247. package/lib/task/workflow-warnings.ts +21 -2
  248. package/lib/update.ts +6 -0
  249. package/lib/version.ts +1 -15
  250. package/package.json +6 -6
  251. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  252. package/templates/.agents/rules/create-issue.en.md +1 -1
  253. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  254. package/templates/.agents/rules/issue-sync.en.md +4 -2
  255. package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
  256. package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
  257. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
  258. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  259. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  260. package/templates/.agents/scripts/package.json +3 -0
  261. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  262. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  263. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  264. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  265. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  266. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  267. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  268. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  269. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  270. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  271. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  272. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
  273. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  274. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  275. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  276. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  277. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +33 -21
  278. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +33 -21
  279. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  280. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  281. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  282. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  283. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  284. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  285. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  286. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  287. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  288. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  289. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  290. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  291. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  292. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  293. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  294. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  295. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  296. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  297. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  298. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  299. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  300. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  301. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  302. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  303. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  304. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  305. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  306. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  307. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  308. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  309. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  310. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  311. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  312. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  313. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  314. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  315. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  316. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  317. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  318. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  319. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  320. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  321. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  322. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  323. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  324. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  325. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  326. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  327. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  328. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  329. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  330. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  331. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  332. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  333. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  334. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  335. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  336. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  337. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  338. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  339. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  340. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  341. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  342. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  343. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  344. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  345. package/templates/.agents/templates/task.en.md +1 -1
  346. package/templates/.agents/templates/task.zh-CN.md +1 -1
  347. package/templates/.claude/commands/import-issue.en.md +1 -1
  348. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  349. package/templates/.claude/commands/restore-task.en.md +1 -1
  350. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
  351. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  352. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  353. package/dist/lib/platform/adapters.js +0 -56
  354. package/lib/platform/adapters.ts +0 -197
@@ -0,0 +1,519 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { writeDurableFile } from '../fs/durable-write.ts';
6
+ import { readStableFileSync } from '../host-control/secure-fs.ts';
7
+ import { parseArtifactName } from './artifact-name.ts';
8
+ import { canonicalSemanticDigest, sha256Content } from './artifact-operations.ts';
9
+ import {
10
+ artifactRecoveryRoot,
11
+ readArtifactRecoveryIntent,
12
+ writeArtifactRecoveryIntent
13
+ } from './artifact-repair-intent.ts';
14
+ import type {
15
+ ArtifactRecoveryIntent,
16
+ ArtifactRecoveryPhase,
17
+ ArtifactRecoveryState
18
+ } from './artifact-repair-intent.ts';
19
+ import type { ArtifactSchemaFamily } from './artifact-schema.ts';
20
+ import { withTaskExecutionLock } from './task-execution-lock.ts';
21
+
22
+ const MAX_ARTIFACT_BYTES = 1024 * 1024;
23
+
24
+ export type ArtifactRecoveryTuple = Readonly<{
25
+ taskId: string;
26
+ family: ArtifactSchemaFamily;
27
+ artifact: string;
28
+ round: number;
29
+ requestId: string;
30
+ phase?: ArtifactRecoveryPhase | null;
31
+ authorityDigest?: string | null;
32
+ }>;
33
+
34
+ export type ArtifactRecoveryContext = Readonly<ArtifactRecoveryTuple & {
35
+ repoRoot: string;
36
+ taskDir: string;
37
+ recoveryId: string;
38
+ stagingId: string;
39
+ formalPath: string;
40
+ stagingPath: string;
41
+ baselinePath: string;
42
+ finalPath: string;
43
+ baselineSha256: string;
44
+ baselineSemanticDigest: string;
45
+ }>;
46
+
47
+ export type ArtifactRecoveryOptions = Readonly<{
48
+ repoRoot: string;
49
+ taskDir: string;
50
+ recoveryId?: string;
51
+ lockAlreadyHeld?: boolean;
52
+ expectedFinalSha256?: string;
53
+ expectedFinalSemanticDigest?: string;
54
+ }>;
55
+
56
+ export type StagedArtifactCandidate = Readonly<{
57
+ recoveryId: string;
58
+ candidateSha256: string;
59
+ semanticDigest: string;
60
+ path: string;
61
+ }>;
62
+
63
+ export type ArtifactRecoveryReconcileResult = Readonly<{
64
+ status: ArtifactRecoveryState | 'indeterminate';
65
+ intent: ArtifactRecoveryIntent;
66
+ }>;
67
+
68
+ export class ArtifactRecoveryError extends Error {
69
+ readonly code: string;
70
+
71
+ constructor(code: string, message: string) {
72
+ super(`${code}: ${message}`);
73
+ this.name = 'ArtifactRecoveryError';
74
+ this.code = code;
75
+ }
76
+ }
77
+
78
+ function fail(code: string, message: string): never {
79
+ throw new ArtifactRecoveryError(code, message);
80
+ }
81
+
82
+ function runLocked<T>(
83
+ context: Pick<ArtifactRecoveryContext, 'repoRoot' | 'taskId'>,
84
+ owner: string,
85
+ callback: () => T,
86
+ lockAlreadyHeld = false
87
+ ): T {
88
+ return lockAlreadyHeld ? callback() : withTaskExecutionLock(context.repoRoot, context.taskId, owner, callback);
89
+ }
90
+
91
+ function assertTaskId(taskId: string): void {
92
+ if (!/^TASK-\d{8}-\d{6}$/u.test(taskId)) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'task id is invalid');
93
+ }
94
+
95
+ function assertDigest(value: string, name: string): void {
96
+ if (!/^[a-f0-9]{64}$/u.test(value)) fail('ARTIFACT_RECOVERY_DIGEST_INVALID', `${name} must be a lowercase SHA-256 digest`);
97
+ }
98
+
99
+ function assertRecoveryId(value: string): void {
100
+ if (!/^[a-f0-9-]{16,64}$/u.test(value)) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery id is invalid');
101
+ }
102
+
103
+ function assertTuple(tuple: ArtifactRecoveryTuple): void {
104
+ assertTaskId(tuple.taskId);
105
+ if (path.basename(tuple.artifact) !== tuple.artifact || !tuple.artifact.endsWith('.md')) {
106
+ fail('ARTIFACT_RECOVERY_TARGET_INVALID', 'artifact must be a canonical top-level Markdown file');
107
+ }
108
+ const parsed = parseArtifactName(tuple.artifact);
109
+ if (!parsed || parsed.family !== tuple.family || parsed.round !== tuple.round) {
110
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'artifact tuple does not match its canonical family and round');
111
+ }
112
+ if (!tuple.requestId || /[\r\n]/u.test(tuple.requestId)) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'request id is invalid');
113
+ if (!Number.isSafeInteger(tuple.round) || tuple.round < 1) fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'round is invalid');
114
+ }
115
+
116
+ function assertDirectory(directory: string, label: string): void {
117
+ let stat: fs.Stats;
118
+ try { stat = fs.lstatSync(directory); }
119
+ catch (error) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`); }
120
+ if (!stat!.isDirectory() || stat!.isSymbolicLink()) fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a real directory`);
121
+ }
122
+
123
+ function assertRegular(file: string, label: string): fs.Stats {
124
+ let stat: fs.Stats;
125
+ try { stat = fs.lstatSync(file); }
126
+ catch (error) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`); }
127
+ if (stat!.isSymbolicLink() || !stat!.isFile()) fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a regular file`);
128
+ return stat!;
129
+ }
130
+
131
+ function ensureOwnedDirectoryTree(rootInput: string, targetInput: string): void {
132
+ const root = path.resolve(rootInput);
133
+ const target = path.resolve(targetInput);
134
+ const relative = path.relative(root, target);
135
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
136
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must remain inside the repository');
137
+ }
138
+ assertDirectory(root, 'repository root');
139
+ let current = root;
140
+ for (const segment of relative.split(path.sep)) {
141
+ current = path.join(current, segment);
142
+ let stat: fs.Stats;
143
+ try {
144
+ stat = fs.lstatSync(current);
145
+ } catch (error) {
146
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
147
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory is unavailable: ${String(error)}`);
148
+ }
149
+ try { fs.mkdirSync(current, { mode: 0o700 }); }
150
+ catch (mkdirError) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be created: ${String(mkdirError)}`); }
151
+ try { stat = fs.lstatSync(current); }
152
+ catch (lstatError) { fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be verified: ${String(lstatError)}`); }
153
+ }
154
+ if (stat!.isSymbolicLink() || !stat!.isDirectory()) fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory must be a real directory');
155
+ if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat!.uid !== process.getuid()) {
156
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory is not owned by the current user');
157
+ }
158
+ }
159
+ }
160
+
161
+ function assertRecoveryRoot(context: ArtifactRecoveryContext): void {
162
+ ensureOwnedDirectoryTree(context.repoRoot, path.dirname(context.stagingPath));
163
+ }
164
+
165
+ function unlinkIfPresent(file: string): void {
166
+ try {
167
+ fs.unlinkSync(file);
168
+ } catch (error) {
169
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
170
+ }
171
+ }
172
+
173
+ function writeBytes(target: string, bytes: Buffer): void {
174
+ if (bytes.length > MAX_ARTIFACT_BYTES) fail('ARTIFACT_RECOVERY_SIZE_LIMIT', 'candidate exceeds the bounded artifact size limit');
175
+ writeDurableFile(target, bytes.toString('utf8'), { mode: 0o600, replace: true });
176
+ }
177
+
178
+ function contextPaths(
179
+ tuple: ArtifactRecoveryTuple,
180
+ options: ArtifactRecoveryOptions,
181
+ recoveryId: string
182
+ ): ArtifactRecoveryContext {
183
+ const taskDir = path.resolve(options.taskDir);
184
+ const root = artifactRecoveryRoot(taskDir, recoveryId);
185
+ return {
186
+ ...tuple,
187
+ repoRoot: options.repoRoot,
188
+ taskDir,
189
+ recoveryId,
190
+ stagingId: recoveryId,
191
+ formalPath: path.join(taskDir, tuple.artifact),
192
+ stagingPath: path.join(root, 'candidate.md'),
193
+ baselinePath: path.join(root, 'baseline.md'),
194
+ finalPath: path.join(root, 'final.md'),
195
+ baselineSha256: '',
196
+ baselineSemanticDigest: ''
197
+ };
198
+ }
199
+
200
+ function readIntentForContext(context: ArtifactRecoveryContext): ArtifactRecoveryIntent {
201
+ const intent = readArtifactRecoveryIntent(context.repoRoot, context.taskId, context.family, context.artifact);
202
+ if (!intent) fail('ARTIFACT_RECOVERY_INTENT_MISSING', 'recovery journal is missing');
203
+ if (intent.recoveryOperationId !== context.recoveryId
204
+ || intent.stagingId !== context.stagingId
205
+ || intent.round !== context.round
206
+ || intent.requestId !== context.requestId) {
207
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery context does not match the journal');
208
+ }
209
+ return intent;
210
+ }
211
+
212
+ function contextWithIntent(context: ArtifactRecoveryContext, intent: ArtifactRecoveryIntent): ArtifactRecoveryContext {
213
+ return {
214
+ ...context,
215
+ phase: intent.phase,
216
+ authorityDigest: intent.authorityDigest,
217
+ baselineSha256: intent.baselineSha256,
218
+ baselineSemanticDigest: intent.baselineSemanticDigest
219
+ };
220
+ }
221
+
222
+ function createIntent(context: ArtifactRecoveryContext, now: number): ArtifactRecoveryIntent {
223
+ return {
224
+ version: 3,
225
+ taskId: context.taskId,
226
+ family: context.family,
227
+ artifact: context.artifact,
228
+ round: context.round,
229
+ state: 'awaiting-recovery',
230
+ baselineSha256: context.baselineSha256,
231
+ baselineSemanticDigest: context.baselineSemanticDigest,
232
+ stagingId: context.stagingId,
233
+ candidateSha256: context.baselineSha256,
234
+ finalArtifactSha256: null,
235
+ finalSemanticDigest: null,
236
+ recoveryOperationId: context.recoveryId,
237
+ phase: context.phase ?? null,
238
+ authorityDigest: context.authorityDigest ?? null,
239
+ requestId: context.requestId,
240
+ errorCode: null,
241
+ errorMessage: null,
242
+ createdAt: now,
243
+ updatedAt: now
244
+ };
245
+ }
246
+
247
+ export function beginArtifactRecovery(
248
+ tuple: ArtifactRecoveryTuple,
249
+ baselineBytes: Buffer,
250
+ options: ArtifactRecoveryOptions
251
+ ): ArtifactRecoveryContext {
252
+ assertTuple(tuple);
253
+ assertDirectory(options.taskDir, 'task directory');
254
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
255
+ assertRecoveryId(context.recoveryId);
256
+ const targetStat = assertRegular(context.formalPath, 'formal artifact');
257
+ const rootParent = path.dirname(context.stagingPath);
258
+ ensureOwnedDirectoryTree(options.repoRoot, rootParent);
259
+ const stagingStat = fs.lstatSync(rootParent);
260
+ if (stagingStat.dev !== targetStat.dev) fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must share the formal artifact file system');
261
+ const baselineSha256 = sha256Content(baselineBytes.toString('utf8'));
262
+ const prepared = { ...context, baselineSha256, baselineSemanticDigest: canonicalSemanticDigest(baselineBytes.toString('utf8')) };
263
+ return runLocked(prepared, 'task-artifact.recovery.begin', () => {
264
+ assertRecoveryRoot(prepared);
265
+ const current = readStableFileSync(prepared.formalPath, { maxBytes: MAX_ARTIFACT_BYTES }).bytes;
266
+ if (sha256Content(current.toString('utf8')) !== baselineSha256 || !current.equals(baselineBytes)) {
267
+ fail('ARTIFACT_RECOVERY_BASELINE_MISMATCH', 'formal artifact does not match the recovery baseline');
268
+ }
269
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
270
+ if (existing && existing.state !== 'aborted' && existing.state !== 'consumed') {
271
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
272
+ }
273
+ writeBytes(prepared.baselinePath, baselineBytes);
274
+ writeBytes(prepared.stagingPath, baselineBytes);
275
+ writeArtifactRecoveryIntent(prepared.repoRoot, createIntent(prepared, Date.now()), { expected: existing });
276
+ return prepared;
277
+ }, options.lockAlreadyHeld);
278
+ }
279
+
280
+ export function recordArtifactRecoveryPassed(
281
+ tuple: ArtifactRecoveryTuple,
282
+ options: ArtifactRecoveryOptions
283
+ ): ArtifactRecoveryContext {
284
+ assertTuple(tuple);
285
+ assertDirectory(options.taskDir, 'task directory');
286
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
287
+ assertRecoveryId(context.recoveryId);
288
+ assertRegular(context.formalPath, 'formal artifact');
289
+ return runLocked(context, 'task-artifact.recovery.record-passed', () => {
290
+ const current = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
291
+ const prepared = {
292
+ ...context,
293
+ baselineSha256: current.sha256,
294
+ baselineSemanticDigest: canonicalSemanticDigest(current.bytes.toString('utf8'))
295
+ };
296
+ if ((options.expectedFinalSha256 !== undefined) !== (options.expectedFinalSemanticDigest !== undefined)) {
297
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'fast-path provenance requires both finalizer digests');
298
+ }
299
+ if (options.expectedFinalSha256 !== undefined && options.expectedFinalSemanticDigest !== undefined
300
+ && (current.sha256 !== options.expectedFinalSha256 || prepared.baselineSemanticDigest !== options.expectedFinalSemanticDigest)) {
301
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'formal artifact does not match the finalizer digests');
302
+ }
303
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
304
+ if (existing && (existing.state === 'passed' || existing.state === 'consumed')
305
+ && existing.finalArtifactSha256 === prepared.baselineSha256
306
+ && existing.finalSemanticDigest === prepared.baselineSemanticDigest) {
307
+ return recoveryContextFromIntent(prepared.repoRoot, prepared.taskDir, existing);
308
+ }
309
+ if (existing && existing.state !== 'aborted') {
310
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
311
+ }
312
+ const now = Date.now();
313
+ const next: ArtifactRecoveryIntent = {
314
+ ...createIntent(prepared, now),
315
+ state: 'passed',
316
+ candidateSha256: prepared.baselineSha256,
317
+ finalArtifactSha256: prepared.baselineSha256,
318
+ finalSemanticDigest: prepared.baselineSemanticDigest
319
+ };
320
+ writeArtifactRecoveryIntent(prepared.repoRoot, next, { expected: existing });
321
+ return prepared;
322
+ }, options.lockAlreadyHeld);
323
+ }
324
+
325
+ export function stageArtifactCandidate(
326
+ context: ArtifactRecoveryContext,
327
+ candidateBytes: Buffer,
328
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
329
+ ): StagedArtifactCandidate {
330
+ return runLocked(context, 'task-artifact.recovery.stage', () => {
331
+ assertRecoveryRoot(context);
332
+ const intent = readIntentForContext(context);
333
+ if (intent.state !== 'awaiting-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
334
+ const candidateSha256 = sha256Content(candidateBytes.toString('utf8'));
335
+ const semanticDigest = canonicalSemanticDigest(candidateBytes.toString('utf8'));
336
+ writeBytes(context.stagingPath, candidateBytes);
337
+ const next = { ...intent, candidateSha256, updatedAt: Date.now() };
338
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
339
+ return { recoveryId: context.recoveryId, candidateSha256, semanticDigest, path: context.stagingPath };
340
+ }, options.lockAlreadyHeld);
341
+ }
342
+
343
+ export function prepareArtifactRecoveryCommit(
344
+ context: ArtifactRecoveryContext,
345
+ finalSha256: string,
346
+ finalSemanticDigest: string,
347
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
348
+ ): ArtifactRecoveryIntent {
349
+ assertDigest(finalSha256, 'final artifact digest');
350
+ assertDigest(finalSemanticDigest, 'final semantic digest');
351
+ return runLocked(context, 'task-artifact.recovery.prepare', () => {
352
+ const intent = readIntentForContext(context);
353
+ if (intent.state !== 'awaiting-recovery') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a commit from '${intent.state}'`);
354
+ assertRecoveryRoot(context);
355
+ const staged = readStableFileSync(context.stagingPath, { maxBytes: MAX_ARTIFACT_BYTES });
356
+ if (staged.sha256 !== finalSha256 || canonicalSemanticDigest(staged.bytes.toString('utf8')) !== finalSemanticDigest) {
357
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'staged candidate does not match the finalizer digest');
358
+ }
359
+ writeBytes(context.finalPath, staged.bytes);
360
+ fs.chmodSync(context.finalPath, 0o400);
361
+ const sealed = readStableFileSync(context.finalPath, { maxBytes: MAX_ARTIFACT_BYTES });
362
+ if (sealed.sha256 !== finalSha256 || canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== finalSemanticDigest) {
363
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'sealed candidate does not match the finalizer digest');
364
+ }
365
+ const next: ArtifactRecoveryIntent = {
366
+ ...intent,
367
+ state: 'finalize-ready',
368
+ candidateSha256: staged.sha256,
369
+ finalArtifactSha256: finalSha256,
370
+ finalSemanticDigest,
371
+ errorCode: null,
372
+ errorMessage: null,
373
+ updatedAt: Date.now()
374
+ };
375
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
376
+ return next;
377
+ }, options.lockAlreadyHeld);
378
+ }
379
+
380
+ function markCommitStarted(context: ArtifactRecoveryContext, intent: ArtifactRecoveryIntent): ArtifactRecoveryIntent {
381
+ if (intent.state === 'commit-started') return intent;
382
+ if (intent.state !== 'finalize-ready') fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot commit from '${intent.state}'`);
383
+ const next: ArtifactRecoveryIntent = { ...intent, state: 'commit-started', updatedAt: Date.now() };
384
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
385
+ return next;
386
+ }
387
+
388
+ export function commitArtifactRecovery(
389
+ context: ArtifactRecoveryContext,
390
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
391
+ ): ArtifactRecoveryIntent {
392
+ return runLocked(context, 'task-artifact.recovery.commit', () => {
393
+ let intent = readIntentForContext(context);
394
+ if (intent.state === 'passed' || intent.state === 'consumed') return intent;
395
+ assertRecoveryRoot(context);
396
+ intent = markCommitStarted(context, intent);
397
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
398
+ if (target.sha256 === intent.finalArtifactSha256) {
399
+ const passed: ArtifactRecoveryIntent = { ...intent, state: 'passed', updatedAt: Date.now() };
400
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
401
+ return passed;
402
+ }
403
+ if (target.sha256 !== intent.baselineSha256) fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact changed outside the recovery baseline');
404
+ const sealed = readStableFileSync(context.finalPath, {
405
+ maxBytes: MAX_ARTIFACT_BYTES,
406
+ expectedSha256: intent.finalArtifactSha256!
407
+ });
408
+ if (canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
409
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'sealed artifact does not match the staged final semantic digest');
410
+ }
411
+ // Publish the bytes that were validated above. The sealed path is only a
412
+ // recovery input; renaming it directly would reopen a same-uid path swap
413
+ // between validation and publication.
414
+ const publishPath = path.join(path.dirname(context.finalPath), 'publish.md');
415
+ unlinkIfPresent(publishPath);
416
+ writeBytes(publishPath, sealed.bytes);
417
+ fs.chmodSync(publishPath, 0o400);
418
+ const publish = readStableFileSync(publishPath, {
419
+ maxBytes: MAX_ARTIFACT_BYTES,
420
+ expectedSha256: intent.finalArtifactSha256!
421
+ });
422
+ if (canonicalSemanticDigest(publish.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
423
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'publish artifact does not match the staged final semantic digest');
424
+ }
425
+ fs.renameSync(publishPath, context.formalPath);
426
+ const published = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
427
+ if (published.sha256 !== intent.finalArtifactSha256) fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact does not match the staged final digest');
428
+ const passed: ArtifactRecoveryIntent = { ...intent, state: 'passed', updatedAt: Date.now() };
429
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
430
+ return passed;
431
+ }, options.lockAlreadyHeld);
432
+ }
433
+
434
+ export function abortArtifactRecovery(
435
+ context: ArtifactRecoveryContext,
436
+ code: string,
437
+ message: string,
438
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
439
+ ): ArtifactRecoveryIntent {
440
+ return runLocked(context, 'task-artifact.recovery.abort', () => {
441
+ const intent = readIntentForContext(context);
442
+ assertRegular(context.baselinePath, 'baseline backup');
443
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
444
+ if (target.sha256 !== intent.baselineSha256) fs.renameSync(context.baselinePath, context.formalPath);
445
+ const restored = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
446
+ if (restored.sha256 !== intent.baselineSha256) fail('ARTIFACT_RECOVERY_INDETERMINATE', 'baseline restore could not be verified');
447
+ const aborted: ArtifactRecoveryIntent = {
448
+ ...intent,
449
+ state: 'aborted',
450
+ errorCode: code,
451
+ errorMessage: message,
452
+ updatedAt: Date.now()
453
+ };
454
+ writeArtifactRecoveryIntent(context.repoRoot, aborted, { expected: intent });
455
+ return aborted;
456
+ }, options.lockAlreadyHeld);
457
+ }
458
+
459
+ export function reconcileArtifactRecovery(
460
+ context: ArtifactRecoveryContext,
461
+ options: Readonly<{
462
+ lockAlreadyHeld?: boolean;
463
+ validateFinal?: () => boolean;
464
+ restoreBaseline?: boolean;
465
+ }> = {}
466
+ ): ArtifactRecoveryReconcileResult {
467
+ return runLocked(context, 'task-artifact.recovery.reconcile', () => {
468
+ const intent = readIntentForContext(context);
469
+ if (intent.state !== 'commit-started') return { status: intent.state, intent };
470
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
471
+ if (target.sha256 === intent.finalArtifactSha256 && (options.validateFinal?.() ?? true)) {
472
+ const passed: ArtifactRecoveryIntent = { ...intent, state: 'passed', updatedAt: Date.now() };
473
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
474
+ return { status: 'passed', intent: passed };
475
+ }
476
+ if (target.sha256 === intent.baselineSha256) {
477
+ const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true });
478
+ return { status: retried.state, intent: retried };
479
+ }
480
+ if (options.restoreBaseline) {
481
+ const aborted = abortArtifactRecovery(context, 'ARTIFACT_RECOVERY_CONTEXT_INVALID', 'formal target is not a verifiable baseline or final candidate', { lockAlreadyHeld: true });
482
+ return { status: 'aborted', intent: aborted };
483
+ }
484
+ return { status: 'indeterminate', intent };
485
+ }, options.lockAlreadyHeld);
486
+ }
487
+
488
+ export function consumeArtifactRecovery(
489
+ context: ArtifactRecoveryContext,
490
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
491
+ ): ArtifactRecoveryIntent {
492
+ return runLocked(context, 'task-artifact.recovery.consume', () => {
493
+ const intent = readIntentForContext(context);
494
+ if (intent.state === 'consumed') return intent;
495
+ if (intent.state !== 'passed') fail('ARTIFACT_RECOVERY_STATE_INVALID', `only passed recovery can be consumed, got '${intent.state}'`);
496
+ const consumed: ArtifactRecoveryIntent = { ...intent, state: 'consumed', updatedAt: Date.now() };
497
+ writeArtifactRecoveryIntent(context.repoRoot, consumed, { expected: intent });
498
+ return consumed;
499
+ }, options.lockAlreadyHeld);
500
+ }
501
+
502
+ export function recoveryContextFromIntent(
503
+ repoRoot: string,
504
+ taskDir: string,
505
+ intent: ArtifactRecoveryIntent
506
+ ): ArtifactRecoveryContext {
507
+ const context = contextPaths({
508
+ taskId: intent.taskId,
509
+ family: intent.family,
510
+ artifact: intent.artifact,
511
+ round: intent.round,
512
+ requestId: intent.requestId,
513
+ phase: intent.phase,
514
+ authorityDigest: intent.authorityDigest
515
+ }, { repoRoot, taskDir, recoveryId: intent.recoveryOperationId }, intent.recoveryOperationId);
516
+ return contextWithIntent(context, intent);
517
+ }
518
+
519
+ export { canonicalSemanticDigest, readArtifactRecoveryIntent, sha256Content };