@fitlab-ai/agent-infra 0.9.2 → 0.9.4

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 (428) hide show
  1. package/README.md +12 -9
  2. package/README.zh-CN.md +12 -9
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -108
  6. package/dist/lib/agent-clients/adapter.js +137 -4
  7. package/dist/lib/agent-clients/adapters/antigravity-cli.js +64 -0
  8. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  9. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  10. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  11. package/dist/lib/agent-clients/adapters/claude-code.js +27 -4
  12. package/dist/lib/agent-clients/adapters/codex.js +9 -0
  13. package/dist/lib/agent-clients/adapters/index.js +2 -2
  14. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  15. package/dist/lib/agent-clients/adapters/opencode.js +24 -3
  16. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  17. package/dist/lib/agent-clients/config.js +42 -4
  18. package/dist/lib/agent-clients/next-steps.js +11 -2
  19. package/dist/lib/agent-clients/reconcile.js +2 -1
  20. package/dist/lib/agent-clients/registry.js +16 -2
  21. package/dist/lib/agent-clients/schema.js +18 -0
  22. package/dist/lib/agent-clients/tokens.js +49 -0
  23. package/dist/lib/agent-clients/types.js +1 -1
  24. package/dist/lib/defaults.json +1 -1
  25. package/dist/lib/git/workflow.js +54 -1
  26. package/dist/lib/internal/agent-client.js +61 -4
  27. package/dist/lib/internal/git-workflow.js +6 -1
  28. package/dist/lib/internal/platform-checks.js +3 -3
  29. package/dist/lib/internal/platform-comment.js +29 -2
  30. package/dist/lib/internal/platform-issue.js +7 -0
  31. package/dist/lib/internal/platform-pr-review.js +132 -0
  32. package/dist/lib/internal/platform-pr.js +20 -2
  33. package/dist/lib/internal/platform-release-notes.js +52 -16
  34. package/dist/lib/internal/pr-review-grade.js +140 -0
  35. package/dist/lib/internal/release-workflow.js +13 -6
  36. package/dist/lib/internal/sandbox-control.js +24 -0
  37. package/dist/lib/internal/task-activity.js +105 -0
  38. package/dist/lib/internal/task-event.js +17 -1
  39. package/dist/lib/internal/task-ledger.js +12 -3
  40. package/dist/lib/internal/task-lifecycle.js +26 -1
  41. package/dist/lib/internal/task-orchestration.js +167 -18
  42. package/dist/lib/internal/task-review.js +8 -3
  43. package/dist/lib/platform/adapters.js +8 -1
  44. package/dist/lib/platform/completion-backfill.js +122 -0
  45. package/dist/lib/platform/github-client.js +19 -2
  46. package/dist/lib/platform/issue-comments.js +3 -2
  47. package/dist/lib/platform/merged-pr-equivalence.js +115 -5
  48. package/dist/lib/platform/pr-checks.js +65 -42
  49. package/dist/lib/platform/pr-review.js +166 -0
  50. package/dist/lib/platform/pull-requests.js +371 -4
  51. package/dist/lib/platform/release-notes.js +85 -3
  52. package/dist/lib/platform/releases.js +13 -5
  53. package/dist/lib/pr-review/evidence-grading.js +229 -0
  54. package/dist/lib/process-data/index.js +239 -0
  55. package/dist/lib/process-data/normalize.js +198 -0
  56. package/dist/lib/process-data/privacy.js +33 -0
  57. package/dist/lib/process-data/reconcile.js +95 -0
  58. package/dist/lib/process-data/repair.js +58 -0
  59. package/dist/lib/process-data/sources.js +345 -0
  60. package/dist/lib/process-data/store.js +282 -0
  61. package/dist/lib/process-data/types.js +2 -0
  62. package/dist/lib/run/index.js +4 -3
  63. package/dist/lib/run/tui.js +10 -13
  64. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  65. package/dist/lib/sandbox/capture.js +7 -2
  66. package/dist/lib/sandbox/commands/create.js +109 -248
  67. package/dist/lib/sandbox/commands/enter.js +15 -47
  68. package/dist/lib/sandbox/commands/list-running.js +16 -6
  69. package/dist/lib/sandbox/commands/ls.js +10 -6
  70. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  71. package/dist/lib/sandbox/commands/refresh.js +4 -84
  72. package/dist/lib/sandbox/commands/rm.js +24 -3
  73. package/dist/lib/sandbox/commands/show.js +13 -4
  74. package/dist/lib/sandbox/commands/start.js +7 -4
  75. package/dist/lib/sandbox/config.js +2 -0
  76. package/dist/lib/sandbox/constants.js +6 -0
  77. package/dist/lib/sandbox/control/client.js +44 -0
  78. package/dist/lib/sandbox/control/protocol.js +35 -0
  79. package/dist/lib/sandbox/control/server.js +109 -0
  80. package/dist/lib/sandbox/dockerfile.js +36 -13
  81. package/dist/lib/sandbox/mounts.js +17 -4
  82. package/dist/lib/sandbox/recovery.js +128 -5
  83. package/dist/lib/sandbox/redaction.js +12 -0
  84. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  85. package/dist/lib/sandbox/workspace-identity.js +95 -0
  86. package/dist/lib/sandbox/workspace-view.js +103 -0
  87. package/dist/lib/task/activity-intent.js +307 -0
  88. package/dist/lib/task/artifact-lifecycle.js +2 -1
  89. package/dist/lib/task/commands/log.js +15 -14
  90. package/dist/lib/task/commit-finalization.js +267 -0
  91. package/dist/lib/task/commit-intent.js +165 -0
  92. package/dist/lib/task/decision-intents.js +31 -17
  93. package/dist/lib/task/delegation-receipts.js +25 -4
  94. package/dist/lib/task/events.js +67 -28
  95. package/dist/lib/task/finalization-artifacts.js +35 -0
  96. package/dist/lib/task/implementation-inputs.js +54 -7
  97. package/dist/lib/task/ledger-intents.js +35 -1
  98. package/dist/lib/task/lifecycle-execution.js +46 -0
  99. package/dist/lib/task/orchestration.js +753 -54
  100. package/dist/lib/task/resolve-ref.js +41 -1
  101. package/dist/lib/task/review-finalization.js +29 -9
  102. package/dist/lib/task/task-execution-lock.js +231 -0
  103. package/dist/lib/task/verification-checks.js +1 -1
  104. package/dist/lib/task/verification-engine.js +218 -2
  105. package/dist/lib/task/verification.js +4 -3
  106. package/dist/lib/task/workspace-snapshot.js +17 -5
  107. package/lib/agent-clients/adapter.ts +201 -4
  108. package/lib/agent-clients/adapters/antigravity-cli.ts +65 -0
  109. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  110. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  111. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  112. package/lib/agent-clients/adapters/claude-code.ts +31 -4
  113. package/lib/agent-clients/adapters/codex.ts +9 -0
  114. package/lib/agent-clients/adapters/index.ts +2 -2
  115. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  116. package/lib/agent-clients/adapters/opencode.ts +27 -3
  117. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  118. package/lib/agent-clients/config.ts +49 -4
  119. package/lib/agent-clients/next-steps.ts +15 -2
  120. package/lib/agent-clients/reconcile.ts +2 -1
  121. package/lib/agent-clients/registry.ts +19 -1
  122. package/lib/agent-clients/schema.ts +18 -0
  123. package/lib/agent-clients/tokens.ts +53 -0
  124. package/lib/agent-clients/types.ts +15 -1
  125. package/lib/defaults.json +1 -1
  126. package/lib/git/workflow.ts +66 -2
  127. package/lib/internal/agent-client.ts +59 -4
  128. package/lib/internal/git-workflow.ts +5 -1
  129. package/lib/internal/platform-checks.ts +4 -4
  130. package/lib/internal/platform-comment.ts +17 -2
  131. package/lib/internal/platform-issue.ts +4 -0
  132. package/lib/internal/platform-pr-review.ts +104 -0
  133. package/lib/internal/platform-pr.ts +14 -1
  134. package/lib/internal/platform-release-notes.ts +59 -16
  135. package/lib/internal/pr-review-grade.ts +130 -0
  136. package/lib/internal/release-workflow.ts +13 -6
  137. package/lib/internal/sandbox-control.ts +24 -0
  138. package/lib/internal/task-activity.ts +91 -0
  139. package/lib/internal/task-event.ts +9 -1
  140. package/lib/internal/task-ledger.ts +12 -3
  141. package/lib/internal/task-lifecycle.ts +26 -1
  142. package/lib/internal/task-orchestration.ts +123 -14
  143. package/lib/internal/task-review.ts +8 -3
  144. package/lib/platform/adapters.ts +29 -2
  145. package/lib/platform/completion-backfill.ts +134 -0
  146. package/lib/platform/github-client.ts +20 -2
  147. package/lib/platform/issue-comments.ts +3 -2
  148. package/lib/platform/merged-pr-equivalence.ts +127 -5
  149. package/lib/platform/pr-checks.ts +73 -46
  150. package/lib/platform/pr-review.ts +201 -0
  151. package/lib/platform/pull-requests.ts +444 -3
  152. package/lib/platform/release-notes.ts +100 -4
  153. package/lib/platform/releases.ts +16 -4
  154. package/lib/pr-review/evidence-grading.ts +318 -0
  155. package/lib/process-data/index.ts +224 -0
  156. package/lib/process-data/normalize.ts +202 -0
  157. package/lib/process-data/privacy.ts +41 -0
  158. package/lib/process-data/reconcile.ts +110 -0
  159. package/lib/process-data/repair.ts +64 -0
  160. package/lib/process-data/sources.ts +354 -0
  161. package/lib/process-data/store.ts +315 -0
  162. package/lib/process-data/types.ts +135 -0
  163. package/lib/run/index.ts +5 -3
  164. package/lib/run/tui.ts +14 -12
  165. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  166. package/lib/sandbox/capture.ts +9 -2
  167. package/lib/sandbox/commands/create.ts +126 -280
  168. package/lib/sandbox/commands/enter.ts +13 -59
  169. package/lib/sandbox/commands/list-running.ts +23 -6
  170. package/lib/sandbox/commands/ls.ts +22 -6
  171. package/lib/sandbox/commands/rebuild.ts +4 -3
  172. package/lib/sandbox/commands/refresh.ts +4 -126
  173. package/lib/sandbox/commands/rm.ts +24 -3
  174. package/lib/sandbox/commands/show.ts +30 -4
  175. package/lib/sandbox/commands/start.ts +6 -4
  176. package/lib/sandbox/config.ts +4 -0
  177. package/lib/sandbox/constants.ts +8 -0
  178. package/lib/sandbox/control/client.ts +55 -0
  179. package/lib/sandbox/control/protocol.ts +75 -0
  180. package/lib/sandbox/control/server.ts +117 -0
  181. package/lib/sandbox/dockerfile.ts +66 -13
  182. package/lib/sandbox/mounts.ts +24 -4
  183. package/lib/sandbox/recovery.ts +163 -6
  184. package/lib/sandbox/redaction.ts +14 -0
  185. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  186. package/lib/sandbox/tool-types.ts +16 -0
  187. package/lib/sandbox/workspace-identity.ts +123 -0
  188. package/lib/sandbox/workspace-view.ts +143 -0
  189. package/lib/server/server.schema.json +2 -2
  190. package/lib/task/activity-intent.ts +417 -0
  191. package/lib/task/artifact-lifecycle.ts +2 -1
  192. package/lib/task/commands/log.ts +15 -14
  193. package/lib/task/commit-finalization.ts +356 -0
  194. package/lib/task/commit-intent.ts +230 -0
  195. package/lib/task/decision-intents.ts +32 -12
  196. package/lib/task/delegation-receipts.ts +34 -5
  197. package/lib/task/events.ts +75 -30
  198. package/lib/task/finalization-artifacts.ts +58 -0
  199. package/lib/task/implementation-inputs.ts +62 -8
  200. package/lib/task/ledger-intents.ts +43 -3
  201. package/lib/task/lifecycle-execution.ts +106 -0
  202. package/lib/task/orchestration.ts +1019 -72
  203. package/lib/task/resolve-ref.ts +46 -1
  204. package/lib/task/review-finalization.ts +40 -9
  205. package/lib/task/task-execution-lock.ts +296 -0
  206. package/lib/task/verification-checks.ts +1 -1
  207. package/lib/task/verification-engine.ts +249 -2
  208. package/lib/task/verification.ts +5 -3
  209. package/lib/task/workspace-snapshot.ts +26 -5
  210. package/package.json +3 -3
  211. package/templates/.agents/QUICKSTART.en.md +25 -8
  212. package/templates/.agents/QUICKSTART.zh-CN.md +25 -8
  213. package/templates/.agents/README.en.md +10 -7
  214. package/templates/.agents/README.zh-CN.md +10 -7
  215. package/templates/.agents/hooks/lifecycle-delegation.js +15 -4
  216. package/templates/.agents/rules/README.en.md +1 -1
  217. package/templates/.agents/rules/README.zh-CN.md +1 -1
  218. package/templates/.agents/rules/create-issue.github.en.md +4 -2
  219. package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -2
  220. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  221. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  222. package/templates/.agents/rules/issue-fields.github.en.md +3 -1
  223. package/templates/.agents/rules/issue-fields.github.zh-CN.md +3 -1
  224. package/templates/.agents/rules/issue-pr-commands.en.md +5 -3
  225. package/templates/.agents/rules/issue-pr-commands.github.en.md +5 -3
  226. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +5 -3
  227. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -3
  228. package/templates/.agents/rules/issue-sync.github.en.md +7 -3
  229. package/templates/.agents/rules/issue-sync.github.zh-CN.md +8 -4
  230. package/templates/.agents/rules/lifecycle-orchestration.en.md +5 -2
  231. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +5 -2
  232. package/templates/.agents/rules/milestone-inference.github.en.md +4 -2
  233. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -2
  234. package/templates/.agents/rules/next-step-output.en.md +3 -1
  235. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -1
  236. package/templates/.agents/rules/no-mid-flow-questions.en.md +17 -0
  237. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +17 -0
  238. package/templates/.agents/rules/pr-checks-commands.en.md +3 -3
  239. package/templates/.agents/rules/pr-checks-commands.github.en.md +2 -2
  240. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +3 -3
  241. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +3 -3
  242. package/templates/.agents/rules/pr-sync.en.md +3 -1
  243. package/templates/.agents/rules/pr-sync.github.en.md +11 -1
  244. package/templates/.agents/rules/pr-sync.github.zh-CN.md +11 -1
  245. package/templates/.agents/rules/pr-sync.zh-CN.md +3 -1
  246. package/templates/.agents/rules/release-commands.en.md +1 -1
  247. package/templates/.agents/rules/release-commands.github.en.md +12 -2
  248. package/templates/.agents/rules/release-commands.github.zh-CN.md +12 -2
  249. package/templates/.agents/rules/release-commands.zh-CN.md +1 -1
  250. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  251. package/templates/.agents/rules/task-management.en.md +13 -0
  252. package/templates/.agents/rules/task-management.zh-CN.md +13 -0
  253. package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -7
  254. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -7
  255. package/templates/.agents/skills/block-task/SKILL.en.md +7 -5
  256. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +7 -5
  257. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -4
  258. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -4
  259. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
  260. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
  261. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
  262. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
  263. package/templates/.agents/skills/code-task/SKILL.en.md +10 -7
  264. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +12 -9
  265. package/templates/.agents/skills/commit/SKILL.en.md +23 -11
  266. package/templates/.agents/skills/commit/SKILL.zh-CN.md +29 -11
  267. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  268. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  269. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  270. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  271. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  272. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  273. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  274. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  275. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  276. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  277. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +6 -4
  278. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +6 -4
  279. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  280. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  281. package/templates/.agents/skills/complete-task/SKILL.en.md +22 -7
  282. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +22 -7
  283. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  284. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  285. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +17 -0
  286. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +17 -0
  287. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -5
  288. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -5
  289. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  290. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  291. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -16
  292. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -16
  293. package/templates/.agents/skills/create-task/SKILL.en.md +5 -3
  294. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -3
  295. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  296. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  297. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -3
  298. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -3
  299. package/templates/.agents/skills/plan-task/SKILL.en.md +8 -5
  300. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +8 -5
  301. package/templates/.agents/skills/post-release/SKILL.en.md +2 -0
  302. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +2 -0
  303. package/templates/.agents/skills/release/SKILL.en.md +16 -10
  304. package/templates/.agents/skills/release/SKILL.zh-CN.md +16 -10
  305. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -1
  306. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -1
  307. package/templates/.agents/skills/review-analysis/SKILL.en.md +7 -4
  308. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +7 -4
  309. package/templates/.agents/skills/review-code/SKILL.en.md +11 -8
  310. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -8
  311. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  312. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  313. package/templates/.agents/skills/review-plan/SKILL.en.md +7 -4
  314. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +7 -4
  315. package/templates/.agents/skills/review-pr/SKILL.en.md +186 -0
  316. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +186 -0
  317. package/templates/.agents/skills/review-pr/config/verify.en.json +37 -0
  318. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +37 -0
  319. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +65 -0
  320. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +65 -0
  321. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +18 -0
  322. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +18 -0
  323. package/templates/.agents/skills/review-pr/reference/report-template.en.md +85 -0
  324. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +85 -0
  325. package/templates/.agents/skills/run-task/SKILL.en.md +8 -8
  326. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +8 -8
  327. package/templates/.agents/skills/run-task/config/verify.en.json +2 -1
  328. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +2 -1
  329. package/templates/.agents/skills/run-task/reference/host-validation.en.md +20 -0
  330. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +20 -0
  331. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +98 -107
  332. package/templates/.agents/skills/watch-pr/SKILL.en.md +13 -13
  333. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +13 -13
  334. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +27 -6
  335. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +26 -5
  336. package/templates/.agents/templates/task.en.md +2 -2
  337. package/templates/.agents/templates/task.zh-CN.md +2 -2
  338. package/templates/.agents/workflows/bug-fix.en.yaml +3 -3
  339. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +3 -3
  340. package/templates/.agents/workflows/code-review.en.yaml +1 -1
  341. package/templates/.agents/workflows/code-review.zh-CN.yaml +1 -1
  342. package/templates/.agents/workflows/feature-development.en.yaml +4 -4
  343. package/templates/.agents/workflows/feature-development.zh-CN.yaml +4 -4
  344. package/templates/.agents/workflows/refactoring.en.yaml +3 -3
  345. package/templates/.agents/workflows/refactoring.zh-CN.yaml +3 -3
  346. package/templates/.claude/commands/post-release.en.md +1 -0
  347. package/templates/.claude/commands/post-release.zh-CN.md +1 -0
  348. package/templates/.claude/commands/review-pr.en.md +8 -0
  349. package/templates/.claude/commands/review-pr.zh-CN.md +8 -0
  350. package/templates/.claude/commands/watch-pr.en.md +1 -1
  351. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  352. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  353. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  354. package/templates/.opencode/commands/post-release.en.md +2 -0
  355. package/templates/.opencode/commands/post-release.zh-CN.md +2 -0
  356. package/templates/.opencode/commands/review-pr.en.md +11 -0
  357. package/templates/.opencode/commands/review-pr.zh-CN.md +11 -0
  358. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  359. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  360. package/dist/lib/agent-clients/adapters/gemini-cli.js +0 -64
  361. package/lib/agent-clients/adapters/gemini-cli.ts +0 -65
  362. package/templates/.gemini/commands/_project_/analyze-task.en.toml +0 -10
  363. package/templates/.gemini/commands/_project_/analyze-task.zh-CN.toml +0 -10
  364. package/templates/.gemini/commands/_project_/archive-tasks.en.toml +0 -10
  365. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +0 -10
  366. package/templates/.gemini/commands/_project_/block-task.en.toml +0 -10
  367. package/templates/.gemini/commands/_project_/block-task.zh-CN.toml +0 -10
  368. package/templates/.gemini/commands/_project_/cancel-task.en.toml +0 -10
  369. package/templates/.gemini/commands/_project_/cancel-task.zh-CN.toml +0 -10
  370. package/templates/.gemini/commands/_project_/check-task.en.toml +0 -10
  371. package/templates/.gemini/commands/_project_/check-task.zh-CN.toml +0 -10
  372. package/templates/.gemini/commands/_project_/close-codescan.en.toml +0 -8
  373. package/templates/.gemini/commands/_project_/close-codescan.zh-CN.toml +0 -8
  374. package/templates/.gemini/commands/_project_/close-dependabot.en.toml +0 -8
  375. package/templates/.gemini/commands/_project_/close-dependabot.zh-CN.toml +0 -8
  376. package/templates/.gemini/commands/_project_/code-task.en.toml +0 -10
  377. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +0 -10
  378. package/templates/.gemini/commands/_project_/commit.en.toml +0 -10
  379. package/templates/.gemini/commands/_project_/commit.zh-CN.toml +0 -10
  380. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +0 -10
  381. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +0 -10
  382. package/templates/.gemini/commands/_project_/complete-task.en.toml +0 -10
  383. package/templates/.gemini/commands/_project_/complete-task.zh-CN.toml +0 -10
  384. package/templates/.gemini/commands/_project_/create-pr.en.toml +0 -10
  385. package/templates/.gemini/commands/_project_/create-pr.zh-CN.toml +0 -10
  386. package/templates/.gemini/commands/_project_/create-release-note.en.toml +0 -8
  387. package/templates/.gemini/commands/_project_/create-release-note.zh-CN.toml +0 -8
  388. package/templates/.gemini/commands/_project_/create-task.en.toml +0 -8
  389. package/templates/.gemini/commands/_project_/create-task.zh-CN.toml +0 -8
  390. package/templates/.gemini/commands/_project_/import-codescan.en.toml +0 -8
  391. package/templates/.gemini/commands/_project_/import-codescan.zh-CN.toml +0 -8
  392. package/templates/.gemini/commands/_project_/import-dependabot.en.toml +0 -8
  393. package/templates/.gemini/commands/_project_/import-dependabot.zh-CN.toml +0 -8
  394. package/templates/.gemini/commands/_project_/import-issue.en.toml +0 -8
  395. package/templates/.gemini/commands/_project_/import-issue.zh-CN.toml +0 -8
  396. package/templates/.gemini/commands/_project_/init-labels.en.toml +0 -8
  397. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +0 -8
  398. package/templates/.gemini/commands/_project_/init-milestones.en.toml +0 -10
  399. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +0 -10
  400. package/templates/.gemini/commands/_project_/plan-task.en.toml +0 -10
  401. package/templates/.gemini/commands/_project_/plan-task.zh-CN.toml +0 -10
  402. package/templates/.gemini/commands/_project_/post-release.en.toml +0 -6
  403. package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +0 -6
  404. package/templates/.gemini/commands/_project_/refine-title.en.toml +0 -8
  405. package/templates/.gemini/commands/_project_/refine-title.zh-CN.toml +0 -8
  406. package/templates/.gemini/commands/_project_/release.en.toml +0 -8
  407. package/templates/.gemini/commands/_project_/release.zh-CN.toml +0 -8
  408. package/templates/.gemini/commands/_project_/restore-task.en.toml +0 -8
  409. package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +0 -8
  410. package/templates/.gemini/commands/_project_/review-analysis.en.toml +0 -10
  411. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +0 -10
  412. package/templates/.gemini/commands/_project_/review-code.en.toml +0 -10
  413. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +0 -10
  414. package/templates/.gemini/commands/_project_/review-plan.en.toml +0 -10
  415. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +0 -10
  416. package/templates/.gemini/commands/_project_/run-task.en.toml +0 -8
  417. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +0 -8
  418. package/templates/.gemini/commands/_project_/test-integration.en.toml +0 -6
  419. package/templates/.gemini/commands/_project_/test-integration.zh-CN.toml +0 -6
  420. package/templates/.gemini/commands/_project_/test.en.toml +0 -6
  421. package/templates/.gemini/commands/_project_/test.zh-CN.toml +0 -6
  422. package/templates/.gemini/commands/_project_/update-agent-infra.en.toml +0 -6
  423. package/templates/.gemini/commands/_project_/update-agent-infra.zh-CN.toml +0 -6
  424. package/templates/.gemini/commands/_project_/upgrade-dependency.en.toml +0 -8
  425. package/templates/.gemini/commands/_project_/upgrade-dependency.zh-CN.toml +0 -8
  426. package/templates/.gemini/commands/_project_/watch-pr.en.toml +0 -10
  427. package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +0 -10
  428. 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,9 @@ 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';
22
+ import { inspectCreatePrCommitGate } from '../task/commit-finalization.ts';
23
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
20
24
 
21
25
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
22
26
 
@@ -37,6 +41,18 @@ type CreateOptions = SharedOptions & {
37
41
  };
38
42
  type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
39
43
  type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean };
44
+ type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
45
+ type ExternalPullRequestSelection =
46
+ | { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
47
+ | { status: 'selected'; source: 'unique' | 'explicit'; selected: PullRequestSnapshot; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] }
48
+ | { status: 'failed'; code: string; message: string; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] };
49
+ type ExternalPullRequestResult = PullRequestResult & {
50
+ mode: 'normal' | 'external' | null;
51
+ authorization: 'unique' | 'explicit' | null;
52
+ candidates: PullRequestSnapshot[];
53
+ eligible: PullRequestSnapshot[];
54
+ selected: PullRequestSnapshot | null;
55
+ };
40
56
 
41
57
  type RemotePullRequest = {
42
58
  number?: number;
@@ -54,6 +70,36 @@ type RemotePullRequest = {
54
70
  labels?: Array<string | { name?: string }>;
55
71
  assignees?: Array<{ login?: string }>;
56
72
  milestone?: { title?: string } | null;
73
+ mergeable?: boolean | null;
74
+ mergeable_state?: string | null;
75
+ };
76
+
77
+ type ClosingPullRequestNode = {
78
+ number?: number;
79
+ id?: string;
80
+ url?: string;
81
+ state?: string;
82
+ title?: string;
83
+ body?: string | null;
84
+ isDraft?: boolean;
85
+ headRefName?: string;
86
+ headRefOid?: string;
87
+ headRepository?: { nameWithOwner?: string } | null;
88
+ baseRefName?: string;
89
+ baseRefOid?: string;
90
+ baseRepository?: { nameWithOwner?: string } | null;
91
+ mergedAt?: string | null;
92
+ mergeCommit?: { oid?: string } | null;
93
+ labels?: { nodes?: Array<{ name?: string }> };
94
+ assignees?: { nodes?: Array<{ login?: string }> };
95
+ milestone?: { title?: string } | null;
96
+ };
97
+
98
+ type ClosingPullRequestPage = {
99
+ data?: { repository?: { issue?: { closedByPullRequestsReferences?: {
100
+ nodes?: ClosingPullRequestNode[];
101
+ pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
102
+ } } } };
57
103
  };
58
104
 
59
105
  function result(
@@ -71,12 +117,33 @@ function result(
71
117
  };
72
118
  }
73
119
 
120
+ function externalResult(
121
+ base: PullRequestResult,
122
+ overrides: Partial<ExternalPullRequestResult> = {}
123
+ ): ExternalPullRequestResult {
124
+ return {
125
+ ...base,
126
+ mode: null,
127
+ authorization: null,
128
+ candidates: [],
129
+ eligible: [],
130
+ selected: null,
131
+ ...overrides
132
+ };
133
+ }
134
+
74
135
  function normalizePullRequest(remote: RemotePullRequest, repository: string): PullRequestSnapshot | null {
75
136
  const number = Number(remote.number);
76
137
  const headRepository = remote.head?.repo?.full_name;
77
138
  const baseRepository = remote.base?.repo?.full_name;
78
139
  if (!Number.isInteger(number) || number <= 0 || !remote.node_id || !remote.html_url ||
79
140
  !remote.head?.ref || !remote.head.sha || !headRepository || !remote.base?.ref || !baseRepository) return null;
141
+ const mergeabilityDetail = remote.mergeable_state?.trim().toLowerCase() || null;
142
+ const mergeability = remote.mergeable === false
143
+ ? { state: 'conflicting' as const, detail: mergeabilityDetail }
144
+ : remote.mergeable === true && mergeabilityDetail !== 'dirty'
145
+ ? { state: 'mergeable' as const, detail: mergeabilityDetail }
146
+ : { state: 'unknown' as const, detail: mergeabilityDetail };
80
147
  return {
81
148
  repository,
82
149
  number,
@@ -92,8 +159,149 @@ function normalizePullRequest(remote: RemotePullRequest, repository: string): Pu
92
159
  mergeCommitSha: remote.merge_commit_sha || null,
93
160
  labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
94
161
  assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
95
- milestone: remote.milestone?.title || null
162
+ milestone: remote.milestone?.title || null,
163
+ mergeability
164
+ };
165
+ }
166
+
167
+ function normalizeClosingPullRequest(node: ClosingPullRequestNode, repository: string): PullRequestSnapshot | null {
168
+ return normalizePullRequest({
169
+ number: node.number,
170
+ node_id: node.id,
171
+ html_url: node.url,
172
+ state: node.state === 'OPEN' ? 'open' : 'closed',
173
+ title: node.title,
174
+ body: node.body,
175
+ draft: node.isDraft,
176
+ head: { ref: node.headRefName, sha: node.headRefOid, repo: node.headRepository ? { full_name: node.headRepository.nameWithOwner } : null },
177
+ base: { ref: node.baseRefName, sha: node.baseRefOid, repo: node.baseRepository ? { full_name: node.baseRepository.nameWithOwner } : null },
178
+ merged_at: node.mergedAt,
179
+ merge_commit_sha: node.mergeCommit?.oid,
180
+ labels: node.labels?.nodes,
181
+ assignees: node.assignees?.nodes,
182
+ milestone: node.milestone
183
+ }, repository);
184
+ }
185
+
186
+ function repositoryKey(repository: string): string {
187
+ return repository.trim().toLowerCase();
188
+ }
189
+
190
+ function identitySignature(pullRequest: PullRequestSnapshot): string {
191
+ return [
192
+ pullRequest.repository, pullRequest.number, pullRequest.nodeId, pullRequest.url,
193
+ pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
194
+ pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
195
+ pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
196
+ ].join('|');
197
+ }
198
+
199
+ function hasCompleteExternalIdentity(pullRequest: PullRequestSnapshot): boolean {
200
+ const required = [
201
+ pullRequest.repository, pullRequest.nodeId, pullRequest.url,
202
+ pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
203
+ pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
204
+ ];
205
+ return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
206
+ required.every((value) => Boolean(value?.trim())) &&
207
+ Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
208
+ }
209
+
210
+ function selectExternalPullRequest(
211
+ candidates: PullRequestSnapshot[],
212
+ repository: string,
213
+ existingPrNumber: number | null,
214
+ explicitPrNumber: number | null
215
+ ): ExternalPullRequestSelection {
216
+ const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
217
+ if (invalid) return {
218
+ status: 'failed', code: 'PR_IDENTITY_INVALID',
219
+ message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
220
+ };
221
+ const byNumber = new Map<number, string>();
222
+ for (const candidate of candidates) {
223
+ const signature = identitySignature(candidate);
224
+ const prior = byNumber.get(candidate.number);
225
+ if (prior && prior !== signature) return {
226
+ status: 'failed', code: 'PR_IDENTITY_INVALID',
227
+ message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
228
+ };
229
+ byNumber.set(candidate.number, signature);
230
+ }
231
+ const unique = candidates.filter((candidate, index) =>
232
+ candidates.findIndex((other) => other.number === candidate.number) === index
233
+ );
234
+ const wantedRepository = repositoryKey(repository);
235
+ const eligible = unique.filter((candidate) =>
236
+ repositoryKey(candidate.repository) === wantedRepository &&
237
+ repositoryKey(candidate.base.repository) === wantedRepository &&
238
+ Boolean(candidate.mergedAt && candidate.mergeCommitSha)
239
+ );
240
+ if (explicitPrNumber !== null) {
241
+ const selected = unique.find((candidate) => candidate.number === explicitPrNumber);
242
+ if (!selected) return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR #${explicitPrNumber} is not a closing PR`, candidates, eligible };
243
+ if (!eligible.some((candidate) => candidate.number === explicitPrNumber)) {
244
+ return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR #${explicitPrNumber} is not an eligible merged PR`, candidates, eligible };
245
+ }
246
+ if (existingPrNumber !== null && existingPrNumber !== explicitPrNumber) {
247
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
248
+ }
249
+ return { status: 'selected', source: 'explicit', selected, candidates, eligible };
250
+ }
251
+ if (existingPrNumber !== null && eligible.length === 1 && existingPrNumber !== eligible[0]!.number) {
252
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
253
+ }
254
+ if (eligible.length === 0) return { status: 'normal', candidates, eligible: [] };
255
+ if (eligible.length > 1) return {
256
+ status: 'failed', code: 'PR_IDENTITY_AMBIGUOUS',
257
+ message: 'Multiple eligible merged closing pull requests were found', candidates, eligible
96
258
  };
259
+ return { status: 'selected', source: 'unique', selected: eligible[0]!, candidates, eligible };
260
+ }
261
+
262
+ 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}}}}}`;
263
+
264
+ function inspectGitHubIssueClosingChangeRequests(
265
+ client: GitHubClient,
266
+ repository: string,
267
+ issueNumber: number,
268
+ cwd: string
269
+ ) {
270
+ const [owner, name] = repository.split('/');
271
+ if (!owner || !name || !Number.isInteger(issueNumber) || issueNumber <= 0) return {
272
+ ok: false as const,
273
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Repository or Issue identity is invalid', retryable: false }
274
+ };
275
+ const candidates: PullRequestSnapshot[] = [];
276
+ let cursor: string | null = null;
277
+ for (;;) {
278
+ const args = [
279
+ 'api', 'graphql', '-f', `query=${CLOSING_PULL_REQUESTS_QUERY}`,
280
+ '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `issue=${issueNumber}`
281
+ ];
282
+ if (cursor) args.push('-F', `cursor=${cursor}`);
283
+ const response = client.json<ClosingPullRequestPage>(args, { cwd });
284
+ if (!response.ok) return response;
285
+ const connection = response.value?.data?.repository?.issue?.closedByPullRequestsReferences;
286
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo) return {
287
+ ok: false as const,
288
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request response is incomplete', retryable: false }
289
+ };
290
+ for (const node of connection.nodes) {
291
+ const normalized = normalizeClosingPullRequest(node, repository);
292
+ if (!normalized) return {
293
+ ok: false as const,
294
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request identity is incomplete', retryable: false }
295
+ };
296
+ candidates.push(normalized);
297
+ }
298
+ if (!connection.pageInfo.hasNextPage) return { ok: true as const, value: candidates };
299
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) return {
300
+ ok: false as const,
301
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request pagination cursor is invalid', retryable: false }
302
+ };
303
+ cursor = connection.pageInfo.endCursor;
304
+ }
97
305
  }
98
306
 
99
307
  function expectedHead(repository: string, head: string): { repository: string; ref: string } {
@@ -246,6 +454,11 @@ registerPlatformCapabilities('github', {
246
454
  inspectChangeRequest({ client, repository, number, cwd }) {
247
455
  return inspectGitHubPullRequest((client as GitHubClient | undefined) || createGitHubClient(), repository, number, cwd);
248
456
  },
457
+ inspectIssueClosingChangeRequests({ client, repository, issueNumber, cwd }) {
458
+ return inspectGitHubIssueClosingChangeRequests(
459
+ (client as GitHubClient | undefined) || createGitHubClient(), repository, issueNumber, cwd
460
+ );
461
+ },
249
462
  resolveChangeRequestGitEvidence(context) {
250
463
  return resolveGitHubChangeRequestGitEvidence(context);
251
464
  }
@@ -297,6 +510,44 @@ function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions
297
510
  });
298
511
  }
299
512
 
513
+ function inspectPlatformPullRequestByNumber(prNumber: number, options: InspectionOptions = {}): PullRequestResult {
514
+ const client = (options.client as GitHubClient | undefined) || createGitHubClient();
515
+ const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
516
+ const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
517
+ if (!usable) {
518
+ return result(context.status, null, null, null, {
519
+ platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
520
+ });
521
+ }
522
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
523
+ return result('failed', null, null, null, {
524
+ platform: context.platform, capabilities: context.capabilities,
525
+ error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
526
+ });
527
+ }
528
+ const fetched = inspectPlatformChangeRequest(context.platform.type, {
529
+ cwd: options.cwd || process.cwd(),
530
+ repository: context.platform.repository!,
531
+ number: prNumber,
532
+ client
533
+ });
534
+ if (!fetched.ok || !fetched.value) {
535
+ const error = fetched.error || {
536
+ code: 'PR_INSPECTION_INVALID',
537
+ message: 'Platform adapter returned no change-request snapshot',
538
+ retryable: false
539
+ };
540
+ return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
541
+ platform: context.platform, capabilities: context.capabilities,
542
+ resource: { kind: 'pull-request', number: prNumber }, error
543
+ });
544
+ }
545
+ return result('no-op', null, null, prNumber, {
546
+ platform: context.platform, capabilities: context.capabilities,
547
+ resource: { kind: 'pull-request', number: prNumber }, pullRequest: fetched.value, error: null
548
+ });
549
+ }
550
+
300
551
  function appendActivity(content: string, line: string): string {
301
552
  const body = extractSection(content, ['活动日志', 'Activity Log']);
302
553
  return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
@@ -358,13 +609,163 @@ function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullReq
358
609
  });
359
610
  }
360
611
 
361
- function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
612
+ function externalEvidenceNote(
613
+ issueNumber: number,
614
+ source: 'unique' | 'explicit',
615
+ pullRequest: PullRequestSnapshot
616
+ ): string {
617
+ return [
618
+ `authorization=${source}`,
619
+ externalIdentityEvidenceNote(issueNumber, pullRequest)
620
+ ].join('; ');
621
+ }
622
+
623
+ function externalIdentityEvidenceNote(
624
+ issueNumber: number,
625
+ pullRequest: PullRequestSnapshot
626
+ ): string {
627
+ return [
628
+ `issue=#${issueNumber}`,
629
+ `pr=#${pullRequest.number}`,
630
+ `url=${pullRequest.url}`,
631
+ `base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
632
+ `head=${pullRequest.head.repository}:${pullRequest.head.ref}@${pullRequest.head.sha}`,
633
+ `mergedAt=${pullRequest.mergedAt}`,
634
+ `mergeCommitSha=${pullRequest.mergeCommitSha}`
635
+ ].join('; ');
636
+ }
637
+
638
+ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): ExternalPullRequestResult {
639
+ const base = resolvedContext(taskRef, options);
640
+ if (!base.ok) return externalResult(base.output);
641
+ const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
642
+ if (inventory.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
643
+ platform: base.context.platform,
644
+ capabilities: base.context.capabilities,
645
+ error: { code: inventory.error?.code || 'ARTIFACT_TOPOLOGY_CONFLICT', message: inventory.error?.message || 'Completion artifact inventory failed', retryable: false }
646
+ }));
647
+ if (inventory.artifacts.length > 0) {
648
+ if (options.pr !== undefined) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
649
+ platform: base.context.platform,
650
+ capabilities: base.context.capabilities,
651
+ error: { code: 'EXTERNAL_DELIVERY_NOT_APPLICABLE', message: 'External PR selection is only valid when the completion inventory is empty', retryable: false }
652
+ }), { mode: 'normal' });
653
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
654
+ platform: base.context.platform,
655
+ capabilities: base.context.capabilities,
656
+ operations: [{ name: 'external-pr:inventory', status: 'no-op', reasonCode: 'CANONICAL_ARTIFACTS_PRESENT' }],
657
+ error: null
658
+ }), { mode: 'normal' });
659
+ }
660
+ if (!base.issueNumber) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
661
+ platform: base.context.platform,
662
+ capabilities: base.context.capabilities,
663
+ error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
664
+ }));
665
+ const inspected = inspectPlatformIssueClosingChangeRequests(base.context.platform.type, {
666
+ cwd: base.resolved.repoRoot,
667
+ repository: base.context.platform.repository!,
668
+ issueNumber: base.issueNumber,
669
+ client: base.client
670
+ });
671
+ if (!inspected.ok || !inspected.value) {
672
+ const error = inspected.error || { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
673
+ return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
674
+ platform: base.context.platform, capabilities: base.context.capabilities, error
675
+ }));
676
+ }
677
+ const selected = selectExternalPullRequest(
678
+ inspected.value,
679
+ base.context.platform.repository!,
680
+ base.prNumber,
681
+ options.pr ?? null
682
+ );
683
+ if (selected.status === 'normal') return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
684
+ platform: base.context.platform,
685
+ capabilities: base.context.capabilities,
686
+ operations: [{ name: 'external-pr:select', status: 'no-op', reasonCode: 'NO_ELIGIBLE_CANDIDATE' }],
687
+ error: null
688
+ }), { mode: 'normal', candidates: selected.candidates, eligible: selected.eligible });
689
+ if (selected.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
690
+ platform: base.context.platform,
691
+ capabilities: base.context.capabilities,
692
+ error: { code: selected.code, message: selected.message, retryable: false }
693
+ }), { candidates: selected.candidates, eligible: selected.eligible });
694
+
695
+ const note = externalEvidenceNote(base.issueNumber, selected.source, selected.selected);
696
+ const identityNote = externalIdentityEvidenceNote(base.issueNumber, selected.selected);
697
+ const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
698
+ const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
699
+ const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
700
+ if (auditLines.length > 0 && !alreadyAudited) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
701
+ platform: base.context.platform,
702
+ capabilities: base.context.capabilities,
703
+ pullRequest: selected.selected,
704
+ error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
705
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
706
+ if (alreadyAudited && base.prNumber !== selected.selected.number) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
707
+ platform: base.context.platform,
708
+ capabilities: base.context.capabilities,
709
+ pullRequest: selected.selected,
710
+ error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
711
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
712
+ if (base.prNumber === selected.selected.number && alreadyAudited) {
713
+ return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
714
+ platform: base.context.platform,
715
+ capabilities: base.context.capabilities,
716
+ resource: { kind: 'pull-request', number: selected.selected.number },
717
+ pullRequest: selected.selected,
718
+ operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
719
+ error: null
720
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
721
+ }
722
+ const metadata = captureTaskWriteMetadata();
723
+ const write = writeTask({
724
+ taskRef: base.resolved.taskId,
725
+ expectedState: 'active',
726
+ dryRun: options.dryRun,
727
+ mutations: [
728
+ { kind: 'frontmatter', set: { pr_number: selected.selected.number, pr_status: 'created', assigned_to: options.agent } },
729
+ { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
730
+ base.content, `- ${metadata.timestamp} — ${activityEntry}`
731
+ ) }
732
+ ]
733
+ }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
734
+ if (write.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
735
+ platform: base.context.platform,
736
+ capabilities: base.context.capabilities,
737
+ pullRequest: selected.selected,
738
+ error: { code: write.error.code, message: write.error.message, retryable: false }
739
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
740
+ const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
741
+ return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selected.selected.number, {
742
+ changed: !options.dryRun && write.changed,
743
+ platform: base.context.platform,
744
+ capabilities: base.context.capabilities,
745
+ resource: { kind: 'pull-request', number: selected.selected.number },
746
+ pullRequest: selected.selected,
747
+ operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
748
+ error: null
749
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
750
+ }
751
+
752
+ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptions): PullRequestResult {
362
753
  const base = resolvedContext(taskRef, options);
363
754
  if (!base.ok) return base.output;
364
755
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
365
756
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
366
757
  });
367
758
  if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
759
+ const gate = inspectCreatePrCommitGate(base.resolved.taskDir, base.resolved.repoRoot, base.resolved.taskId);
760
+ if (!gate.allowed) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
761
+ platform: base.context.platform,
762
+ capabilities: base.context.capabilities,
763
+ error: {
764
+ code: gate.code!,
765
+ message: `${gate.message}; action=${gate.action}`,
766
+ retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
767
+ }
768
+ });
368
769
  const located = locatePullRequest(base, options.head, options.base);
369
770
  if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
370
771
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
@@ -422,6 +823,42 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
422
823
  });
423
824
  }
424
825
 
826
+ function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
827
+ const base = resolvedContext(taskRef, options);
828
+ if (!base.ok) return base.output;
829
+ if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
830
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
831
+ error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
832
+ });
833
+ }
834
+ if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
835
+ try {
836
+ return withTaskExecutionLock(
837
+ base.resolved.repoRoot,
838
+ base.resolved.taskId,
839
+ 'platform-pr.create',
840
+ () => createPlatformPullRequestUnlocked(taskRef, options)
841
+ );
842
+ } catch (error) {
843
+ if (error instanceof TaskExecutionLockError) {
844
+ return result('blocked', base.resolved.taskId, base.issueNumber, null, {
845
+ platform: base.context.platform,
846
+ capabilities: base.context.capabilities,
847
+ error: { code: error.code, message: error.message, retryable: true }
848
+ });
849
+ }
850
+ return result('failed', base.resolved.taskId, base.issueNumber, null, {
851
+ platform: base.context.platform,
852
+ capabilities: base.context.capabilities,
853
+ error: {
854
+ code: 'PR_CREATE_FAILED',
855
+ message: error instanceof Error ? error.message : String(error),
856
+ retryable: false
857
+ }
858
+ });
859
+ }
860
+ }
861
+
425
862
  function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullRequestResult {
426
863
  const base = resolvedContext(taskRef, options);
427
864
  if (!base.ok) return base.output;
@@ -486,10 +923,14 @@ export {
486
923
  bindPlatformPullRequest,
487
924
  createPlatformPullRequest,
488
925
  inspectGitHubPullRequest,
926
+ inspectGitHubIssueClosingChangeRequests,
489
927
  inspectPlatformPullRequest,
928
+ inspectPlatformPullRequestByNumber,
490
929
  normalizePullRequest,
491
930
  resolveGitHubChangeRequestGitEvidence,
931
+ resolveExternalPullRequest,
932
+ selectExternalPullRequest,
492
933
  selectPullRequest,
493
934
  syncPlatformPullRequest
494
935
  };
495
- export type { BindOptions, CreateOptions, PullRequestResult, PullRequestSnapshot, SyncOptions };
936
+ 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
+ };