@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,430 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { writeDurableFile } from '../fs/durable-write.js';
5
+ import { readStableFileSync } from '../host-control/secure-fs.js';
6
+ import { parseArtifactName } from './artifact-name.js';
7
+ import { canonicalSemanticDigest, sha256Content } from './artifact-operations.js';
8
+ import { artifactRecoveryRoot, readArtifactRecoveryIntent, writeArtifactRecoveryIntent } from './artifact-repair-intent.js';
9
+ import { withTaskExecutionLock } from './task-execution-lock.js';
10
+ const MAX_ARTIFACT_BYTES = 1024 * 1024;
11
+ export class ArtifactRecoveryError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(`${code}: ${message}`);
15
+ this.name = 'ArtifactRecoveryError';
16
+ this.code = code;
17
+ }
18
+ }
19
+ function fail(code, message) {
20
+ throw new ArtifactRecoveryError(code, message);
21
+ }
22
+ function runLocked(context, owner, callback, lockAlreadyHeld = false) {
23
+ return lockAlreadyHeld ? callback() : withTaskExecutionLock(context.repoRoot, context.taskId, owner, callback);
24
+ }
25
+ function assertTaskId(taskId) {
26
+ if (!/^TASK-\d{8}-\d{6}$/u.test(taskId))
27
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'task id is invalid');
28
+ }
29
+ function assertDigest(value, name) {
30
+ if (!/^[a-f0-9]{64}$/u.test(value))
31
+ fail('ARTIFACT_RECOVERY_DIGEST_INVALID', `${name} must be a lowercase SHA-256 digest`);
32
+ }
33
+ function assertRecoveryId(value) {
34
+ if (!/^[a-f0-9-]{16,64}$/u.test(value))
35
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery id is invalid');
36
+ }
37
+ function assertTuple(tuple) {
38
+ assertTaskId(tuple.taskId);
39
+ if (path.basename(tuple.artifact) !== tuple.artifact || !tuple.artifact.endsWith('.md')) {
40
+ fail('ARTIFACT_RECOVERY_TARGET_INVALID', 'artifact must be a canonical top-level Markdown file');
41
+ }
42
+ const parsed = parseArtifactName(tuple.artifact);
43
+ if (!parsed || parsed.family !== tuple.family || parsed.round !== tuple.round) {
44
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'artifact tuple does not match its canonical family and round');
45
+ }
46
+ if (!tuple.requestId || /[\r\n]/u.test(tuple.requestId))
47
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'request id is invalid');
48
+ if (!Number.isSafeInteger(tuple.round) || tuple.round < 1)
49
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'round is invalid');
50
+ }
51
+ function assertDirectory(directory, label) {
52
+ let stat;
53
+ try {
54
+ stat = fs.lstatSync(directory);
55
+ }
56
+ catch (error) {
57
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`);
58
+ }
59
+ if (!stat.isDirectory() || stat.isSymbolicLink())
60
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a real directory`);
61
+ }
62
+ function assertRegular(file, label) {
63
+ let stat;
64
+ try {
65
+ stat = fs.lstatSync(file);
66
+ }
67
+ catch (error) {
68
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} is unavailable: ${String(error)}`);
69
+ }
70
+ if (stat.isSymbolicLink() || !stat.isFile())
71
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `${label} must be a regular file`);
72
+ return stat;
73
+ }
74
+ function ensureOwnedDirectoryTree(rootInput, targetInput) {
75
+ const root = path.resolve(rootInput);
76
+ const target = path.resolve(targetInput);
77
+ const relative = path.relative(root, target);
78
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
79
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must remain inside the repository');
80
+ }
81
+ assertDirectory(root, 'repository root');
82
+ let current = root;
83
+ for (const segment of relative.split(path.sep)) {
84
+ current = path.join(current, segment);
85
+ let stat;
86
+ try {
87
+ stat = fs.lstatSync(current);
88
+ }
89
+ catch (error) {
90
+ if (error.code !== 'ENOENT') {
91
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory is unavailable: ${String(error)}`);
92
+ }
93
+ try {
94
+ fs.mkdirSync(current, { mode: 0o700 });
95
+ }
96
+ catch (mkdirError) {
97
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be created: ${String(mkdirError)}`);
98
+ }
99
+ try {
100
+ stat = fs.lstatSync(current);
101
+ }
102
+ catch (lstatError) {
103
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', `recovery directory could not be verified: ${String(lstatError)}`);
104
+ }
105
+ }
106
+ if (stat.isSymbolicLink() || !stat.isDirectory())
107
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory must be a real directory');
108
+ if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
109
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery directory is not owned by the current user');
110
+ }
111
+ }
112
+ }
113
+ function assertRecoveryRoot(context) {
114
+ ensureOwnedDirectoryTree(context.repoRoot, path.dirname(context.stagingPath));
115
+ }
116
+ function unlinkIfPresent(file) {
117
+ try {
118
+ fs.unlinkSync(file);
119
+ }
120
+ catch (error) {
121
+ if (error.code !== 'ENOENT')
122
+ throw error;
123
+ }
124
+ }
125
+ function writeBytes(target, bytes) {
126
+ if (bytes.length > MAX_ARTIFACT_BYTES)
127
+ fail('ARTIFACT_RECOVERY_SIZE_LIMIT', 'candidate exceeds the bounded artifact size limit');
128
+ writeDurableFile(target, bytes.toString('utf8'), { mode: 0o600, replace: true });
129
+ }
130
+ function contextPaths(tuple, options, recoveryId) {
131
+ const taskDir = path.resolve(options.taskDir);
132
+ const root = artifactRecoveryRoot(taskDir, recoveryId);
133
+ return {
134
+ ...tuple,
135
+ repoRoot: options.repoRoot,
136
+ taskDir,
137
+ recoveryId,
138
+ stagingId: recoveryId,
139
+ formalPath: path.join(taskDir, tuple.artifact),
140
+ stagingPath: path.join(root, 'candidate.md'),
141
+ baselinePath: path.join(root, 'baseline.md'),
142
+ finalPath: path.join(root, 'final.md'),
143
+ baselineSha256: '',
144
+ baselineSemanticDigest: ''
145
+ };
146
+ }
147
+ function readIntentForContext(context) {
148
+ const intent = readArtifactRecoveryIntent(context.repoRoot, context.taskId, context.family, context.artifact);
149
+ if (!intent)
150
+ fail('ARTIFACT_RECOVERY_INTENT_MISSING', 'recovery journal is missing');
151
+ if (intent.recoveryOperationId !== context.recoveryId
152
+ || intent.stagingId !== context.stagingId
153
+ || intent.round !== context.round
154
+ || intent.requestId !== context.requestId) {
155
+ fail('ARTIFACT_RECOVERY_IDENTITY_INVALID', 'recovery context does not match the journal');
156
+ }
157
+ return intent;
158
+ }
159
+ function contextWithIntent(context, intent) {
160
+ return {
161
+ ...context,
162
+ phase: intent.phase,
163
+ authorityDigest: intent.authorityDigest,
164
+ baselineSha256: intent.baselineSha256,
165
+ baselineSemanticDigest: intent.baselineSemanticDigest
166
+ };
167
+ }
168
+ function createIntent(context, now) {
169
+ return {
170
+ version: 3,
171
+ taskId: context.taskId,
172
+ family: context.family,
173
+ artifact: context.artifact,
174
+ round: context.round,
175
+ state: 'awaiting-recovery',
176
+ baselineSha256: context.baselineSha256,
177
+ baselineSemanticDigest: context.baselineSemanticDigest,
178
+ stagingId: context.stagingId,
179
+ candidateSha256: context.baselineSha256,
180
+ finalArtifactSha256: null,
181
+ finalSemanticDigest: null,
182
+ recoveryOperationId: context.recoveryId,
183
+ phase: context.phase ?? null,
184
+ authorityDigest: context.authorityDigest ?? null,
185
+ requestId: context.requestId,
186
+ errorCode: null,
187
+ errorMessage: null,
188
+ createdAt: now,
189
+ updatedAt: now
190
+ };
191
+ }
192
+ export function beginArtifactRecovery(tuple, baselineBytes, options) {
193
+ assertTuple(tuple);
194
+ assertDirectory(options.taskDir, 'task directory');
195
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
196
+ assertRecoveryId(context.recoveryId);
197
+ const targetStat = assertRegular(context.formalPath, 'formal artifact');
198
+ const rootParent = path.dirname(context.stagingPath);
199
+ ensureOwnedDirectoryTree(options.repoRoot, rootParent);
200
+ const stagingStat = fs.lstatSync(rootParent);
201
+ if (stagingStat.dev !== targetStat.dev)
202
+ fail('ARTIFACT_RECOVERY_PATH_INVALID', 'recovery staging must share the formal artifact file system');
203
+ const baselineSha256 = sha256Content(baselineBytes.toString('utf8'));
204
+ const prepared = { ...context, baselineSha256, baselineSemanticDigest: canonicalSemanticDigest(baselineBytes.toString('utf8')) };
205
+ return runLocked(prepared, 'task-artifact.recovery.begin', () => {
206
+ assertRecoveryRoot(prepared);
207
+ const current = readStableFileSync(prepared.formalPath, { maxBytes: MAX_ARTIFACT_BYTES }).bytes;
208
+ if (sha256Content(current.toString('utf8')) !== baselineSha256 || !current.equals(baselineBytes)) {
209
+ fail('ARTIFACT_RECOVERY_BASELINE_MISMATCH', 'formal artifact does not match the recovery baseline');
210
+ }
211
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
212
+ if (existing && existing.state !== 'aborted' && existing.state !== 'consumed') {
213
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
214
+ }
215
+ writeBytes(prepared.baselinePath, baselineBytes);
216
+ writeBytes(prepared.stagingPath, baselineBytes);
217
+ writeArtifactRecoveryIntent(prepared.repoRoot, createIntent(prepared, Date.now()), { expected: existing });
218
+ return prepared;
219
+ }, options.lockAlreadyHeld);
220
+ }
221
+ export function recordArtifactRecoveryPassed(tuple, options) {
222
+ assertTuple(tuple);
223
+ assertDirectory(options.taskDir, 'task directory');
224
+ const context = contextPaths(tuple, options, options.recoveryId ?? randomUUID());
225
+ assertRecoveryId(context.recoveryId);
226
+ assertRegular(context.formalPath, 'formal artifact');
227
+ return runLocked(context, 'task-artifact.recovery.record-passed', () => {
228
+ const current = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
229
+ const prepared = {
230
+ ...context,
231
+ baselineSha256: current.sha256,
232
+ baselineSemanticDigest: canonicalSemanticDigest(current.bytes.toString('utf8'))
233
+ };
234
+ if ((options.expectedFinalSha256 !== undefined) !== (options.expectedFinalSemanticDigest !== undefined)) {
235
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'fast-path provenance requires both finalizer digests');
236
+ }
237
+ if (options.expectedFinalSha256 !== undefined && options.expectedFinalSemanticDigest !== undefined
238
+ && (current.sha256 !== options.expectedFinalSha256 || prepared.baselineSemanticDigest !== options.expectedFinalSemanticDigest)) {
239
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'formal artifact does not match the finalizer digests');
240
+ }
241
+ const existing = readArtifactRecoveryIntent(prepared.repoRoot, prepared.taskId, prepared.family, prepared.artifact);
242
+ if (existing && (existing.state === 'passed' || existing.state === 'consumed')
243
+ && existing.finalArtifactSha256 === prepared.baselineSha256
244
+ && existing.finalSemanticDigest === prepared.baselineSemanticDigest) {
245
+ return recoveryContextFromIntent(prepared.repoRoot, prepared.taskDir, existing);
246
+ }
247
+ if (existing && existing.state !== 'aborted') {
248
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'an active recovery journal already exists for this artifact');
249
+ }
250
+ const now = Date.now();
251
+ const next = {
252
+ ...createIntent(prepared, now),
253
+ state: 'passed',
254
+ candidateSha256: prepared.baselineSha256,
255
+ finalArtifactSha256: prepared.baselineSha256,
256
+ finalSemanticDigest: prepared.baselineSemanticDigest
257
+ };
258
+ writeArtifactRecoveryIntent(prepared.repoRoot, next, { expected: existing });
259
+ return prepared;
260
+ }, options.lockAlreadyHeld);
261
+ }
262
+ export function stageArtifactCandidate(context, candidateBytes, options = {}) {
263
+ return runLocked(context, 'task-artifact.recovery.stage', () => {
264
+ assertRecoveryRoot(context);
265
+ const intent = readIntentForContext(context);
266
+ if (intent.state !== 'awaiting-recovery')
267
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot stage a candidate from '${intent.state}'`);
268
+ const candidateSha256 = sha256Content(candidateBytes.toString('utf8'));
269
+ const semanticDigest = canonicalSemanticDigest(candidateBytes.toString('utf8'));
270
+ writeBytes(context.stagingPath, candidateBytes);
271
+ const next = { ...intent, candidateSha256, updatedAt: Date.now() };
272
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
273
+ return { recoveryId: context.recoveryId, candidateSha256, semanticDigest, path: context.stagingPath };
274
+ }, options.lockAlreadyHeld);
275
+ }
276
+ export function prepareArtifactRecoveryCommit(context, finalSha256, finalSemanticDigest, options = {}) {
277
+ assertDigest(finalSha256, 'final artifact digest');
278
+ assertDigest(finalSemanticDigest, 'final semantic digest');
279
+ return runLocked(context, 'task-artifact.recovery.prepare', () => {
280
+ const intent = readIntentForContext(context);
281
+ if (intent.state !== 'awaiting-recovery')
282
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot prepare a commit from '${intent.state}'`);
283
+ assertRecoveryRoot(context);
284
+ const staged = readStableFileSync(context.stagingPath, { maxBytes: MAX_ARTIFACT_BYTES });
285
+ if (staged.sha256 !== finalSha256 || canonicalSemanticDigest(staged.bytes.toString('utf8')) !== finalSemanticDigest) {
286
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'staged candidate does not match the finalizer digest');
287
+ }
288
+ writeBytes(context.finalPath, staged.bytes);
289
+ fs.chmodSync(context.finalPath, 0o400);
290
+ const sealed = readStableFileSync(context.finalPath, { maxBytes: MAX_ARTIFACT_BYTES });
291
+ if (sealed.sha256 !== finalSha256 || canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== finalSemanticDigest) {
292
+ fail('ARTIFACT_RECOVERY_CANDIDATE_MISMATCH', 'sealed candidate does not match the finalizer digest');
293
+ }
294
+ const next = {
295
+ ...intent,
296
+ state: 'finalize-ready',
297
+ candidateSha256: staged.sha256,
298
+ finalArtifactSha256: finalSha256,
299
+ finalSemanticDigest,
300
+ errorCode: null,
301
+ errorMessage: null,
302
+ updatedAt: Date.now()
303
+ };
304
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
305
+ return next;
306
+ }, options.lockAlreadyHeld);
307
+ }
308
+ function markCommitStarted(context, intent) {
309
+ if (intent.state === 'commit-started')
310
+ return intent;
311
+ if (intent.state !== 'finalize-ready')
312
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `cannot commit from '${intent.state}'`);
313
+ const next = { ...intent, state: 'commit-started', updatedAt: Date.now() };
314
+ writeArtifactRecoveryIntent(context.repoRoot, next, { expected: intent });
315
+ return next;
316
+ }
317
+ export function commitArtifactRecovery(context, options = {}) {
318
+ return runLocked(context, 'task-artifact.recovery.commit', () => {
319
+ let intent = readIntentForContext(context);
320
+ if (intent.state === 'passed' || intent.state === 'consumed')
321
+ return intent;
322
+ assertRecoveryRoot(context);
323
+ intent = markCommitStarted(context, intent);
324
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
325
+ if (target.sha256 === intent.finalArtifactSha256) {
326
+ const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
327
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
328
+ return passed;
329
+ }
330
+ if (target.sha256 !== intent.baselineSha256)
331
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact changed outside the recovery baseline');
332
+ const sealed = readStableFileSync(context.finalPath, {
333
+ maxBytes: MAX_ARTIFACT_BYTES,
334
+ expectedSha256: intent.finalArtifactSha256
335
+ });
336
+ if (canonicalSemanticDigest(sealed.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
337
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'sealed artifact does not match the staged final semantic digest');
338
+ }
339
+ // Publish the bytes that were validated above. The sealed path is only a
340
+ // recovery input; renaming it directly would reopen a same-uid path swap
341
+ // between validation and publication.
342
+ const publishPath = path.join(path.dirname(context.finalPath), 'publish.md');
343
+ unlinkIfPresent(publishPath);
344
+ writeBytes(publishPath, sealed.bytes);
345
+ fs.chmodSync(publishPath, 0o400);
346
+ const publish = readStableFileSync(publishPath, {
347
+ maxBytes: MAX_ARTIFACT_BYTES,
348
+ expectedSha256: intent.finalArtifactSha256
349
+ });
350
+ if (canonicalSemanticDigest(publish.bytes.toString('utf8')) !== intent.finalSemanticDigest) {
351
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'publish artifact does not match the staged final semantic digest');
352
+ }
353
+ fs.renameSync(publishPath, context.formalPath);
354
+ const published = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
355
+ if (published.sha256 !== intent.finalArtifactSha256)
356
+ fail('ARTIFACT_RECOVERY_CONFLICT', 'formal artifact does not match the staged final digest');
357
+ const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
358
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
359
+ return passed;
360
+ }, options.lockAlreadyHeld);
361
+ }
362
+ export function abortArtifactRecovery(context, code, message, options = {}) {
363
+ return runLocked(context, 'task-artifact.recovery.abort', () => {
364
+ const intent = readIntentForContext(context);
365
+ assertRegular(context.baselinePath, 'baseline backup');
366
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
367
+ if (target.sha256 !== intent.baselineSha256)
368
+ fs.renameSync(context.baselinePath, context.formalPath);
369
+ const restored = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
370
+ if (restored.sha256 !== intent.baselineSha256)
371
+ fail('ARTIFACT_RECOVERY_INDETERMINATE', 'baseline restore could not be verified');
372
+ const aborted = {
373
+ ...intent,
374
+ state: 'aborted',
375
+ errorCode: code,
376
+ errorMessage: message,
377
+ updatedAt: Date.now()
378
+ };
379
+ writeArtifactRecoveryIntent(context.repoRoot, aborted, { expected: intent });
380
+ return aborted;
381
+ }, options.lockAlreadyHeld);
382
+ }
383
+ export function reconcileArtifactRecovery(context, options = {}) {
384
+ return runLocked(context, 'task-artifact.recovery.reconcile', () => {
385
+ const intent = readIntentForContext(context);
386
+ if (intent.state !== 'commit-started')
387
+ return { status: intent.state, intent };
388
+ const target = readStableFileSync(context.formalPath, { maxBytes: MAX_ARTIFACT_BYTES });
389
+ if (target.sha256 === intent.finalArtifactSha256 && (options.validateFinal?.() ?? true)) {
390
+ const passed = { ...intent, state: 'passed', updatedAt: Date.now() };
391
+ writeArtifactRecoveryIntent(context.repoRoot, passed, { expected: intent });
392
+ return { status: 'passed', intent: passed };
393
+ }
394
+ if (target.sha256 === intent.baselineSha256) {
395
+ const retried = commitArtifactRecovery(context, { lockAlreadyHeld: true });
396
+ return { status: retried.state, intent: retried };
397
+ }
398
+ if (options.restoreBaseline) {
399
+ const aborted = abortArtifactRecovery(context, 'ARTIFACT_RECOVERY_CONTEXT_INVALID', 'formal target is not a verifiable baseline or final candidate', { lockAlreadyHeld: true });
400
+ return { status: 'aborted', intent: aborted };
401
+ }
402
+ return { status: 'indeterminate', intent };
403
+ }, options.lockAlreadyHeld);
404
+ }
405
+ export function consumeArtifactRecovery(context, options = {}) {
406
+ return runLocked(context, 'task-artifact.recovery.consume', () => {
407
+ const intent = readIntentForContext(context);
408
+ if (intent.state === 'consumed')
409
+ return intent;
410
+ if (intent.state !== 'passed')
411
+ fail('ARTIFACT_RECOVERY_STATE_INVALID', `only passed recovery can be consumed, got '${intent.state}'`);
412
+ const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
413
+ writeArtifactRecoveryIntent(context.repoRoot, consumed, { expected: intent });
414
+ return consumed;
415
+ }, options.lockAlreadyHeld);
416
+ }
417
+ export function recoveryContextFromIntent(repoRoot, taskDir, intent) {
418
+ const context = contextPaths({
419
+ taskId: intent.taskId,
420
+ family: intent.family,
421
+ artifact: intent.artifact,
422
+ round: intent.round,
423
+ requestId: intent.requestId,
424
+ phase: intent.phase,
425
+ authorityDigest: intent.authorityDigest
426
+ }, { repoRoot, taskDir, recoveryId: intent.recoveryOperationId }, intent.recoveryOperationId);
427
+ return contextWithIntent(context, intent);
428
+ }
429
+ export { canonicalSemanticDigest, readArtifactRecoveryIntent, sha256Content };
430
+ //# sourceMappingURL=artifact-recovery.js.map
@@ -1,26 +1,81 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- function intentRoot(repoRoot) {
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { parseArtifactName } from './artifact-name.js';
5
+ export function artifactRecoveryIntentRoot(repoRoot) {
4
6
  return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
5
7
  }
8
+ export function artifactRecoveryRoot(taskDir, stagingId) {
9
+ return path.join(taskDir, '.local-artifact-recovery', stagingId);
10
+ }
6
11
  function intentPath(repoRoot, taskId, family, artifact) {
7
- return path.join(intentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
12
+ return path.join(artifactRecoveryIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
13
+ }
14
+ function validDigest(value) {
15
+ return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
16
+ }
17
+ function validTaskId(value) {
18
+ return typeof value === 'string' && /^TASK-\d{8}-\d{6}$/u.test(value);
19
+ }
20
+ function validToken(value) {
21
+ return typeof value === 'string' && value.length > 0 && !/[\r\n]/u.test(value);
22
+ }
23
+ function validRecoveryId(value) {
24
+ return typeof value === 'string' && /^[a-f0-9-]{16,64}$/u.test(value);
25
+ }
26
+ function validArtifact(value, family, round) {
27
+ if (typeof value !== 'string' || path.basename(value) !== value || !value.endsWith('.md'))
28
+ return false;
29
+ const parsed = parseArtifactName(value);
30
+ return Boolean(parsed && parsed.family === family && parsed.round === round);
8
31
  }
9
- function isArtifactRepairIntent(value) {
32
+ function exactKeys(value, keys) {
33
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
34
+ }
35
+ export function isArtifactRecoveryIntent(value) {
10
36
  if (!value || typeof value !== 'object' || Array.isArray(value))
11
37
  return false;
12
38
  const intent = value;
13
- return intent.version === 1
14
- && typeof intent.taskId === 'string' && /^TASK-\d{8}-\d{6}$/.test(intent.taskId)
15
- && typeof intent.family === 'string' && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
16
- && typeof intent.artifact === 'string' && intent.artifact.length > 0
17
- && ['awaiting-repair', 'passed', 'consumed'].includes(String(intent.state))
18
- && (intent.baselineSemanticDigest === null || (typeof intent.baselineSemanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.baselineSemanticDigest)))
19
- && typeof intent.artifactSha256 === 'string' && /^[a-f0-9]{64}$/.test(intent.artifactSha256)
20
- && typeof intent.semanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.semanticDigest)
21
- && (intent.state === 'awaiting-repair' ? intent.baselineSemanticDigest === intent.semanticDigest : true);
22
- }
23
- function readArtifactRepairIntent(repoRoot, taskId, family, artifact) {
39
+ return exactKeys(intent, [
40
+ 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
41
+ 'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
42
+ 'phase', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
43
+ 'version'
44
+ ])
45
+ && intent.version === 3
46
+ && validTaskId(intent.taskId)
47
+ && typeof intent.family === 'string'
48
+ && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
49
+ && Number.isSafeInteger(intent.round) && intent.round > 0
50
+ && validArtifact(intent.artifact, intent.family, intent.round)
51
+ && ['awaiting-recovery', 'finalize-ready', 'commit-started', 'passed', 'consumed', 'aborted'].includes(String(intent.state))
52
+ && validDigest(intent.baselineSha256)
53
+ && validDigest(intent.baselineSemanticDigest)
54
+ && validRecoveryId(intent.stagingId)
55
+ && (intent.candidateSha256 === null || validDigest(intent.candidateSha256))
56
+ && (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
57
+ && (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
58
+ && validRecoveryId(intent.recoveryOperationId)
59
+ && intent.stagingId === intent.recoveryOperationId
60
+ && (intent.phase === null || ['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(String(intent.phase)))
61
+ && (intent.authorityDigest === null || validDigest(intent.authorityDigest))
62
+ && validToken(intent.requestId)
63
+ && (intent.errorCode === null || validToken(intent.errorCode))
64
+ && (intent.errorMessage === null || validToken(intent.errorMessage))
65
+ && Number.isSafeInteger(intent.createdAt)
66
+ && Number.isSafeInteger(intent.updatedAt)
67
+ && (intent.state === 'awaiting-recovery'
68
+ ? intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
69
+ : true)
70
+ && (intent.state === 'finalize-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
71
+ ? validDigest(intent.candidateSha256) && validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
72
+ : true)
73
+ && (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
74
+ }
75
+ export function readArtifactRecoveryIntent(repoRoot, taskId, family, artifact) {
76
+ if (!validTaskId(taskId) || !validArtifact(artifact, family, parseArtifactName(artifact)?.round)) {
77
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal identity is invalid');
78
+ }
24
79
  const target = intentPath(repoRoot, taskId, family, artifact);
25
80
  if (!fs.existsSync(target))
26
81
  return null;
@@ -29,31 +84,81 @@ function readArtifactRepairIntent(repoRoot, taskId, family, artifact) {
29
84
  value = JSON.parse(fs.readFileSync(target, 'utf8'));
30
85
  }
31
86
  catch (error) {
32
- throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`);
87
+ throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`);
33
88
  }
34
- if (!isArtifactRepairIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
35
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
89
+ if (!isArtifactRecoveryIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
90
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
36
91
  }
37
92
  return value;
38
93
  }
39
- function writeArtifactRepairIntent(repoRoot, value) {
40
- if (!isArtifactRepairIntent(value))
41
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
42
- const directory = intentRoot(repoRoot);
94
+ export function writeArtifactRecoveryIntent(repoRoot, value, options = {}) {
95
+ if (!isArtifactRecoveryIntent(value))
96
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
97
+ const intent = value;
98
+ const directory = artifactRecoveryIntentRoot(repoRoot);
43
99
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
44
- const target = intentPath(repoRoot, value.taskId, value.family, value.artifact);
45
- const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
46
- fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
100
+ const target = intentPath(repoRoot, intent.taskId, intent.family, intent.artifact);
101
+ const lock = `${target}.lock`;
102
+ let descriptor;
47
103
  try {
48
- fs.renameSync(temporary, target);
104
+ descriptor = fs.openSync(lock, 'wx', 0o600);
49
105
  }
50
106
  catch (error) {
51
- try {
52
- fs.unlinkSync(temporary);
107
+ if (error.code === 'EEXIST') {
108
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: another journal writer owns the intent');
53
109
  }
54
- catch { /* preserve primary error */ }
55
110
  throw error;
56
111
  }
112
+ try {
113
+ const current = fs.existsSync(target)
114
+ ? readArtifactRecoveryIntent(repoRoot, intent.taskId, intent.family, intent.artifact)
115
+ : null;
116
+ if (!isDeepStrictEqual(current, options.expected ?? null)) {
117
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: recovery journal changed during finalization');
118
+ }
119
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
120
+ fs.writeFileSync(temporary, `${JSON.stringify(intent)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
121
+ try {
122
+ fs.renameSync(temporary, target);
123
+ }
124
+ catch (error) {
125
+ try {
126
+ fs.unlinkSync(temporary);
127
+ }
128
+ catch { /* preserve the primary error */ }
129
+ throw error;
130
+ }
131
+ }
132
+ finally {
133
+ fs.closeSync(descriptor);
134
+ try {
135
+ fs.unlinkSync(lock);
136
+ }
137
+ catch { /* preserve the primary result */ }
138
+ }
139
+ }
140
+ export function findArtifactRecoveryIntentsByOperation(repoRoot, operationId) {
141
+ if (!validToken(operationId))
142
+ throw new Error('ARTIFACT_RECOVERY_OPERATION_INVALID: recovery operation id is invalid');
143
+ const directory = artifactRecoveryIntentRoot(repoRoot);
144
+ if (!fs.existsSync(directory))
145
+ return Object.freeze([]);
146
+ return Object.freeze(fs.readdirSync(directory)
147
+ .filter((name) => name.endsWith('.json'))
148
+ .map((name) => {
149
+ const target = path.join(directory, name);
150
+ let value;
151
+ try {
152
+ value = JSON.parse(fs.readFileSync(target, 'utf8'));
153
+ }
154
+ catch (error) {
155
+ throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`);
156
+ }
157
+ if (!isArtifactRecoveryIntent(value))
158
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
159
+ return value;
160
+ })
161
+ .filter((value) => value.recoveryOperationId === operationId));
57
162
  }
58
- export { readArtifactRepairIntent, writeArtifactRepairIntent };
163
+ export { intentPath as artifactRecoveryIntentPath };
59
164
  //# sourceMappingURL=artifact-repair-intent.js.map
@@ -1,14 +1,4 @@
1
- const ARTIFACT_FAMILY_CATALOG = [
2
- { family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
3
- { family: 'review-analysis', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['需求分析审查', 'Analysis Review'] },
4
- { family: 'plan', sectionAliases: ['设计', 'Design'], heading: '设计', labels: ['技术方案', 'Technical Plan'] },
5
- { family: 'review-plan', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['技术方案审查', 'Plan Review'] },
6
- { family: 'code', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['实现报告', 'Implementation Report'] },
7
- { family: 'review-code', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['代码审查', 'Code Review'] },
8
- { family: 'manual-validation', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['人工验证', 'Manual Validation'] },
9
- { family: 'validation-run', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['验证运行证据', 'Validation Run Evidence'] },
10
- { family: 'pr-review', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['PR 审查报告', 'PR Review Report'] }
11
- ];
1
+ import { parseArtifactName } from './artifact-name.js';
12
2
  const LOCAL_SECTIONS = {
13
3
  analysis: [
14
4
  ['source', '需求来源', 'Requirements Source'],
@@ -114,8 +104,10 @@ function renderArtifactSkeleton(input) {
114
104
  if (!schema)
115
105
  throw new Error(`unknown artifact schema family '${input.family}'`);
116
106
  const locale = input.locale ?? 'zh-CN';
117
- const roundMatch = input.artifact.match(/-r(\d+)\.md$/);
118
- const round = roundMatch ? Number(roundMatch[1]) : 1;
107
+ const identity = parseArtifactName(input.artifact);
108
+ if (identity?.family !== input.family)
109
+ throw new Error(`invalid artifact identity '${input.artifact}' for '${input.family}'`);
110
+ const round = identity.round;
119
111
  const title = schema.title[locale === 'en' ? 'en' : 'zh'];
120
112
  const headings = locale === 'en' ? 'en' : 'zh';
121
113
  const lines = [
@@ -131,5 +123,5 @@ function renderArtifactSkeleton(input) {
131
123
  }
132
124
  return `${lines.join('\n').trimEnd()}\n`;
133
125
  }
134
- export { ARTIFACT_FAMILY_CATALOG, ARTIFACT_SCHEMAS, getArtifactSchema, renderArtifactSkeleton };
126
+ export { ARTIFACT_SCHEMAS, getArtifactSchema, renderArtifactSkeleton };
135
127
  //# sourceMappingURL=artifact-schema.js.map