@fitlab-ai/agent-infra 0.9.2 → 0.9.3

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 (318) hide show
  1. package/README.md +10 -9
  2. package/README.zh-CN.md +10 -9
  3. package/dist/bin/internal-cli.js +15 -0
  4. package/dist/lib/agent-clients/adapter.js +45 -2
  5. package/dist/lib/agent-clients/adapters/antigravity-cli.js +64 -0
  6. package/dist/lib/agent-clients/adapters/claude-code.js +10 -1
  7. package/dist/lib/agent-clients/adapters/codex.js +9 -0
  8. package/dist/lib/agent-clients/adapters/index.js +2 -2
  9. package/dist/lib/agent-clients/adapters/opencode.js +9 -0
  10. package/dist/lib/agent-clients/config.js +42 -4
  11. package/dist/lib/agent-clients/next-steps.js +11 -2
  12. package/dist/lib/agent-clients/reconcile.js +2 -1
  13. package/dist/lib/agent-clients/registry.js +7 -1
  14. package/dist/lib/agent-clients/schema.js +18 -0
  15. package/dist/lib/agent-clients/tokens.js +49 -0
  16. package/dist/lib/agent-clients/types.js +1 -1
  17. package/dist/lib/defaults.json +1 -1
  18. package/dist/lib/git/workflow.js +54 -1
  19. package/dist/lib/internal/agent-client.js +61 -4
  20. package/dist/lib/internal/git-workflow.js +6 -1
  21. package/dist/lib/internal/platform-checks.js +3 -3
  22. package/dist/lib/internal/platform-comment.js +29 -2
  23. package/dist/lib/internal/platform-issue.js +7 -0
  24. package/dist/lib/internal/platform-pr-review.js +132 -0
  25. package/dist/lib/internal/platform-pr.js +20 -2
  26. package/dist/lib/internal/platform-release-notes.js +52 -16
  27. package/dist/lib/internal/pr-review-grade.js +140 -0
  28. package/dist/lib/internal/release-workflow.js +13 -6
  29. package/dist/lib/internal/task-activity.js +73 -0
  30. package/dist/lib/internal/task-event.js +17 -1
  31. package/dist/lib/internal/task-lifecycle.js +7 -0
  32. package/dist/lib/internal/task-orchestration.js +51 -6
  33. package/dist/lib/internal/task-review.js +8 -3
  34. package/dist/lib/platform/adapters.js +8 -1
  35. package/dist/lib/platform/completion-backfill.js +122 -0
  36. package/dist/lib/platform/issue-comments.js +3 -2
  37. package/dist/lib/platform/merged-pr-equivalence.js +115 -5
  38. package/dist/lib/platform/pr-checks.js +64 -41
  39. package/dist/lib/platform/pr-review.js +166 -0
  40. package/dist/lib/platform/pull-requests.js +324 -3
  41. package/dist/lib/platform/release-notes.js +85 -3
  42. package/dist/lib/pr-review/evidence-grading.js +229 -0
  43. package/dist/lib/run/tui.js +3 -5
  44. package/dist/lib/sandbox/commands/create.js +0 -20
  45. package/dist/lib/sandbox/commands/show.js +1 -1
  46. package/dist/lib/task/activity-intent.js +101 -0
  47. package/dist/lib/task/artifact-lifecycle.js +2 -1
  48. package/dist/lib/task/commands/log.js +15 -14
  49. package/dist/lib/task/delegation-receipts.js +23 -2
  50. package/dist/lib/task/events.js +67 -28
  51. package/dist/lib/task/finalization-artifacts.js +35 -0
  52. package/dist/lib/task/lifecycle-execution.js +46 -0
  53. package/dist/lib/task/orchestration.js +408 -44
  54. package/dist/lib/task/review-finalization.js +29 -9
  55. package/dist/lib/task/task-execution-lock.js +231 -0
  56. package/dist/lib/task/verification-checks.js +1 -1
  57. package/dist/lib/task/verification-engine.js +218 -2
  58. package/dist/lib/task/verification.js +4 -3
  59. package/dist/lib/task/workspace-snapshot.js +17 -5
  60. package/lib/agent-clients/adapter.ts +75 -2
  61. package/lib/agent-clients/adapters/antigravity-cli.ts +65 -0
  62. package/lib/agent-clients/adapters/claude-code.ts +10 -1
  63. package/lib/agent-clients/adapters/codex.ts +9 -0
  64. package/lib/agent-clients/adapters/index.ts +2 -2
  65. package/lib/agent-clients/adapters/opencode.ts +9 -0
  66. package/lib/agent-clients/config.ts +49 -4
  67. package/lib/agent-clients/next-steps.ts +15 -2
  68. package/lib/agent-clients/reconcile.ts +2 -1
  69. package/lib/agent-clients/registry.ts +10 -0
  70. package/lib/agent-clients/schema.ts +18 -0
  71. package/lib/agent-clients/tokens.ts +53 -0
  72. package/lib/agent-clients/types.ts +15 -1
  73. package/lib/defaults.json +1 -1
  74. package/lib/git/workflow.ts +66 -2
  75. package/lib/internal/agent-client.ts +59 -4
  76. package/lib/internal/git-workflow.ts +5 -1
  77. package/lib/internal/platform-checks.ts +4 -4
  78. package/lib/internal/platform-comment.ts +17 -2
  79. package/lib/internal/platform-issue.ts +4 -0
  80. package/lib/internal/platform-pr-review.ts +104 -0
  81. package/lib/internal/platform-pr.ts +14 -1
  82. package/lib/internal/platform-release-notes.ts +59 -16
  83. package/lib/internal/pr-review-grade.ts +130 -0
  84. package/lib/internal/release-workflow.ts +13 -6
  85. package/lib/internal/task-activity.ts +55 -0
  86. package/lib/internal/task-event.ts +9 -1
  87. package/lib/internal/task-lifecycle.ts +4 -0
  88. package/lib/internal/task-orchestration.ts +45 -6
  89. package/lib/internal/task-review.ts +8 -3
  90. package/lib/platform/adapters.ts +29 -2
  91. package/lib/platform/completion-backfill.ts +134 -0
  92. package/lib/platform/issue-comments.ts +3 -2
  93. package/lib/platform/merged-pr-equivalence.ts +127 -5
  94. package/lib/platform/pr-checks.ts +72 -45
  95. package/lib/platform/pr-review.ts +201 -0
  96. package/lib/platform/pull-requests.ts +395 -2
  97. package/lib/platform/release-notes.ts +100 -4
  98. package/lib/pr-review/evidence-grading.ts +318 -0
  99. package/lib/run/tui.ts +3 -4
  100. package/lib/sandbox/commands/create.ts +0 -20
  101. package/lib/sandbox/commands/show.ts +1 -1
  102. package/lib/server/server.schema.json +2 -2
  103. package/lib/task/activity-intent.ts +128 -0
  104. package/lib/task/artifact-lifecycle.ts +2 -1
  105. package/lib/task/commands/log.ts +15 -14
  106. package/lib/task/delegation-receipts.ts +32 -3
  107. package/lib/task/events.ts +75 -30
  108. package/lib/task/finalization-artifacts.ts +58 -0
  109. package/lib/task/lifecycle-execution.ts +106 -0
  110. package/lib/task/orchestration.ts +543 -64
  111. package/lib/task/review-finalization.ts +40 -9
  112. package/lib/task/task-execution-lock.ts +296 -0
  113. package/lib/task/verification-checks.ts +1 -1
  114. package/lib/task/verification-engine.ts +249 -2
  115. package/lib/task/verification.ts +5 -3
  116. package/lib/task/workspace-snapshot.ts +26 -5
  117. package/package.json +3 -3
  118. package/templates/.agents/QUICKSTART.en.md +25 -8
  119. package/templates/.agents/QUICKSTART.zh-CN.md +25 -8
  120. package/templates/.agents/README.en.md +10 -7
  121. package/templates/.agents/README.zh-CN.md +10 -7
  122. package/templates/.agents/hooks/lifecycle-delegation.js +15 -4
  123. package/templates/.agents/rules/create-issue.github.en.md +4 -2
  124. package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -2
  125. package/templates/.agents/rules/issue-fields.github.en.md +3 -1
  126. package/templates/.agents/rules/issue-fields.github.zh-CN.md +3 -1
  127. package/templates/.agents/rules/issue-pr-commands.en.md +5 -3
  128. package/templates/.agents/rules/issue-pr-commands.github.en.md +5 -3
  129. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +5 -3
  130. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -3
  131. package/templates/.agents/rules/issue-sync.github.en.md +7 -3
  132. package/templates/.agents/rules/issue-sync.github.zh-CN.md +8 -4
  133. package/templates/.agents/rules/lifecycle-orchestration.en.md +5 -2
  134. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +5 -2
  135. package/templates/.agents/rules/milestone-inference.github.en.md +4 -2
  136. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -2
  137. package/templates/.agents/rules/next-step-output.en.md +3 -1
  138. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -1
  139. package/templates/.agents/rules/no-mid-flow-questions.en.md +17 -0
  140. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +17 -0
  141. package/templates/.agents/rules/pr-checks-commands.en.md +3 -3
  142. package/templates/.agents/rules/pr-checks-commands.github.en.md +2 -2
  143. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  144. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +3 -3
  145. package/templates/.agents/rules/pr-sync.en.md +3 -1
  146. package/templates/.agents/rules/pr-sync.github.en.md +11 -1
  147. package/templates/.agents/rules/pr-sync.github.zh-CN.md +11 -1
  148. package/templates/.agents/rules/pr-sync.zh-CN.md +3 -1
  149. package/templates/.agents/rules/release-commands.en.md +1 -1
  150. package/templates/.agents/rules/release-commands.github.en.md +12 -2
  151. package/templates/.agents/rules/release-commands.github.zh-CN.md +12 -2
  152. package/templates/.agents/rules/release-commands.zh-CN.md +1 -1
  153. package/templates/.agents/rules/task-management.en.md +11 -0
  154. package/templates/.agents/rules/task-management.zh-CN.md +11 -0
  155. package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -7
  156. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -7
  157. package/templates/.agents/skills/block-task/SKILL.en.md +7 -5
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +7 -5
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -4
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -4
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
  165. package/templates/.agents/skills/code-task/SKILL.en.md +9 -6
  166. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -8
  167. package/templates/.agents/skills/commit/SKILL.en.md +3 -1
  168. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -1
  169. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  170. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  171. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  174. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  175. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +6 -4
  176. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +6 -4
  177. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  178. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  179. package/templates/.agents/skills/complete-task/SKILL.en.md +21 -6
  180. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +21 -6
  181. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  182. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  183. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +17 -0
  184. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +17 -0
  185. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -1
  186. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -1
  187. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  188. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  189. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -16
  190. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -16
  191. package/templates/.agents/skills/create-task/SKILL.en.md +5 -3
  192. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -3
  193. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -3
  194. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -3
  195. package/templates/.agents/skills/plan-task/SKILL.en.md +8 -5
  196. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +8 -5
  197. package/templates/.agents/skills/post-release/SKILL.en.md +2 -0
  198. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +2 -0
  199. package/templates/.agents/skills/release/SKILL.en.md +16 -10
  200. package/templates/.agents/skills/release/SKILL.zh-CN.md +16 -10
  201. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -1
  202. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -1
  203. package/templates/.agents/skills/review-analysis/SKILL.en.md +7 -4
  204. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +7 -4
  205. package/templates/.agents/skills/review-code/SKILL.en.md +9 -6
  206. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +9 -6
  207. package/templates/.agents/skills/review-plan/SKILL.en.md +7 -4
  208. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +7 -4
  209. package/templates/.agents/skills/review-pr/SKILL.en.md +158 -0
  210. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +158 -0
  211. package/templates/.agents/skills/review-pr/config/verify.en.json +37 -0
  212. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +37 -0
  213. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +65 -0
  214. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +65 -0
  215. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +18 -0
  216. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +18 -0
  217. package/templates/.agents/skills/review-pr/reference/report-template.en.md +85 -0
  218. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +85 -0
  219. package/templates/.agents/skills/run-task/SKILL.en.md +8 -8
  220. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +8 -8
  221. package/templates/.agents/skills/run-task/config/verify.en.json +2 -1
  222. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +2 -1
  223. package/templates/.agents/skills/run-task/reference/host-validation.en.md +20 -0
  224. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +20 -0
  225. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +35 -61
  226. package/templates/.agents/skills/watch-pr/SKILL.en.md +13 -13
  227. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +13 -13
  228. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +27 -6
  229. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +26 -5
  230. package/templates/.agents/templates/task.en.md +1 -1
  231. package/templates/.agents/templates/task.zh-CN.md +1 -1
  232. package/templates/.agents/workflows/bug-fix.en.yaml +3 -3
  233. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +3 -3
  234. package/templates/.agents/workflows/code-review.en.yaml +1 -1
  235. package/templates/.agents/workflows/code-review.zh-CN.yaml +1 -1
  236. package/templates/.agents/workflows/feature-development.en.yaml +4 -4
  237. package/templates/.agents/workflows/feature-development.zh-CN.yaml +4 -4
  238. package/templates/.agents/workflows/refactoring.en.yaml +3 -3
  239. package/templates/.agents/workflows/refactoring.zh-CN.yaml +3 -3
  240. package/templates/.claude/commands/post-release.en.md +1 -0
  241. package/templates/.claude/commands/post-release.zh-CN.md +1 -0
  242. package/templates/.claude/commands/review-pr.en.md +8 -0
  243. package/templates/.claude/commands/review-pr.zh-CN.md +8 -0
  244. package/templates/.gemini/commands/_project_/review-pr.en.toml +10 -0
  245. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +10 -0
  246. package/templates/.opencode/commands/post-release.en.md +2 -0
  247. package/templates/.opencode/commands/post-release.zh-CN.md +2 -0
  248. package/templates/.opencode/commands/review-pr.en.md +11 -0
  249. package/templates/.opencode/commands/review-pr.zh-CN.md +11 -0
  250. package/dist/lib/agent-clients/adapters/gemini-cli.js +0 -64
  251. package/lib/agent-clients/adapters/gemini-cli.ts +0 -65
  252. package/templates/.gemini/commands/_project_/analyze-task.en.toml +0 -10
  253. package/templates/.gemini/commands/_project_/analyze-task.zh-CN.toml +0 -10
  254. package/templates/.gemini/commands/_project_/archive-tasks.en.toml +0 -10
  255. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +0 -10
  256. package/templates/.gemini/commands/_project_/block-task.en.toml +0 -10
  257. package/templates/.gemini/commands/_project_/block-task.zh-CN.toml +0 -10
  258. package/templates/.gemini/commands/_project_/cancel-task.en.toml +0 -10
  259. package/templates/.gemini/commands/_project_/cancel-task.zh-CN.toml +0 -10
  260. package/templates/.gemini/commands/_project_/check-task.en.toml +0 -10
  261. package/templates/.gemini/commands/_project_/check-task.zh-CN.toml +0 -10
  262. package/templates/.gemini/commands/_project_/close-codescan.en.toml +0 -8
  263. package/templates/.gemini/commands/_project_/close-codescan.zh-CN.toml +0 -8
  264. package/templates/.gemini/commands/_project_/close-dependabot.en.toml +0 -8
  265. package/templates/.gemini/commands/_project_/close-dependabot.zh-CN.toml +0 -8
  266. package/templates/.gemini/commands/_project_/code-task.en.toml +0 -10
  267. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +0 -10
  268. package/templates/.gemini/commands/_project_/commit.en.toml +0 -10
  269. package/templates/.gemini/commands/_project_/commit.zh-CN.toml +0 -10
  270. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +0 -10
  271. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +0 -10
  272. package/templates/.gemini/commands/_project_/complete-task.en.toml +0 -10
  273. package/templates/.gemini/commands/_project_/complete-task.zh-CN.toml +0 -10
  274. package/templates/.gemini/commands/_project_/create-pr.en.toml +0 -10
  275. package/templates/.gemini/commands/_project_/create-pr.zh-CN.toml +0 -10
  276. package/templates/.gemini/commands/_project_/create-release-note.en.toml +0 -8
  277. package/templates/.gemini/commands/_project_/create-release-note.zh-CN.toml +0 -8
  278. package/templates/.gemini/commands/_project_/create-task.en.toml +0 -8
  279. package/templates/.gemini/commands/_project_/create-task.zh-CN.toml +0 -8
  280. package/templates/.gemini/commands/_project_/import-codescan.en.toml +0 -8
  281. package/templates/.gemini/commands/_project_/import-codescan.zh-CN.toml +0 -8
  282. package/templates/.gemini/commands/_project_/import-dependabot.en.toml +0 -8
  283. package/templates/.gemini/commands/_project_/import-dependabot.zh-CN.toml +0 -8
  284. package/templates/.gemini/commands/_project_/import-issue.en.toml +0 -8
  285. package/templates/.gemini/commands/_project_/import-issue.zh-CN.toml +0 -8
  286. package/templates/.gemini/commands/_project_/init-labels.en.toml +0 -8
  287. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +0 -8
  288. package/templates/.gemini/commands/_project_/init-milestones.en.toml +0 -10
  289. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +0 -10
  290. package/templates/.gemini/commands/_project_/plan-task.en.toml +0 -10
  291. package/templates/.gemini/commands/_project_/plan-task.zh-CN.toml +0 -10
  292. package/templates/.gemini/commands/_project_/post-release.en.toml +0 -6
  293. package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +0 -6
  294. package/templates/.gemini/commands/_project_/refine-title.en.toml +0 -8
  295. package/templates/.gemini/commands/_project_/refine-title.zh-CN.toml +0 -8
  296. package/templates/.gemini/commands/_project_/release.en.toml +0 -8
  297. package/templates/.gemini/commands/_project_/release.zh-CN.toml +0 -8
  298. package/templates/.gemini/commands/_project_/restore-task.en.toml +0 -8
  299. package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +0 -8
  300. package/templates/.gemini/commands/_project_/review-analysis.en.toml +0 -10
  301. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +0 -10
  302. package/templates/.gemini/commands/_project_/review-code.en.toml +0 -10
  303. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +0 -10
  304. package/templates/.gemini/commands/_project_/review-plan.en.toml +0 -10
  305. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +0 -10
  306. package/templates/.gemini/commands/_project_/run-task.en.toml +0 -8
  307. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +0 -8
  308. package/templates/.gemini/commands/_project_/test-integration.en.toml +0 -6
  309. package/templates/.gemini/commands/_project_/test-integration.zh-CN.toml +0 -6
  310. package/templates/.gemini/commands/_project_/test.en.toml +0 -6
  311. package/templates/.gemini/commands/_project_/test.zh-CN.toml +0 -6
  312. package/templates/.gemini/commands/_project_/update-agent-infra.en.toml +0 -6
  313. package/templates/.gemini/commands/_project_/update-agent-infra.zh-CN.toml +0 -6
  314. package/templates/.gemini/commands/_project_/upgrade-dependency.en.toml +0 -8
  315. package/templates/.gemini/commands/_project_/upgrade-dependency.zh-CN.toml +0 -8
  316. package/templates/.gemini/commands/_project_/watch-pr.en.toml +0 -10
  317. package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +0 -10
  318. package/templates/.gemini/settings.json +0 -3
@@ -4,12 +4,13 @@ import { parseTaskFrontmatter } from '../task/frontmatter.js';
4
4
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { extractSection } from '../task/sections.js';
6
6
  import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
7
- import { inspectPlatformChangeRequest, registerPlatformCapabilities } from './adapters.js';
7
+ import { inspectPlatformChangeRequest, inspectPlatformIssueClosingChangeRequests, registerPlatformCapabilities } from './adapters.js';
8
8
  import { parseGitHubRemote, resolvePlatformContext } from './context.js';
9
9
  import { createGitHubClient } from './github-client.js';
10
10
  import { inspectPlatformIssue } from './issues.js';
11
11
  import { planPullRequestMetadata } from './pull-request-metadata.js';
12
12
  import { platformResult } from './types.js';
13
+ import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
13
14
  function result(status, taskId, issueNumber, prNumber, overrides = {}) {
14
15
  return {
15
16
  ...platformResult(status),
@@ -18,6 +19,17 @@ function result(status, taskId, issueNumber, prNumber, overrides = {}) {
18
19
  ...overrides
19
20
  };
20
21
  }
22
+ function externalResult(base, overrides = {}) {
23
+ return {
24
+ ...base,
25
+ mode: null,
26
+ authorization: null,
27
+ candidates: [],
28
+ eligible: [],
29
+ selected: null,
30
+ ...overrides
31
+ };
32
+ }
21
33
  function normalizePullRequest(remote, repository) {
22
34
  const number = Number(remote.number);
23
35
  const headRepository = remote.head?.repo?.full_name;
@@ -25,6 +37,12 @@ function normalizePullRequest(remote, repository) {
25
37
  if (!Number.isInteger(number) || number <= 0 || !remote.node_id || !remote.html_url ||
26
38
  !remote.head?.ref || !remote.head.sha || !headRepository || !remote.base?.ref || !baseRepository)
27
39
  return null;
40
+ const mergeabilityDetail = remote.mergeable_state?.trim().toLowerCase() || null;
41
+ const mergeability = remote.mergeable === false
42
+ ? { state: 'conflicting', detail: mergeabilityDetail }
43
+ : remote.mergeable === true && mergeabilityDetail !== 'dirty'
44
+ ? { state: 'mergeable', detail: mergeabilityDetail }
45
+ : { state: 'unknown', detail: mergeabilityDetail };
28
46
  return {
29
47
  repository,
30
48
  number,
@@ -40,9 +58,141 @@ function normalizePullRequest(remote, repository) {
40
58
  mergeCommitSha: remote.merge_commit_sha || null,
41
59
  labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
42
60
  assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
43
- milestone: remote.milestone?.title || null
61
+ milestone: remote.milestone?.title || null,
62
+ mergeability
44
63
  };
45
64
  }
65
+ function normalizeClosingPullRequest(node, repository) {
66
+ return normalizePullRequest({
67
+ number: node.number,
68
+ node_id: node.id,
69
+ html_url: node.url,
70
+ state: node.state === 'OPEN' ? 'open' : 'closed',
71
+ title: node.title,
72
+ body: node.body,
73
+ draft: node.isDraft,
74
+ head: { ref: node.headRefName, sha: node.headRefOid, repo: node.headRepository ? { full_name: node.headRepository.nameWithOwner } : null },
75
+ base: { ref: node.baseRefName, sha: node.baseRefOid, repo: node.baseRepository ? { full_name: node.baseRepository.nameWithOwner } : null },
76
+ merged_at: node.mergedAt,
77
+ merge_commit_sha: node.mergeCommit?.oid,
78
+ labels: node.labels?.nodes,
79
+ assignees: node.assignees?.nodes,
80
+ milestone: node.milestone
81
+ }, repository);
82
+ }
83
+ function repositoryKey(repository) {
84
+ return repository.trim().toLowerCase();
85
+ }
86
+ function identitySignature(pullRequest) {
87
+ return [
88
+ pullRequest.repository, pullRequest.number, pullRequest.nodeId, pullRequest.url,
89
+ pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
90
+ pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
91
+ pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
92
+ ].join('|');
93
+ }
94
+ function hasCompleteExternalIdentity(pullRequest) {
95
+ const required = [
96
+ pullRequest.repository, pullRequest.nodeId, pullRequest.url,
97
+ pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
98
+ pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
99
+ ];
100
+ return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
101
+ required.every((value) => Boolean(value?.trim())) &&
102
+ Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
103
+ }
104
+ function selectExternalPullRequest(candidates, repository, existingPrNumber, explicitPrNumber) {
105
+ const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
106
+ if (invalid)
107
+ return {
108
+ status: 'failed', code: 'PR_IDENTITY_INVALID',
109
+ message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
110
+ };
111
+ const byNumber = new Map();
112
+ for (const candidate of candidates) {
113
+ const signature = identitySignature(candidate);
114
+ const prior = byNumber.get(candidate.number);
115
+ if (prior && prior !== signature)
116
+ return {
117
+ status: 'failed', code: 'PR_IDENTITY_INVALID',
118
+ message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
119
+ };
120
+ byNumber.set(candidate.number, signature);
121
+ }
122
+ const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => other.number === candidate.number) === index);
123
+ const wantedRepository = repositoryKey(repository);
124
+ const eligible = unique.filter((candidate) => repositoryKey(candidate.repository) === wantedRepository &&
125
+ repositoryKey(candidate.base.repository) === wantedRepository &&
126
+ Boolean(candidate.mergedAt && candidate.mergeCommitSha));
127
+ if (explicitPrNumber !== null) {
128
+ const selected = unique.find((candidate) => candidate.number === explicitPrNumber);
129
+ if (!selected)
130
+ return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR #${explicitPrNumber} is not a closing PR`, candidates, eligible };
131
+ if (!eligible.some((candidate) => candidate.number === explicitPrNumber)) {
132
+ return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR #${explicitPrNumber} is not an eligible merged PR`, candidates, eligible };
133
+ }
134
+ if (existingPrNumber !== null && existingPrNumber !== explicitPrNumber) {
135
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
136
+ }
137
+ return { status: 'selected', source: 'explicit', selected, candidates, eligible };
138
+ }
139
+ if (existingPrNumber !== null && eligible.length === 1 && existingPrNumber !== eligible[0].number) {
140
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
141
+ }
142
+ if (eligible.length === 0)
143
+ return { status: 'normal', candidates, eligible: [] };
144
+ if (eligible.length > 1)
145
+ return {
146
+ status: 'failed', code: 'PR_IDENTITY_AMBIGUOUS',
147
+ message: 'Multiple eligible merged closing pull requests were found', candidates, eligible
148
+ };
149
+ return { status: 'selected', source: 'unique', selected: eligible[0], candidates, eligible };
150
+ }
151
+ const CLOSING_PULL_REQUESTS_QUERY = `query($owner:String!,$name:String!,$issue:Int!,$cursor:String){repository(owner:$owner,name:$name){issue(number:$issue){closedByPullRequestsReferences(first:100,after:$cursor){nodes{number id url state title body isDraft headRefName headRefOid headRepository{nameWithOwner} baseRefName baseRefOid baseRepository{nameWithOwner} mergedAt mergeCommit{oid} labels(first:100){nodes{name}} assignees(first:100){nodes{login}} milestone{title}} pageInfo{hasNextPage endCursor}}}}}`;
152
+ function inspectGitHubIssueClosingChangeRequests(client, repository, issueNumber, cwd) {
153
+ const [owner, name] = repository.split('/');
154
+ if (!owner || !name || !Number.isInteger(issueNumber) || issueNumber <= 0)
155
+ return {
156
+ ok: false,
157
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Repository or Issue identity is invalid', retryable: false }
158
+ };
159
+ const candidates = [];
160
+ let cursor = null;
161
+ for (;;) {
162
+ const args = [
163
+ 'api', 'graphql', '-f', `query=${CLOSING_PULL_REQUESTS_QUERY}`,
164
+ '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `issue=${issueNumber}`
165
+ ];
166
+ if (cursor)
167
+ args.push('-F', `cursor=${cursor}`);
168
+ const response = client.json(args, { cwd });
169
+ if (!response.ok)
170
+ return response;
171
+ const connection = response.value?.data?.repository?.issue?.closedByPullRequestsReferences;
172
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo)
173
+ return {
174
+ ok: false,
175
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request response is incomplete', retryable: false }
176
+ };
177
+ for (const node of connection.nodes) {
178
+ const normalized = normalizeClosingPullRequest(node, repository);
179
+ if (!normalized)
180
+ return {
181
+ ok: false,
182
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request identity is incomplete', retryable: false }
183
+ };
184
+ candidates.push(normalized);
185
+ }
186
+ if (!connection.pageInfo.hasNextPage)
187
+ return { ok: true, value: candidates };
188
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor)
189
+ return {
190
+ ok: false,
191
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request pagination cursor is invalid', retryable: false }
192
+ };
193
+ cursor = connection.pageInfo.endCursor;
194
+ }
195
+ }
46
196
  function expectedHead(repository, head) {
47
197
  const colon = head.indexOf(':');
48
198
  if (colon === -1)
@@ -179,6 +329,9 @@ registerPlatformCapabilities('github', {
179
329
  inspectChangeRequest({ client, repository, number, cwd }) {
180
330
  return inspectGitHubPullRequest(client || createGitHubClient(), repository, number, cwd);
181
331
  },
332
+ inspectIssueClosingChangeRequests({ client, repository, issueNumber, cwd }) {
333
+ return inspectGitHubIssueClosingChangeRequests(client || createGitHubClient(), repository, issueNumber, cwd);
334
+ },
182
335
  resolveChangeRequestGitEvidence(context) {
183
336
  return resolveGitHubChangeRequestGitEvidence(context);
184
337
  }
@@ -230,6 +383,43 @@ function inspectPlatformPullRequest(taskRef, options = {}) {
230
383
  resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, error: null
231
384
  });
232
385
  }
386
+ function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
387
+ const client = options.client || createGitHubClient();
388
+ const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
389
+ const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
390
+ if (!usable) {
391
+ return result(context.status, null, null, null, {
392
+ platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
393
+ });
394
+ }
395
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
396
+ return result('failed', null, null, null, {
397
+ platform: context.platform, capabilities: context.capabilities,
398
+ error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
399
+ });
400
+ }
401
+ const fetched = inspectPlatformChangeRequest(context.platform.type, {
402
+ cwd: options.cwd || process.cwd(),
403
+ repository: context.platform.repository,
404
+ number: prNumber,
405
+ client
406
+ });
407
+ if (!fetched.ok || !fetched.value) {
408
+ const error = fetched.error || {
409
+ code: 'PR_INSPECTION_INVALID',
410
+ message: 'Platform adapter returned no change-request snapshot',
411
+ retryable: false
412
+ };
413
+ return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
414
+ platform: context.platform, capabilities: context.capabilities,
415
+ resource: { kind: 'pull-request', number: prNumber }, error
416
+ });
417
+ }
418
+ return result('no-op', null, null, prNumber, {
419
+ platform: context.platform, capabilities: context.capabilities,
420
+ resource: { kind: 'pull-request', number: prNumber }, pullRequest: fetched.value, error: null
421
+ });
422
+ }
233
423
  function appendActivity(content, line) {
234
424
  const body = extractSection(content, ['活动日志', 'Activity Log']);
235
425
  return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
@@ -291,6 +481,137 @@ function bindPlatformPullRequest(taskRef, options) {
291
481
  operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
292
482
  });
293
483
  }
484
+ function externalEvidenceNote(issueNumber, source, pullRequest) {
485
+ return [
486
+ `authorization=${source}`,
487
+ externalIdentityEvidenceNote(issueNumber, pullRequest)
488
+ ].join('; ');
489
+ }
490
+ function externalIdentityEvidenceNote(issueNumber, pullRequest) {
491
+ return [
492
+ `issue=#${issueNumber}`,
493
+ `pr=#${pullRequest.number}`,
494
+ `url=${pullRequest.url}`,
495
+ `base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
496
+ `head=${pullRequest.head.repository}:${pullRequest.head.ref}@${pullRequest.head.sha}`,
497
+ `mergedAt=${pullRequest.mergedAt}`,
498
+ `mergeCommitSha=${pullRequest.mergeCommitSha}`
499
+ ].join('; ');
500
+ }
501
+ function resolveExternalPullRequest(taskRef, options) {
502
+ const base = resolvedContext(taskRef, options);
503
+ if (!base.ok)
504
+ return externalResult(base.output);
505
+ const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
506
+ if (inventory.status === 'failed')
507
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
508
+ platform: base.context.platform,
509
+ capabilities: base.context.capabilities,
510
+ error: { code: inventory.error?.code || 'ARTIFACT_TOPOLOGY_CONFLICT', message: inventory.error?.message || 'Completion artifact inventory failed', retryable: false }
511
+ }));
512
+ if (inventory.artifacts.length > 0) {
513
+ if (options.pr !== undefined)
514
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
515
+ platform: base.context.platform,
516
+ capabilities: base.context.capabilities,
517
+ error: { code: 'EXTERNAL_DELIVERY_NOT_APPLICABLE', message: 'External PR selection is only valid when the completion inventory is empty', retryable: false }
518
+ }), { mode: 'normal' });
519
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
520
+ platform: base.context.platform,
521
+ capabilities: base.context.capabilities,
522
+ operations: [{ name: 'external-pr:inventory', status: 'no-op', reasonCode: 'CANONICAL_ARTIFACTS_PRESENT' }],
523
+ error: null
524
+ }), { mode: 'normal' });
525
+ }
526
+ if (!base.issueNumber)
527
+ return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
528
+ platform: base.context.platform,
529
+ capabilities: base.context.capabilities,
530
+ error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
531
+ }));
532
+ const inspected = inspectPlatformIssueClosingChangeRequests(base.context.platform.type, {
533
+ cwd: base.resolved.repoRoot,
534
+ repository: base.context.platform.repository,
535
+ issueNumber: base.issueNumber,
536
+ client: base.client
537
+ });
538
+ if (!inspected.ok || !inspected.value) {
539
+ const error = inspected.error || { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
540
+ return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
541
+ platform: base.context.platform, capabilities: base.context.capabilities, error
542
+ }));
543
+ }
544
+ const selected = selectExternalPullRequest(inspected.value, base.context.platform.repository, base.prNumber, options.pr ?? null);
545
+ if (selected.status === 'normal')
546
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
547
+ platform: base.context.platform,
548
+ capabilities: base.context.capabilities,
549
+ operations: [{ name: 'external-pr:select', status: 'no-op', reasonCode: 'NO_ELIGIBLE_CANDIDATE' }],
550
+ error: null
551
+ }), { mode: 'normal', candidates: selected.candidates, eligible: selected.eligible });
552
+ if (selected.status === 'failed')
553
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
554
+ platform: base.context.platform,
555
+ capabilities: base.context.capabilities,
556
+ error: { code: selected.code, message: selected.message, retryable: false }
557
+ }), { candidates: selected.candidates, eligible: selected.eligible });
558
+ const note = externalEvidenceNote(base.issueNumber, selected.source, selected.selected);
559
+ const identityNote = externalIdentityEvidenceNote(base.issueNumber, selected.selected);
560
+ const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
561
+ const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
562
+ const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
563
+ if (auditLines.length > 0 && !alreadyAudited)
564
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
565
+ platform: base.context.platform,
566
+ capabilities: base.context.capabilities,
567
+ pullRequest: selected.selected,
568
+ error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
569
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
570
+ if (alreadyAudited && base.prNumber !== selected.selected.number)
571
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
572
+ platform: base.context.platform,
573
+ capabilities: base.context.capabilities,
574
+ pullRequest: selected.selected,
575
+ error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
576
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
577
+ if (base.prNumber === selected.selected.number && alreadyAudited) {
578
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
579
+ platform: base.context.platform,
580
+ capabilities: base.context.capabilities,
581
+ resource: { kind: 'pull-request', number: selected.selected.number },
582
+ pullRequest: selected.selected,
583
+ operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
584
+ error: null
585
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
586
+ }
587
+ const metadata = captureTaskWriteMetadata();
588
+ const write = writeTask({
589
+ taskRef: base.resolved.taskId,
590
+ expectedState: 'active',
591
+ dryRun: options.dryRun,
592
+ mutations: [
593
+ { kind: 'frontmatter', set: { pr_number: selected.selected.number, pr_status: 'created', assigned_to: options.agent } },
594
+ { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — ${activityEntry}`) }
595
+ ]
596
+ }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
597
+ if (write.status === 'failed')
598
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
599
+ platform: base.context.platform,
600
+ capabilities: base.context.capabilities,
601
+ pullRequest: selected.selected,
602
+ error: { code: write.error.code, message: write.error.message, retryable: false }
603
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
604
+ const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
605
+ return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selected.selected.number, {
606
+ changed: !options.dryRun && write.changed,
607
+ platform: base.context.platform,
608
+ capabilities: base.context.capabilities,
609
+ resource: { kind: 'pull-request', number: selected.selected.number },
610
+ pullRequest: selected.selected,
611
+ operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
612
+ error: null
613
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
614
+ }
294
615
  function createPlatformPullRequest(taskRef, options) {
295
616
  const base = resolvedContext(taskRef, options);
296
617
  if (!base.ok)
@@ -440,5 +761,5 @@ function syncPlatformPullRequest(taskRef, options) {
440
761
  operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
441
762
  });
442
763
  }
443
- export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectPlatformPullRequest, normalizePullRequest, resolveGitHubChangeRequestGitEvidence, selectPullRequest, syncPlatformPullRequest };
764
+ export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveGitHubChangeRequestGitEvidence, resolveExternalPullRequest, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest };
444
765
  //# sourceMappingURL=pull-requests.js.map
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
4
5
  import { resolvePlatformContext } from './context.js';
5
6
  import { fetchGitHubReleaseNoteData, publishGitHubReleaseNotes } from './github-release-notes.js';
6
7
  const unsupportedError = {
@@ -8,6 +9,77 @@ const unsupportedError = {
8
9
  message: 'Release notes are not supported for the configured platform',
9
10
  retryable: false
10
11
  };
12
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
13
+ function releaseNoteFailure(code, message) {
14
+ return {
15
+ status: 'failed',
16
+ changed: false,
17
+ operation: null,
18
+ url: null,
19
+ error: { code, message, retryable: false }
20
+ };
21
+ }
22
+ function normalizeReleaseNoteBytes(input) {
23
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(input);
24
+ const normalized = text.replace(/\r\n?/g, '\n').replace(/\n*$/, '') + '\n';
25
+ return Buffer.from(normalized, 'utf8');
26
+ }
27
+ function releaseNoteSha256(bytes) {
28
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
29
+ }
30
+ function isWithin(parent, candidate) {
31
+ const relative = path.relative(parent, candidate);
32
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
33
+ }
34
+ function stageReleaseNotes(input, options = {}) {
35
+ if (!input.notesFile || input.notesFile === '-') {
36
+ return { status: 'failed', changed: false, notesFile: null, sha256: null, byteLength: null, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'notes-file must identify an existing external file', retryable: false } };
37
+ }
38
+ const requestedPath = path.resolve(input.notesFile);
39
+ let notesFile;
40
+ let sourceStat;
41
+ try {
42
+ if (fs.lstatSync(requestedPath).isSymbolicLink())
43
+ throw new Error('symbolic links are not allowed');
44
+ notesFile = fs.realpathSync(requestedPath);
45
+ sourceStat = fs.statSync(notesFile);
46
+ const worktree = fs.realpathSync(path.resolve(options.cwd || process.cwd()));
47
+ if (!sourceStat.isFile() || isWithin(worktree, notesFile))
48
+ throw new Error('path is not an external regular file');
49
+ }
50
+ catch {
51
+ return { status: 'failed', changed: false, notesFile: null, sha256: null, byteLength: null, error: { code: 'RELEASE_NOTES_PATH_INVALID', message: 'notes-file must be a regular file outside the current worktree and must not be a symbolic link', retryable: false } };
52
+ }
53
+ let original;
54
+ let normalized;
55
+ try {
56
+ original = fs.readFileSync(notesFile);
57
+ normalized = normalizeReleaseNoteBytes(original);
58
+ }
59
+ catch {
60
+ return { status: 'failed', changed: false, notesFile, sha256: null, byteLength: null, error: { code: 'RELEASE_NOTES_CONTENT_INVALID', message: 'notes-file must contain valid UTF-8', retryable: false } };
61
+ }
62
+ const changed = !original.equals(normalized);
63
+ if (changed) {
64
+ const temporary = path.join(path.dirname(notesFile), `.${path.basename(notesFile)}.${process.pid}.${Date.now()}.tmp`);
65
+ try {
66
+ fs.writeFileSync(temporary, normalized, { flag: 'wx', mode: sourceStat.mode });
67
+ fs.renameSync(temporary, notesFile);
68
+ }
69
+ catch {
70
+ fs.rmSync(temporary, { force: true });
71
+ return { status: 'failed', changed: false, notesFile, sha256: null, byteLength: null, error: { code: 'RELEASE_NOTES_STAGE_FAILED', message: 'notes-file could not be staged atomically', retryable: false } };
72
+ }
73
+ }
74
+ return {
75
+ status: changed ? 'applied' : 'no-op',
76
+ changed,
77
+ notesFile,
78
+ sha256: releaseNoteSha256(normalized),
79
+ byteLength: normalized.byteLength,
80
+ error: null
81
+ };
82
+ }
11
83
  function baseResult(status) {
12
84
  return {
13
85
  status,
@@ -126,8 +198,18 @@ function releaseNoteContext(input, options = {}) {
126
198
  };
127
199
  }
128
200
  function publishReleaseNotes(input, options = {}) {
129
- if (!input.tag || !input.title || !input.notesFile) {
130
- return { status: 'failed', changed: false, operation: null, url: null, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'tag, title, and notes-file are required', retryable: false } };
201
+ if (!input.tag || !input.title || !input.notesFile || !SHA256_PATTERN.test(input.expectedSha256)) {
202
+ return releaseNoteFailure('RELEASE_NOTES_INPUT_INVALID', 'tag, title, notes-file, and expected-sha256 are required');
203
+ }
204
+ let actualSha256;
205
+ try {
206
+ actualSha256 = releaseNoteSha256(fs.readFileSync(input.notesFile));
207
+ }
208
+ catch {
209
+ return releaseNoteFailure('RELEASE_NOTES_FILE_UNREADABLE', 'notes-file could not be read');
210
+ }
211
+ if (actualSha256 !== input.expectedSha256) {
212
+ return releaseNoteFailure('RELEASE_NOTES_DIGEST_MISMATCH', 'notes-file does not match the confirmed SHA-256 digest');
131
213
  }
132
214
  const cwd = path.resolve(options.cwd || process.cwd());
133
215
  const platformType = options.platformType ?? configuredPlatform(cwd);
@@ -139,5 +221,5 @@ function publishReleaseNotes(input, options = {}) {
139
221
  }
140
222
  return publishGitHubReleaseNotes({ ...input, repository: context.platform.repository }, { cwd, client: options.client });
141
223
  }
142
- export { publishReleaseNotes, releaseNoteContext };
224
+ export { normalizeReleaseNoteBytes, publishReleaseNotes, releaseNoteContext, releaseNoteSha256, stageReleaseNotes };
143
225
  //# sourceMappingURL=release-notes.js.map