@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,493 @@
1
+ import { inspectOwnedPath } from '../owned-path.ts';
2
+ import { taskFinalizationReceiptState } from './finalization-state.ts';
3
+ import { withTaskExecutionLock } from './task-execution-lock.ts';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ import { checkpointCommitMatches } from './commit-identity.ts';
8
+ import {
9
+ readCheckpointIntent,
10
+ type CheckpointIntent
11
+ } from './commit-intent.ts';
12
+ import { parseTaskFrontmatter } from './frontmatter.ts';
13
+ import {
14
+ readTaskFinalizationReceipt,
15
+ type TaskFinalizationReceipt
16
+ } from './finalization.ts';
17
+ import {
18
+ semanticDigest,
19
+ sha256Content
20
+ } from './local-artifact-finalization.ts';
21
+ import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
22
+ import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
23
+ import { parseArtifactName } from './artifact-name.ts';
24
+ import type { ArtifactSchemaFamily } from './artifact-schema.ts';
25
+ import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
26
+ import type { TaskControlBindingEvidence } from './finalization-state.ts';
27
+
28
+ const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
29
+ const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
30
+ const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
31
+ const AUXILIARY_ROOTS = [
32
+ '.local-artifact-finalization-intents',
33
+ '.task-commit-intents'
34
+ ] as const;
35
+
36
+ type CleanupKind = 'LFAI-CONSUMED' | 'COMMIT-SYNCED' | 'EMPTY-AUX-PARENT';
37
+ type CleanupDisposition = 'planned' | 'protected' | 'skipped' | 'deleted' | 'failed';
38
+ type FileIdentity = Readonly<{ dev: string; ino: string; size: number; mtimeMs: number }>;
39
+
40
+ type IntermediateCleanupItem = Readonly<{
41
+ kind: CleanupKind | 'AUXILIARY-UNKNOWN';
42
+ taskId: string | null;
43
+ path: string;
44
+ disposition: CleanupDisposition;
45
+ reason: string;
46
+ identity: FileIdentity | null;
47
+ }>;
48
+
49
+ type IntermediateCleanupReport = Readonly<{
50
+ status: 'dry-run' | 'completed' | 'partial';
51
+ items: readonly IntermediateCleanupItem[];
52
+ remaining: readonly IntermediateCleanupItem[];
53
+ }>;
54
+
55
+ type IntermediateCleanupOptions = Readonly<{
56
+ taskIds?: readonly string[];
57
+ controlBindingEvidence?: TaskControlBindingEvidence;
58
+ }>;
59
+
60
+ type ScanOptions = IntermediateCleanupOptions & Readonly<{ preflight?: boolean }>;
61
+
62
+ type TaskRecord = Readonly<{
63
+ taskId: string;
64
+ taskDir: string;
65
+ state: TaskWorkspaceState;
66
+ frontmatter: Record<string, string>;
67
+ }>;
68
+
69
+ function auxiliaryRoot(repoRoot: string, name: typeof AUXILIARY_ROOTS[number]): string {
70
+ return path.join(repoRoot, '.agents', 'workspace', name);
71
+ }
72
+
73
+ function taskRoot(repoRoot: string): string {
74
+ return path.join(repoRoot, '.agents', 'workspace');
75
+ }
76
+
77
+ function fileIdentity(stat: fs.Stats): FileIdentity {
78
+ return {
79
+ dev: String(stat.dev),
80
+ ino: String(stat.ino),
81
+ size: stat.size,
82
+ mtimeMs: stat.mtimeMs
83
+ };
84
+ }
85
+
86
+ function sameFileIdentity(left: FileIdentity, right: FileIdentity): boolean {
87
+ return left.dev === right.dev && left.ino === right.ino
88
+ && left.size === right.size && left.mtimeMs === right.mtimeMs;
89
+ }
90
+
91
+ function errorCode(error: unknown): string | null {
92
+ const code = (error as NodeJS.ErrnoException | null)?.code;
93
+ return typeof code === 'string' ? code : null;
94
+ }
95
+
96
+ function safeLstat(root: string, target: string, expect: 'file' | 'directory'): FileIdentity | null {
97
+ const stat = inspectOwnedPath(root, target, expect);
98
+ return stat ? fileIdentity(stat) : null;
99
+ }
100
+
101
+ function inspectTaskRecords(repoRoot: string, taskIds?: ReadonlySet<string>): Map<string, TaskRecord> {
102
+ const records = new Map<string, TaskRecord>();
103
+ for (const entry of enumerateAllTaskDirs(repoRoot)) {
104
+ if (taskIds && !taskIds.has(entry.taskId)) continue;
105
+ const taskMdPath = path.join(entry.taskDir, 'task.md');
106
+ if (safeLstat(taskRoot(repoRoot), taskMdPath, 'file') === null) continue;
107
+ const content = fs.readFileSync(taskMdPath, 'utf8');
108
+ records.set(entry.taskId, {
109
+ taskId: entry.taskId,
110
+ taskDir: entry.taskDir,
111
+ state: entry.state,
112
+ frontmatter: parseTaskFrontmatter(content)
113
+ });
114
+ }
115
+ return records;
116
+ }
117
+
118
+ function item(
119
+ kind: CleanupKind | 'AUXILIARY-UNKNOWN',
120
+ taskId: string | null,
121
+ filePath: string,
122
+ disposition: CleanupDisposition,
123
+ reason: string,
124
+ identity: FileIdentity | null = null
125
+ ): IntermediateCleanupItem {
126
+ return { kind, taskId, path: path.resolve(filePath), disposition, reason, identity };
127
+ }
128
+
129
+ function taskGate(task: TaskRecord | undefined): string | null {
130
+ if (!task) return 'TASK_NOT_FOUND';
131
+ if (task.state !== 'completed' && task.state !== 'archive') return 'TASK_STATE_PROTECTED';
132
+ if (task.frontmatter.id !== task.taskId || task.frontmatter.status !== 'completed') return 'TASK_STATE_PROTECTED';
133
+ if (!task.frontmatter.branch) return 'TASK_IDENTITY_INVALID';
134
+ return null;
135
+ }
136
+
137
+ function receiptGate(
138
+ repoRoot: string,
139
+ task: TaskRecord,
140
+ options: ScanOptions
141
+ ): string | null {
142
+ let receipt: TaskFinalizationReceipt | null;
143
+ try { receipt = readTaskFinalizationReceipt(repoRoot, task.taskId); }
144
+ catch { return 'FINALIZATION_RECEIPT_INVALID'; }
145
+ if (!receipt) return 'FINALIZATION_RECEIPT_MISSING';
146
+ const state = taskFinalizationReceiptState(receipt);
147
+ if (state === 'pending') return 'FINALIZATION_RECEIPT_PENDING';
148
+ if (state === 'unresolved') return 'LFAI_RETRY_OR_WARNING_OPEN';
149
+ if (receipt.controlBinding) {
150
+ const evidence = options.controlBindingEvidence?.(task.taskId, receipt.controlBinding);
151
+ if (evidence !== 'terminal' && !(options.preflight && evidence === 'pending')) return 'CONTROL_BINDING_MISMATCH';
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function canonicalLocalIntentPath(taskId: string, family: string, artifact: string): string {
157
+ return `${taskId}-${family}-${artifact}.json`;
158
+ }
159
+
160
+ function localIntentCandidate(
161
+ repoRoot: string,
162
+ task: TaskRecord | undefined,
163
+ filePath: string,
164
+ taskId: string,
165
+ family: ArtifactSchemaFamily,
166
+ artifact: string,
167
+ identity: FileIdentity,
168
+ options: ScanOptions
169
+ ): IntermediateCleanupItem {
170
+ const protectedReason = taskGate(task);
171
+ if (protectedReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
172
+ if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
173
+ || !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
174
+ return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
175
+ }
176
+ let intent: ArtifactRecoveryIntent | null;
177
+ try { intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact); }
178
+ catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
179
+ if (!intent || intent.state !== 'consumed') {
180
+ return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
181
+ }
182
+ const artifactPath = path.join(task!.taskDir, artifact);
183
+ if (safeLstat(task!.taskDir, artifactPath, 'file') === null) {
184
+ return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
185
+ }
186
+ let content: string;
187
+ try { content = fs.readFileSync(artifactPath, 'utf8'); }
188
+ catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
189
+ if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
190
+ return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
191
+ }
192
+ const receiptReason = receiptGate(repoRoot, task!, options);
193
+ if (receiptReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
194
+ return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
195
+ }
196
+
197
+ function commitIntentCandidate(
198
+ repoRoot: string,
199
+ task: TaskRecord | undefined,
200
+ filePath: string,
201
+ taskId: string,
202
+ identity: FileIdentity,
203
+ options: ScanOptions
204
+ ): IntermediateCleanupItem {
205
+ const protectedReason = taskGate(task);
206
+ if (protectedReason) return item('COMMIT-SYNCED', taskId, filePath, 'protected', protectedReason, identity);
207
+ let intent: CheckpointIntent | null;
208
+ try { intent = readCheckpointIntent(repoRoot, taskId); }
209
+ catch { return item('COMMIT-SYNCED', taskId, filePath, 'protected', 'COMMIT_INTENT_INVALID', identity); }
210
+ if (!intent || intent.state !== 'synced' || intent.taskId !== taskId
211
+ || intent.branch !== task!.frontmatter.branch || !intent.committedHead
212
+ || task!.frontmatter.checkpoint_commit !== intent.committedHead
213
+ || !checkpointCommitMatches(repoRoot, intent, intent.committedHead)) {
214
+ return item('COMMIT-SYNCED', taskId, filePath, 'protected', 'COMMIT_EVIDENCE_MISMATCH', identity);
215
+ }
216
+ const receiptReason = receiptGate(repoRoot, task!, options);
217
+ if (receiptReason) return item('COMMIT-SYNCED', taskId, filePath, 'protected', receiptReason, identity);
218
+ return item('COMMIT-SYNCED', taskId, filePath, 'planned', 'COMMIT_SYNCED_AND_VERIFIED', identity);
219
+ }
220
+
221
+ function readAuxiliaryCandidates(
222
+ repoRoot: string,
223
+ tasks: Map<string, TaskRecord>,
224
+ options: ScanOptions
225
+ ): IntermediateCleanupItem[] {
226
+ const candidates: IntermediateCleanupItem[] = [];
227
+ const selected = options.taskIds ? new Set(options.taskIds) : null;
228
+ for (const rootName of AUXILIARY_ROOTS) {
229
+ const root = auxiliaryRoot(repoRoot, rootName);
230
+ let rootIdentity: FileIdentity | null;
231
+ try { rootIdentity = safeLstat(taskRoot(repoRoot), root, 'directory'); }
232
+ catch { rootIdentity = null; }
233
+ if (rootIdentity === null) {
234
+ let stat: fs.Stats | null = null;
235
+ try { stat = fs.lstatSync(root); } catch { stat = null; }
236
+ if (stat?.isSymbolicLink()) candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'PATH_SYMLINK'));
237
+ else if (stat) candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'PATH_IDENTITY_MISMATCH'));
238
+ continue;
239
+ }
240
+ let entries: fs.Dirent[];
241
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); }
242
+ catch {
243
+ candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'AUXILIARY_ROOT_UNREADABLE', rootIdentity));
244
+ continue;
245
+ }
246
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
247
+ const filePath = path.join(root, entry.name);
248
+ if (entry.isSymbolicLink()) {
249
+ candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_SYMLINK'));
250
+ continue;
251
+ }
252
+ if (!entry.isFile()) {
253
+ candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
254
+ continue;
255
+ }
256
+ const identity = safeLstat(root, filePath, 'file');
257
+ if (!identity) {
258
+ candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
259
+ continue;
260
+ }
261
+ if (rootName === '.local-artifact-finalization-intents') {
262
+ const match = LOCAL_INTENT_RE.exec(entry.name);
263
+ if (!match || (selected && !selected.has(match[1]!))) {
264
+ if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
265
+ continue;
266
+ }
267
+ candidates.push(localIntentCandidate(
268
+ repoRoot, tasks.get(match[1]!), filePath, match[1]!, match[2] as 'analysis' | 'plan' | 'code', match[3]!, identity, options
269
+ ));
270
+ } else {
271
+ const match = COMMIT_INTENT_RE.exec(entry.name);
272
+ if (!match || (selected && !selected.has(match[1]!))) {
273
+ if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
274
+ continue;
275
+ }
276
+ candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
277
+ }
278
+ }
279
+ if (entries.length === 0) {
280
+ candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
281
+ }
282
+ }
283
+ return candidates;
284
+ }
285
+
286
+ function buildIntermediateCleanupReport(items: readonly IntermediateCleanupItem[], dryRun = false, partial = false): IntermediateCleanupReport {
287
+ const remaining = items.filter((candidate) => candidate.disposition !== 'deleted' && candidate.disposition !== 'skipped');
288
+ return {
289
+ status: dryRun ? 'dry-run' : partial || remaining.some((candidate) => candidate.disposition === 'failed') ? 'partial' : 'completed',
290
+ items,
291
+ remaining
292
+ };
293
+ }
294
+
295
+ function scanInternal(repoRootInput: string, options: ScanOptions): IntermediateCleanupItem[] {
296
+ const repoRoot = path.resolve(repoRootInput);
297
+ if (safeLstat(path.dirname(repoRoot), repoRoot, 'directory') === null) {
298
+ throw new Error('INTERMEDIATE_CLEANUP_REPOSITORY_INVALID');
299
+ }
300
+ const selected = options.taskIds ? new Set(options.taskIds.filter((taskId) => TASK_ID_RE.test(taskId))) : undefined;
301
+ const tasks = inspectTaskRecords(repoRoot, selected);
302
+ return readAuxiliaryCandidates(repoRoot, tasks, options);
303
+ }
304
+
305
+ function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): IntermediateCleanupReport {
306
+ const candidates = scanInternal(repoRoot, options);
307
+ return buildIntermediateCleanupReport(candidates, true);
308
+ }
309
+
310
+ function cleanupConsumedRecoveryFiles(repoRoot: string, task: TaskRecord | undefined, candidate: IntermediateCleanupItem): boolean {
311
+ if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId) return true;
312
+ if (!task) return false;
313
+ const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
314
+ if (!match) return false;
315
+ let intent: ReturnType<typeof readArtifactRecoveryIntent>;
316
+ try { intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2] as ArtifactSchemaFamily, match[3]!); }
317
+ catch { return false; }
318
+ if (!intent || intent.state !== 'consumed') return false;
319
+
320
+ const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
321
+ let stat: fs.Stats | null = null;
322
+ try { stat = fs.lstatSync(recovery); } catch (error) {
323
+ if (errorCode(error) === 'ENOENT') return true;
324
+ return false;
325
+ }
326
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
327
+ let entries: fs.Dirent[];
328
+ try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
329
+ for (const entry of entries) {
330
+ if (entry.isSymbolicLink() || !entry.isFile() || !['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
331
+ }
332
+ try {
333
+ for (const entry of entries) fs.unlinkSync(path.join(recovery, entry.name));
334
+ fs.rmdirSync(recovery);
335
+ const recoveryRoot = path.dirname(recovery);
336
+ if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
337
+ return true;
338
+ } catch (error) {
339
+ return errorCode(error) === 'ENOENT';
340
+ }
341
+ }
342
+
343
+ function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
344
+ if (candidate.disposition !== 'planned') return candidate;
345
+ const root = candidate.kind === 'EMPTY-AUX-PARENT'
346
+ ? path.dirname(path.dirname(candidate.path))
347
+ : path.dirname(candidate.path);
348
+ const expected = candidate.identity;
349
+ if (!expected) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_MISSING' };
350
+ let raw: fs.Stats;
351
+ try { raw = fs.lstatSync(candidate.path); }
352
+ catch (error) {
353
+ if (errorCode(error) === 'ENOENT') return { ...candidate, disposition: 'skipped', reason: 'ALREADY_GONE' };
354
+ return { ...candidate, disposition: 'failed', reason: 'CLEANUP_RECHECK_FAILED' };
355
+ }
356
+ if (raw.isSymbolicLink()) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
357
+ const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
358
+ if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
359
+ if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
360
+ if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
361
+ return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
362
+ }
363
+ try {
364
+ if (candidate.kind === 'EMPTY-AUX-PARENT') {
365
+ if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
366
+ fs.rmdirSync(candidate.path);
367
+ } else {
368
+ fs.unlinkSync(candidate.path);
369
+ }
370
+ return { ...candidate, disposition: 'deleted', reason: 'DELETED_AFTER_LOCKED_RECHECK' };
371
+ } catch (error) {
372
+ if (errorCode(error) === 'ENOENT') return { ...candidate, disposition: 'skipped', reason: 'ALREADY_GONE' };
373
+ return { ...candidate, disposition: 'failed', reason: 'CLEANUP_DELETE_FAILED' };
374
+ }
375
+ }
376
+
377
+
378
+ type IntermediateCleanupCoordinatorOptions = IntermediateCleanupOptions & Readonly<{
379
+ dryRun?: boolean;
380
+ lockedTaskIds?: ReadonlySet<string>;
381
+ }>;
382
+
383
+ function intermediateCleanupKey(item: IntermediateCleanupItem): string {
384
+ return `${item.kind}\0${item.path}`;
385
+ }
386
+
387
+ function cleanupIntermediateUnderRemovalCoordinator(
388
+ repoRoot: string,
389
+ options: IntermediateCleanupCoordinatorOptions = {}
390
+ ): IntermediateCleanupReport {
391
+ const scanOptions: IntermediateCleanupOptions = {
392
+ ...(options.taskIds === undefined ? {} : { taskIds: options.taskIds }),
393
+ ...(options.controlBindingEvidence === undefined ? {} : { controlBindingEvidence: options.controlBindingEvidence })
394
+ };
395
+ const initial = scanIntermediateCleanup(repoRoot, scanOptions);
396
+ if (options.dryRun) return initial;
397
+
398
+ const items = [...initial.items];
399
+ const lockedTaskIds = options.lockedTaskIds ?? new Set<string>();
400
+ const byTask = new Map<string, number[]>();
401
+ for (const [index, candidate] of items.entries()) {
402
+ if (!candidate.taskId || candidate.disposition !== 'planned') continue;
403
+ const indexes = byTask.get(candidate.taskId) ?? [];
404
+ indexes.push(index);
405
+ byTask.set(candidate.taskId, indexes);
406
+ }
407
+ const tasks = inspectTaskRecords(repoRoot, new Set(byTask.keys()));
408
+
409
+ for (const taskId of [...byTask.keys()].sort()) {
410
+ const execute = (): void => {
411
+ const refreshed = scanIntermediateCleanup(repoRoot, {
412
+ ...scanOptions,
413
+ taskIds: [taskId]
414
+ });
415
+ const refreshedByKey = new Map(refreshed.items.map((candidate) => [intermediateCleanupKey(candidate), candidate]));
416
+ for (const index of byTask.get(taskId) ?? []) {
417
+ const initialCandidate = items[index]!;
418
+ if (initialCandidate.disposition !== 'planned') continue;
419
+ const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
420
+ items[index] = current
421
+ ? removeCandidate(repoRoot, current, tasks.get(taskId))
422
+ : { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
423
+ }
424
+ };
425
+ try {
426
+ if (lockedTaskIds.has(taskId)) execute();
427
+ else withTaskExecutionLock(repoRoot, taskId, 'sandbox-removal', execute);
428
+ } catch {
429
+ for (const index of byTask.get(taskId) ?? []) {
430
+ const candidate = items[index]!;
431
+ if (candidate.disposition === 'planned') items[index] = { ...candidate, disposition: 'protected', reason: 'TASK_LOCK_BUSY' };
432
+ }
433
+ }
434
+ }
435
+
436
+ for (const [index, candidate] of items.entries()) {
437
+ if (candidate.taskId || candidate.disposition !== 'planned') continue;
438
+ items[index] = removeCandidate(repoRoot, candidate);
439
+ }
440
+
441
+ const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
442
+ for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
443
+ if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
444
+ || knownPaths.has(intermediateCleanupKey(candidate))) continue;
445
+ items.push(removeCandidate(repoRoot, candidate));
446
+ }
447
+
448
+ return buildIntermediateCleanupReport(items);
449
+ }
450
+
451
+ function protectIntermediateCleanupReport(
452
+ report: IntermediateCleanupReport,
453
+ reason: string
454
+ ): IntermediateCleanupReport {
455
+ const items = report.items.map((item) => item.disposition === 'planned'
456
+ ? { ...item, disposition: 'protected' as const, reason }
457
+ : item
458
+ );
459
+ return buildIntermediateCleanupReport(items, false, true);
460
+ }
461
+
462
+ function mergeIntermediateCleanupReports(
463
+ reports: readonly IntermediateCleanupReport[]
464
+ ): IntermediateCleanupReport {
465
+ const items = reports.flatMap((report) => report.items);
466
+ return buildIntermediateCleanupReport(items, false, reports.some((report) => report.status === 'partial'));
467
+ }
468
+
469
+ function formatIntermediateCleanupReport(report: IntermediateCleanupReport): string[] {
470
+ return [
471
+ `remaining ${report.remaining.length} item(s)`,
472
+ ...report.items.map((candidate) => (
473
+ `${candidate.disposition} ${candidate.kind} ${candidate.path}`
474
+ + ` (${candidate.taskId ?? 'unbound'}; ${candidate.reason})`
475
+ ))
476
+ ];
477
+ }
478
+
479
+ export {
480
+ formatIntermediateCleanupReport,
481
+ cleanupIntermediateUnderRemovalCoordinator,
482
+ protectIntermediateCleanupReport,
483
+ mergeIntermediateCleanupReports,
484
+ scanIntermediateCleanup
485
+ };
486
+ export type {
487
+ CleanupDisposition,
488
+ CleanupKind,
489
+ FileIdentity,
490
+ IntermediateCleanupItem,
491
+ IntermediateCleanupOptions,
492
+ IntermediateCleanupReport
493
+ };
@@ -1,4 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { findSectionRange, parseTable } from './sections.ts';
3
+ import { scanVisibleMarkdown } from './markdown.ts';
2
4
 
3
5
  const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'] as const;
4
6
  const OPERATION_COLUMNS = [
@@ -84,58 +86,14 @@ function createInvalidationOperation(
84
86
  };
85
87
  }
86
88
 
87
- function lineCells(line: string): string[] | null {
88
- const first = line.indexOf('|');
89
- const last = line.lastIndexOf('|');
90
- if (first < 0 || first === last || line.slice(0, first).trim() || line.slice(last + 1).trim()) return null;
91
- const inner = line.slice(first + 1, last);
92
- const cells: string[] = [];
93
- let start = 0;
94
- let escaped = false;
95
- for (let index = 0; index < inner.length; index += 1) {
96
- const char = inner[index]!;
97
- if (char === '|' && !escaped) {
98
- cells.push(inner.slice(start, index));
99
- start = index + 1;
100
- }
101
- escaped = char === '\\' && !escaped;
102
- if (char !== '\\') escaped = false;
89
+ function parseInvalidationTable(body: string, heading: string, columns: readonly string[]) {
90
+ const section = findSectionRange(body, [heading], 3);
91
+ const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
92
+ const table = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
93
+ if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
94
+ throw new Error(`invalidation ${heading} section must contain only its canonical table`);
103
95
  }
104
- cells.push(inner.slice(start));
105
- return cells.map((cell) => cell.replace(/\\([\\|])/g, '$1').trim());
106
- }
107
-
108
- function isSeparator(cells: readonly string[], count: number): boolean {
109
- return cells.length === count && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
110
- }
111
-
112
- function sectionBody(content: string): string | null {
113
- const heading = INVALIDATION_HEADINGS.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
114
- const match = new RegExp(`^##\\s+(${heading})\\s*$`, 'm').exec(content);
115
- if (!match) return null;
116
- const start = (match.index ?? 0) + match[0].length;
117
- const rest = content.slice(start);
118
- const end = rest.search(/^##\s+/m);
119
- return rest.slice(0, end < 0 ? rest.length : end);
120
- }
121
-
122
- function parseTable(body: string, heading: string, columns: readonly string[]): Record<string, string>[] | null | false {
123
- const marker = new RegExp(`^###\\s+${heading}\\s*$`, 'm').exec(body);
124
- if (!marker) return null;
125
- const start = (marker.index ?? 0) + marker[0].length;
126
- const rest = body.slice(start);
127
- const end = rest.search(/^###\s+/m);
128
- const lines = rest.slice(0, end < 0 ? rest.length : end).split(/\r?\n/).filter((line) => line.trim());
129
- const header = lines.length > 0 ? lineCells(lines[0]!) : null;
130
- const separator = lines.length > 1 ? lineCells(lines[1]!) : null;
131
- if (!header || !separator || header.length !== columns.length || !header.every((value, index) => value === columns[index]) || !isSeparator(separator, columns.length)) return false;
132
- const rows: Record<string, string>[] = [];
133
- for (const line of lines.slice(2)) {
134
- const cells = lineCells(line);
135
- if (!cells || cells.length !== columns.length) return false;
136
- rows.push(Object.fromEntries(columns.map((column, index) => [column, cells[index]!])));
137
- }
138
- return rows;
96
+ return table.rows;
139
97
  }
140
98
 
141
99
  function required(value: string, field: string): string {
@@ -155,15 +113,13 @@ function numberValue(value: string, field: string, allowZero = true): number {
155
113
  }
156
114
 
157
115
  function parseInvalidationDocument(content: string): InvalidationParseResult {
158
- const body = sectionBody(content);
159
- if (body === null) return { ok: true, present: false, document: { operations: [], targets: [] } };
116
+ const section = findSectionRange(content, INVALIDATION_HEADINGS);
117
+ if (section === null) return { ok: true, present: false, document: { operations: [], targets: [] } };
160
118
  try {
161
- const operationRows = parseTable(body, 'Operations', OPERATION_COLUMNS);
162
- const targetRows = parseTable(body, 'Targets', TARGET_COLUMNS);
163
- if (operationRows === null || targetRows === null || operationRows === false || targetRows === false) {
164
- return invalid('invalidation section must contain Operations and Targets tables');
165
- }
166
- const operations = operationRows.map((row) => ({
119
+ const body = content.slice(section.bodyStart, section.end);
120
+ const operationRows = parseInvalidationTable(body, 'Operations', OPERATION_COLUMNS);
121
+ const targetRows = parseInvalidationTable(body, 'Targets', TARGET_COLUMNS);
122
+ const operations = operationRows.map(({ values: row }) => ({
167
123
  operationId: required(row.operation_id!, 'operation_id'), sourceFamily: required(row.source_family!, 'source_family'),
168
124
  sourceArtifact: required(row.source_artifact!, 'source_artifact'), sourceRound: numberValue(row.source_round!, 'source_round', false),
169
125
  sourceSha256: required(row.source_sha256!, 'source_sha256'), status: status(row.status!, 'operation status'),
@@ -171,7 +127,7 @@ function parseInvalidationDocument(content: string): InvalidationParseResult {
171
127
  createdAt: required(row.created_at!, 'created_at'), updatedAt: required(row.updated_at!, 'updated_at'),
172
128
  completedAt: row.completed_at ?? '', error: row.error ?? ''
173
129
  } satisfies InvalidationOperation));
174
- const targets = targetRows.map((row) => ({
130
+ const targets = targetRows.map(({ values: row }) => ({
175
131
  targetId: required(row.target_id!, 'target_id'), operationId: required(row.operation_id!, 'operation_id'),
176
132
  targetKind: row.target_kind as InvalidationTargetKind, targetFamily: required(row.target_family!, 'target_family'),
177
133
  targetArtifact: required(row.target_artifact!, 'target_artifact'), targetRound: numberValue(row.target_round!, 'target_round', false),
@@ -297,11 +253,11 @@ function isArtifactInvalidated(document: InvalidationDocument, family: string, a
297
253
  }
298
254
 
299
255
  function invalidationMutation(content: string, document: InvalidationDocument) {
300
- const existing = sectionBody(content) !== null;
256
+ const existing = findSectionRange(content, INVALIDATION_HEADINGS);
301
257
  return {
302
258
  kind: 'section' as const,
303
259
  aliases: INVALIDATION_HEADINGS,
304
- heading: existing && /^##\s+Artifact Invalidation\s*$/m.test(content) ? INVALIDATION_HEADINGS[1] : INVALIDATION_HEADINGS[0],
260
+ heading: existing?.heading ?? INVALIDATION_HEADINGS[0],
305
261
  body: renderInvalidation(document)
306
262
  };
307
263
  }