@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
@@ -7,6 +7,7 @@ import { extractSection } from '../task/sections.ts';
7
7
  import { captureTaskWriteMetadata, writeTask } from '../task/write.ts';
8
8
  import {
9
9
  inspectPlatformChangeRequest,
10
+ inspectPlatformIssueClosingChangeRequests,
10
11
  registerPlatformCapabilities
11
12
  } from './adapters.ts';
12
13
  import type { PlatformChangeRequestSnapshot } from './adapters.ts';
@@ -17,6 +18,7 @@ import { inspectPlatformIssue } from './issues.ts';
17
18
  import { planPullRequestMetadata } from './pull-request-metadata.ts';
18
19
  import { platformResult } from './types.ts';
19
20
  import type { PlatformOperation, PlatformResult } from './types.ts';
21
+ import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
20
22
 
21
23
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
22
24
 
@@ -37,6 +39,18 @@ type CreateOptions = SharedOptions & {
37
39
  };
38
40
  type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
39
41
  type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean };
42
+ type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
43
+ type ExternalPullRequestSelection =
44
+ | { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
45
+ | { status: 'selected'; source: 'unique' | 'explicit'; selected: PullRequestSnapshot; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] }
46
+ | { status: 'failed'; code: string; message: string; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] };
47
+ type ExternalPullRequestResult = PullRequestResult & {
48
+ mode: 'normal' | 'external' | null;
49
+ authorization: 'unique' | 'explicit' | null;
50
+ candidates: PullRequestSnapshot[];
51
+ eligible: PullRequestSnapshot[];
52
+ selected: PullRequestSnapshot | null;
53
+ };
40
54
 
41
55
  type RemotePullRequest = {
42
56
  number?: number;
@@ -54,6 +68,36 @@ type RemotePullRequest = {
54
68
  labels?: Array<string | { name?: string }>;
55
69
  assignees?: Array<{ login?: string }>;
56
70
  milestone?: { title?: string } | null;
71
+ mergeable?: boolean | null;
72
+ mergeable_state?: string | null;
73
+ };
74
+
75
+ type ClosingPullRequestNode = {
76
+ number?: number;
77
+ id?: string;
78
+ url?: string;
79
+ state?: string;
80
+ title?: string;
81
+ body?: string | null;
82
+ isDraft?: boolean;
83
+ headRefName?: string;
84
+ headRefOid?: string;
85
+ headRepository?: { nameWithOwner?: string } | null;
86
+ baseRefName?: string;
87
+ baseRefOid?: string;
88
+ baseRepository?: { nameWithOwner?: string } | null;
89
+ mergedAt?: string | null;
90
+ mergeCommit?: { oid?: string } | null;
91
+ labels?: { nodes?: Array<{ name?: string }> };
92
+ assignees?: { nodes?: Array<{ login?: string }> };
93
+ milestone?: { title?: string } | null;
94
+ };
95
+
96
+ type ClosingPullRequestPage = {
97
+ data?: { repository?: { issue?: { closedByPullRequestsReferences?: {
98
+ nodes?: ClosingPullRequestNode[];
99
+ pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
100
+ } } } };
57
101
  };
58
102
 
59
103
  function result(
@@ -71,12 +115,33 @@ function result(
71
115
  };
72
116
  }
73
117
 
118
+ function externalResult(
119
+ base: PullRequestResult,
120
+ overrides: Partial<ExternalPullRequestResult> = {}
121
+ ): ExternalPullRequestResult {
122
+ return {
123
+ ...base,
124
+ mode: null,
125
+ authorization: null,
126
+ candidates: [],
127
+ eligible: [],
128
+ selected: null,
129
+ ...overrides
130
+ };
131
+ }
132
+
74
133
  function normalizePullRequest(remote: RemotePullRequest, repository: string): PullRequestSnapshot | null {
75
134
  const number = Number(remote.number);
76
135
  const headRepository = remote.head?.repo?.full_name;
77
136
  const baseRepository = remote.base?.repo?.full_name;
78
137
  if (!Number.isInteger(number) || number <= 0 || !remote.node_id || !remote.html_url ||
79
138
  !remote.head?.ref || !remote.head.sha || !headRepository || !remote.base?.ref || !baseRepository) return null;
139
+ const mergeabilityDetail = remote.mergeable_state?.trim().toLowerCase() || null;
140
+ const mergeability = remote.mergeable === false
141
+ ? { state: 'conflicting' as const, detail: mergeabilityDetail }
142
+ : remote.mergeable === true && mergeabilityDetail !== 'dirty'
143
+ ? { state: 'mergeable' as const, detail: mergeabilityDetail }
144
+ : { state: 'unknown' as const, detail: mergeabilityDetail };
80
145
  return {
81
146
  repository,
82
147
  number,
@@ -92,8 +157,149 @@ function normalizePullRequest(remote: RemotePullRequest, repository: string): Pu
92
157
  mergeCommitSha: remote.merge_commit_sha || null,
93
158
  labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
94
159
  assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
95
- milestone: remote.milestone?.title || null
160
+ milestone: remote.milestone?.title || null,
161
+ mergeability
162
+ };
163
+ }
164
+
165
+ function normalizeClosingPullRequest(node: ClosingPullRequestNode, repository: string): PullRequestSnapshot | null {
166
+ return normalizePullRequest({
167
+ number: node.number,
168
+ node_id: node.id,
169
+ html_url: node.url,
170
+ state: node.state === 'OPEN' ? 'open' : 'closed',
171
+ title: node.title,
172
+ body: node.body,
173
+ draft: node.isDraft,
174
+ head: { ref: node.headRefName, sha: node.headRefOid, repo: node.headRepository ? { full_name: node.headRepository.nameWithOwner } : null },
175
+ base: { ref: node.baseRefName, sha: node.baseRefOid, repo: node.baseRepository ? { full_name: node.baseRepository.nameWithOwner } : null },
176
+ merged_at: node.mergedAt,
177
+ merge_commit_sha: node.mergeCommit?.oid,
178
+ labels: node.labels?.nodes,
179
+ assignees: node.assignees?.nodes,
180
+ milestone: node.milestone
181
+ }, repository);
182
+ }
183
+
184
+ function repositoryKey(repository: string): string {
185
+ return repository.trim().toLowerCase();
186
+ }
187
+
188
+ function identitySignature(pullRequest: PullRequestSnapshot): string {
189
+ return [
190
+ pullRequest.repository, pullRequest.number, pullRequest.nodeId, pullRequest.url,
191
+ pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
192
+ pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
193
+ pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
194
+ ].join('|');
195
+ }
196
+
197
+ function hasCompleteExternalIdentity(pullRequest: PullRequestSnapshot): boolean {
198
+ const required = [
199
+ pullRequest.repository, pullRequest.nodeId, pullRequest.url,
200
+ pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
201
+ pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
202
+ ];
203
+ return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
204
+ required.every((value) => Boolean(value?.trim())) &&
205
+ Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
206
+ }
207
+
208
+ function selectExternalPullRequest(
209
+ candidates: PullRequestSnapshot[],
210
+ repository: string,
211
+ existingPrNumber: number | null,
212
+ explicitPrNumber: number | null
213
+ ): ExternalPullRequestSelection {
214
+ const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
215
+ if (invalid) return {
216
+ status: 'failed', code: 'PR_IDENTITY_INVALID',
217
+ message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
218
+ };
219
+ const byNumber = new Map<number, string>();
220
+ for (const candidate of candidates) {
221
+ const signature = identitySignature(candidate);
222
+ const prior = byNumber.get(candidate.number);
223
+ if (prior && prior !== signature) return {
224
+ status: 'failed', code: 'PR_IDENTITY_INVALID',
225
+ message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
226
+ };
227
+ byNumber.set(candidate.number, signature);
228
+ }
229
+ const unique = candidates.filter((candidate, index) =>
230
+ candidates.findIndex((other) => other.number === candidate.number) === index
231
+ );
232
+ const wantedRepository = repositoryKey(repository);
233
+ const eligible = unique.filter((candidate) =>
234
+ repositoryKey(candidate.repository) === wantedRepository &&
235
+ repositoryKey(candidate.base.repository) === wantedRepository &&
236
+ Boolean(candidate.mergedAt && candidate.mergeCommitSha)
237
+ );
238
+ if (explicitPrNumber !== null) {
239
+ const selected = unique.find((candidate) => candidate.number === explicitPrNumber);
240
+ if (!selected) return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR #${explicitPrNumber} is not a closing PR`, candidates, eligible };
241
+ if (!eligible.some((candidate) => candidate.number === explicitPrNumber)) {
242
+ return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR #${explicitPrNumber} is not an eligible merged PR`, candidates, eligible };
243
+ }
244
+ if (existingPrNumber !== null && existingPrNumber !== explicitPrNumber) {
245
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
246
+ }
247
+ return { status: 'selected', source: 'explicit', selected, candidates, eligible };
248
+ }
249
+ if (existingPrNumber !== null && eligible.length === 1 && existingPrNumber !== eligible[0]!.number) {
250
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
251
+ }
252
+ if (eligible.length === 0) return { status: 'normal', candidates, eligible: [] };
253
+ if (eligible.length > 1) return {
254
+ status: 'failed', code: 'PR_IDENTITY_AMBIGUOUS',
255
+ message: 'Multiple eligible merged closing pull requests were found', candidates, eligible
256
+ };
257
+ return { status: 'selected', source: 'unique', selected: eligible[0]!, candidates, eligible };
258
+ }
259
+
260
+ 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}}}}}`;
261
+
262
+ function inspectGitHubIssueClosingChangeRequests(
263
+ client: GitHubClient,
264
+ repository: string,
265
+ issueNumber: number,
266
+ cwd: string
267
+ ) {
268
+ const [owner, name] = repository.split('/');
269
+ if (!owner || !name || !Number.isInteger(issueNumber) || issueNumber <= 0) return {
270
+ ok: false as const,
271
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Repository or Issue identity is invalid', retryable: false }
96
272
  };
273
+ const candidates: PullRequestSnapshot[] = [];
274
+ let cursor: string | null = null;
275
+ for (;;) {
276
+ const args = [
277
+ 'api', 'graphql', '-f', `query=${CLOSING_PULL_REQUESTS_QUERY}`,
278
+ '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `issue=${issueNumber}`
279
+ ];
280
+ if (cursor) args.push('-F', `cursor=${cursor}`);
281
+ const response = client.json<ClosingPullRequestPage>(args, { cwd });
282
+ if (!response.ok) return response;
283
+ const connection = response.value?.data?.repository?.issue?.closedByPullRequestsReferences;
284
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo) return {
285
+ ok: false as const,
286
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request response is incomplete', retryable: false }
287
+ };
288
+ for (const node of connection.nodes) {
289
+ const normalized = normalizeClosingPullRequest(node, repository);
290
+ if (!normalized) return {
291
+ ok: false as const,
292
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request identity is incomplete', retryable: false }
293
+ };
294
+ candidates.push(normalized);
295
+ }
296
+ if (!connection.pageInfo.hasNextPage) return { ok: true as const, value: candidates };
297
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) return {
298
+ ok: false as const,
299
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request pagination cursor is invalid', retryable: false }
300
+ };
301
+ cursor = connection.pageInfo.endCursor;
302
+ }
97
303
  }
98
304
 
99
305
  function expectedHead(repository: string, head: string): { repository: string; ref: string } {
@@ -246,6 +452,11 @@ registerPlatformCapabilities('github', {
246
452
  inspectChangeRequest({ client, repository, number, cwd }) {
247
453
  return inspectGitHubPullRequest((client as GitHubClient | undefined) || createGitHubClient(), repository, number, cwd);
248
454
  },
455
+ inspectIssueClosingChangeRequests({ client, repository, issueNumber, cwd }) {
456
+ return inspectGitHubIssueClosingChangeRequests(
457
+ (client as GitHubClient | undefined) || createGitHubClient(), repository, issueNumber, cwd
458
+ );
459
+ },
249
460
  resolveChangeRequestGitEvidence(context) {
250
461
  return resolveGitHubChangeRequestGitEvidence(context);
251
462
  }
@@ -297,6 +508,44 @@ function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions
297
508
  });
298
509
  }
299
510
 
511
+ function inspectPlatformPullRequestByNumber(prNumber: number, options: InspectionOptions = {}): PullRequestResult {
512
+ const client = (options.client as GitHubClient | undefined) || createGitHubClient();
513
+ const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
514
+ const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
515
+ if (!usable) {
516
+ return result(context.status, null, null, null, {
517
+ platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
518
+ });
519
+ }
520
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
521
+ return result('failed', null, null, null, {
522
+ platform: context.platform, capabilities: context.capabilities,
523
+ error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
524
+ });
525
+ }
526
+ const fetched = inspectPlatformChangeRequest(context.platform.type, {
527
+ cwd: options.cwd || process.cwd(),
528
+ repository: context.platform.repository!,
529
+ number: prNumber,
530
+ client
531
+ });
532
+ if (!fetched.ok || !fetched.value) {
533
+ const error = fetched.error || {
534
+ code: 'PR_INSPECTION_INVALID',
535
+ message: 'Platform adapter returned no change-request snapshot',
536
+ retryable: false
537
+ };
538
+ return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
539
+ platform: context.platform, capabilities: context.capabilities,
540
+ resource: { kind: 'pull-request', number: prNumber }, error
541
+ });
542
+ }
543
+ return result('no-op', null, null, prNumber, {
544
+ platform: context.platform, capabilities: context.capabilities,
545
+ resource: { kind: 'pull-request', number: prNumber }, pullRequest: fetched.value, error: null
546
+ });
547
+ }
548
+
300
549
  function appendActivity(content: string, line: string): string {
301
550
  const body = extractSection(content, ['活动日志', 'Activity Log']);
302
551
  return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
@@ -358,6 +607,146 @@ function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullReq
358
607
  });
359
608
  }
360
609
 
610
+ function externalEvidenceNote(
611
+ issueNumber: number,
612
+ source: 'unique' | 'explicit',
613
+ pullRequest: PullRequestSnapshot
614
+ ): string {
615
+ return [
616
+ `authorization=${source}`,
617
+ externalIdentityEvidenceNote(issueNumber, pullRequest)
618
+ ].join('; ');
619
+ }
620
+
621
+ function externalIdentityEvidenceNote(
622
+ issueNumber: number,
623
+ pullRequest: PullRequestSnapshot
624
+ ): string {
625
+ return [
626
+ `issue=#${issueNumber}`,
627
+ `pr=#${pullRequest.number}`,
628
+ `url=${pullRequest.url}`,
629
+ `base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
630
+ `head=${pullRequest.head.repository}:${pullRequest.head.ref}@${pullRequest.head.sha}`,
631
+ `mergedAt=${pullRequest.mergedAt}`,
632
+ `mergeCommitSha=${pullRequest.mergeCommitSha}`
633
+ ].join('; ');
634
+ }
635
+
636
+ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): ExternalPullRequestResult {
637
+ const base = resolvedContext(taskRef, options);
638
+ if (!base.ok) return externalResult(base.output);
639
+ const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
640
+ if (inventory.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
641
+ platform: base.context.platform,
642
+ capabilities: base.context.capabilities,
643
+ error: { code: inventory.error?.code || 'ARTIFACT_TOPOLOGY_CONFLICT', message: inventory.error?.message || 'Completion artifact inventory failed', retryable: false }
644
+ }));
645
+ if (inventory.artifacts.length > 0) {
646
+ if (options.pr !== undefined) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
647
+ platform: base.context.platform,
648
+ capabilities: base.context.capabilities,
649
+ error: { code: 'EXTERNAL_DELIVERY_NOT_APPLICABLE', message: 'External PR selection is only valid when the completion inventory is empty', retryable: false }
650
+ }), { mode: 'normal' });
651
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
652
+ platform: base.context.platform,
653
+ capabilities: base.context.capabilities,
654
+ operations: [{ name: 'external-pr:inventory', status: 'no-op', reasonCode: 'CANONICAL_ARTIFACTS_PRESENT' }],
655
+ error: null
656
+ }), { mode: 'normal' });
657
+ }
658
+ if (!base.issueNumber) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
659
+ platform: base.context.platform,
660
+ capabilities: base.context.capabilities,
661
+ error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
662
+ }));
663
+ const inspected = inspectPlatformIssueClosingChangeRequests(base.context.platform.type, {
664
+ cwd: base.resolved.repoRoot,
665
+ repository: base.context.platform.repository!,
666
+ issueNumber: base.issueNumber,
667
+ client: base.client
668
+ });
669
+ if (!inspected.ok || !inspected.value) {
670
+ const error = inspected.error || { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
671
+ return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
672
+ platform: base.context.platform, capabilities: base.context.capabilities, error
673
+ }));
674
+ }
675
+ const selected = selectExternalPullRequest(
676
+ inspected.value,
677
+ base.context.platform.repository!,
678
+ base.prNumber,
679
+ options.pr ?? null
680
+ );
681
+ if (selected.status === 'normal') return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
682
+ platform: base.context.platform,
683
+ capabilities: base.context.capabilities,
684
+ operations: [{ name: 'external-pr:select', status: 'no-op', reasonCode: 'NO_ELIGIBLE_CANDIDATE' }],
685
+ error: null
686
+ }), { mode: 'normal', candidates: selected.candidates, eligible: selected.eligible });
687
+ if (selected.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
688
+ platform: base.context.platform,
689
+ capabilities: base.context.capabilities,
690
+ error: { code: selected.code, message: selected.message, retryable: false }
691
+ }), { candidates: selected.candidates, eligible: selected.eligible });
692
+
693
+ const note = externalEvidenceNote(base.issueNumber, selected.source, selected.selected);
694
+ const identityNote = externalIdentityEvidenceNote(base.issueNumber, selected.selected);
695
+ const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
696
+ const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
697
+ const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
698
+ if (auditLines.length > 0 && !alreadyAudited) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
699
+ platform: base.context.platform,
700
+ capabilities: base.context.capabilities,
701
+ pullRequest: selected.selected,
702
+ error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
703
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
704
+ if (alreadyAudited && base.prNumber !== selected.selected.number) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
705
+ platform: base.context.platform,
706
+ capabilities: base.context.capabilities,
707
+ pullRequest: selected.selected,
708
+ error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
709
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
710
+ if (base.prNumber === selected.selected.number && alreadyAudited) {
711
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
712
+ platform: base.context.platform,
713
+ capabilities: base.context.capabilities,
714
+ resource: { kind: 'pull-request', number: selected.selected.number },
715
+ pullRequest: selected.selected,
716
+ operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
717
+ error: null
718
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
719
+ }
720
+ const metadata = captureTaskWriteMetadata();
721
+ const write = writeTask({
722
+ taskRef: base.resolved.taskId,
723
+ expectedState: 'active',
724
+ dryRun: options.dryRun,
725
+ mutations: [
726
+ { kind: 'frontmatter', set: { pr_number: selected.selected.number, pr_status: 'created', assigned_to: options.agent } },
727
+ { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
728
+ base.content, `- ${metadata.timestamp} — ${activityEntry}`
729
+ ) }
730
+ ]
731
+ }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
732
+ if (write.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
733
+ platform: base.context.platform,
734
+ capabilities: base.context.capabilities,
735
+ pullRequest: selected.selected,
736
+ error: { code: write.error.code, message: write.error.message, retryable: false }
737
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
738
+ const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
739
+ return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selected.selected.number, {
740
+ changed: !options.dryRun && write.changed,
741
+ platform: base.context.platform,
742
+ capabilities: base.context.capabilities,
743
+ resource: { kind: 'pull-request', number: selected.selected.number },
744
+ pullRequest: selected.selected,
745
+ operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
746
+ error: null
747
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
748
+ }
749
+
361
750
  function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
362
751
  const base = resolvedContext(taskRef, options);
363
752
  if (!base.ok) return base.output;
@@ -486,10 +875,14 @@ export {
486
875
  bindPlatformPullRequest,
487
876
  createPlatformPullRequest,
488
877
  inspectGitHubPullRequest,
878
+ inspectGitHubIssueClosingChangeRequests,
489
879
  inspectPlatformPullRequest,
880
+ inspectPlatformPullRequestByNumber,
490
881
  normalizePullRequest,
491
882
  resolveGitHubChangeRequestGitEvidence,
883
+ resolveExternalPullRequest,
884
+ selectExternalPullRequest,
492
885
  selectPullRequest,
493
886
  syncPlatformPullRequest
494
887
  };
495
- export type { BindOptions, CreateOptions, PullRequestResult, PullRequestSnapshot, SyncOptions };
888
+ export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
@@ -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
 
5
6
  import { resolvePlatformContext } from './context.ts';
6
7
  import {
@@ -24,6 +25,86 @@ const unsupportedError = {
24
25
  retryable: false
25
26
  };
26
27
 
28
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
29
+
30
+ function releaseNoteFailure(code: string, message: string) {
31
+ return {
32
+ status: 'failed' as const,
33
+ changed: false,
34
+ operation: null,
35
+ url: null,
36
+ error: { code, message, retryable: false }
37
+ };
38
+ }
39
+
40
+ function normalizeReleaseNoteBytes(input: Uint8Array): Buffer {
41
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(input);
42
+ const normalized = text.replace(/\r\n?/g, '\n').replace(/\n*$/, '') + '\n';
43
+ return Buffer.from(normalized, 'utf8');
44
+ }
45
+
46
+ function releaseNoteSha256(bytes: Uint8Array): string {
47
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
48
+ }
49
+
50
+ function isWithin(parent: string, candidate: string): boolean {
51
+ const relative = path.relative(parent, candidate);
52
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
53
+ }
54
+
55
+ function stageReleaseNotes(
56
+ input: { notesFile: string },
57
+ options: Pick<ReleaseNoteOptions, 'cwd'> = {}
58
+ ) {
59
+ if (!input.notesFile || input.notesFile === '-') {
60
+ return { status: 'failed' as const, 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 } };
61
+ }
62
+ const requestedPath = path.resolve(input.notesFile);
63
+ let notesFile: string;
64
+ let sourceStat: fs.Stats;
65
+ try {
66
+ if (fs.lstatSync(requestedPath).isSymbolicLink()) throw new Error('symbolic links are not allowed');
67
+ notesFile = fs.realpathSync(requestedPath);
68
+ sourceStat = fs.statSync(notesFile);
69
+ const worktree = fs.realpathSync(path.resolve(options.cwd || process.cwd()));
70
+ if (!sourceStat.isFile() || isWithin(worktree, notesFile)) throw new Error('path is not an external regular file');
71
+ } catch {
72
+ return { status: 'failed' as const, 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 } };
73
+ }
74
+
75
+ let original: Buffer;
76
+ let normalized: Buffer;
77
+ try {
78
+ original = fs.readFileSync(notesFile);
79
+ normalized = normalizeReleaseNoteBytes(original);
80
+ } catch {
81
+ return { status: 'failed' as const, changed: false, notesFile, sha256: null, byteLength: null, error: { code: 'RELEASE_NOTES_CONTENT_INVALID', message: 'notes-file must contain valid UTF-8', retryable: false } };
82
+ }
83
+
84
+ const changed = !original.equals(normalized);
85
+ if (changed) {
86
+ const temporary = path.join(
87
+ path.dirname(notesFile),
88
+ `.${path.basename(notesFile)}.${process.pid}.${Date.now()}.tmp`
89
+ );
90
+ try {
91
+ fs.writeFileSync(temporary, normalized, { flag: 'wx', mode: sourceStat.mode });
92
+ fs.renameSync(temporary, notesFile);
93
+ } catch {
94
+ fs.rmSync(temporary, { force: true });
95
+ return { status: 'failed' as const, changed: false, notesFile, sha256: null, byteLength: null, error: { code: 'RELEASE_NOTES_STAGE_FAILED', message: 'notes-file could not be staged atomically', retryable: false } };
96
+ }
97
+ }
98
+ return {
99
+ status: changed ? 'applied' as const : 'no-op' as const,
100
+ changed,
101
+ notesFile,
102
+ sha256: releaseNoteSha256(normalized),
103
+ byteLength: normalized.byteLength,
104
+ error: null
105
+ };
106
+ }
107
+
27
108
  function baseResult(status: 'no-op' | 'failed' | 'blocked') {
28
109
  return {
29
110
  status,
@@ -148,11 +229,20 @@ function releaseNoteContext(
148
229
  }
149
230
 
150
231
  function publishReleaseNotes(
151
- input: { tag: string; title: string; notesFile: string; dryRun?: boolean },
232
+ input: { tag: string; title: string; notesFile: string; expectedSha256: string; dryRun?: boolean },
152
233
  options: ReleaseNoteOptions = {}
153
234
  ) {
154
- if (!input.tag || !input.title || !input.notesFile) {
155
- return { status: 'failed' as const, changed: false, operation: null, url: null, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'tag, title, and notes-file are required', retryable: false } };
235
+ if (!input.tag || !input.title || !input.notesFile || !SHA256_PATTERN.test(input.expectedSha256)) {
236
+ return releaseNoteFailure('RELEASE_NOTES_INPUT_INVALID', 'tag, title, notes-file, and expected-sha256 are required');
237
+ }
238
+ let actualSha256: string;
239
+ try {
240
+ actualSha256 = releaseNoteSha256(fs.readFileSync(input.notesFile));
241
+ } catch {
242
+ return releaseNoteFailure('RELEASE_NOTES_FILE_UNREADABLE', 'notes-file could not be read');
243
+ }
244
+ if (actualSha256 !== input.expectedSha256) {
245
+ return releaseNoteFailure('RELEASE_NOTES_DIGEST_MISMATCH', 'notes-file does not match the confirmed SHA-256 digest');
156
246
  }
157
247
  const cwd = path.resolve(options.cwd || process.cwd());
158
248
  const platformType = options.platformType ?? configuredPlatform(cwd);
@@ -164,4 +254,10 @@ function publishReleaseNotes(
164
254
  return publishGitHubReleaseNotes({ ...input, repository: context.platform.repository }, { cwd, client: options.client });
165
255
  }
166
256
 
167
- export { publishReleaseNotes, releaseNoteContext };
257
+ export {
258
+ normalizeReleaseNoteBytes,
259
+ publishReleaseNotes,
260
+ releaseNoteContext,
261
+ releaseNoteSha256,
262
+ stageReleaseNotes
263
+ };