@fitlab-ai/agent-infra 0.9.11 → 0.9.13

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 (356) hide show
  1. package/README.md +13 -13
  2. package/README.zh-CN.md +13 -13
  3. package/dist/bin/internal-cli.js +29 -19
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
  5. package/dist/lib/agent-clients/next-steps.js +6 -1
  6. package/dist/lib/decide.js +3 -3
  7. package/dist/lib/defaults.json +4 -0
  8. package/dist/lib/git/workflow.js +43 -1
  9. package/dist/lib/init.js +1 -0
  10. package/dist/lib/internal/git-workflow.js +8 -1
  11. package/dist/lib/internal/platform-checks.js +3 -3
  12. package/dist/lib/internal/platform-comment.js +9 -9
  13. package/dist/lib/internal/platform-context.js +2 -2
  14. package/dist/lib/internal/platform-issue.js +8 -9
  15. package/dist/lib/internal/platform-pr-review.js +10 -10
  16. package/dist/lib/internal/platform-pr.js +68 -23
  17. package/dist/lib/internal/platform-release-notes.js +3 -3
  18. package/dist/lib/internal/pr-review-grade.js +11 -9
  19. package/dist/lib/internal/release-workflow.js +4 -4
  20. package/dist/lib/internal/sandbox-control.js +30 -4
  21. package/dist/lib/internal/task-activity.js +3 -3
  22. package/dist/lib/internal/task-artifact.js +76 -24
  23. package/dist/lib/internal/task-create.js +2 -2
  24. package/dist/lib/internal/task-delivery.js +59 -0
  25. package/dist/lib/internal/task-event.js +6 -3
  26. package/dist/lib/internal/task-finalization.js +2 -2
  27. package/dist/lib/internal/task-invalidation.js +60 -0
  28. package/dist/lib/internal/task-ledger.js +9 -6
  29. package/dist/lib/internal/task-lifecycle.js +2 -2
  30. package/dist/lib/internal/task-override.js +4 -4
  31. package/dist/lib/internal/task-review.js +3 -3
  32. package/dist/lib/internal/task-verify.js +6 -6
  33. package/dist/lib/internal/task-warning.js +3 -3
  34. package/dist/lib/platform/adapters.js +1 -3
  35. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  36. package/dist/lib/platform/completion-backfill.js +6 -5
  37. package/dist/lib/platform/context.js +70 -130
  38. package/dist/lib/platform/github-client.js +8 -2
  39. package/dist/lib/platform/github-provider.js +903 -0
  40. package/dist/lib/platform/in-label-sync.js +143 -0
  41. package/dist/lib/platform/issue-comments.js +94 -41
  42. package/dist/lib/platform/issues.js +335 -189
  43. package/dist/lib/platform/metadata-labels.js +6 -2
  44. package/dist/lib/platform/none-provider.js +28 -0
  45. package/dist/lib/platform/pr-change-report.js +551 -0
  46. package/dist/lib/platform/pr-checks.js +124 -106
  47. package/dist/lib/platform/pr-review.js +88 -72
  48. package/dist/lib/platform/pr-summary.js +523 -49
  49. package/dist/lib/platform/provider-bridge.js +45 -0
  50. package/dist/lib/platform/provider-contract.d.ts +487 -0
  51. package/dist/lib/platform/provider-contract.js +95 -0
  52. package/dist/lib/platform/provider-loader.js +251 -0
  53. package/dist/lib/platform/provider-validation.js +504 -0
  54. package/dist/lib/platform/pull-request-metadata.js +8 -6
  55. package/dist/lib/platform/pull-requests.js +1050 -502
  56. package/dist/lib/platform/release-notes.js +78 -55
  57. package/dist/lib/platform/releases.js +79 -50
  58. package/dist/lib/platform/resource-identity.d.ts +26 -0
  59. package/dist/lib/platform/resource-identity.js +92 -0
  60. package/dist/lib/platform/task-identities.js +31 -0
  61. package/dist/lib/platform/verification-required.js +22 -12
  62. package/dist/lib/platform/verification-sync.js +118 -239
  63. package/dist/lib/pr-review/evidence-grading.js +30 -9
  64. package/dist/lib/run/index.js +63 -23
  65. package/dist/lib/sandbox/config.js +5 -0
  66. package/dist/lib/sandbox/control/client.js +96 -15
  67. package/dist/lib/sandbox/control/executor.js +3 -3
  68. package/dist/lib/sandbox/control/lifecycle.js +29 -7
  69. package/dist/lib/sandbox/control/protocol.js +77 -0
  70. package/dist/lib/sandbox/control/server.js +398 -25
  71. package/dist/lib/sandbox/control/state.js +204 -1
  72. package/dist/lib/server/process-state.js +2 -2
  73. package/dist/lib/server/protocol.js +38 -1
  74. package/dist/lib/task/activity-log.js +8 -1
  75. package/dist/lib/task/artifact-lifecycle.js +35 -7
  76. package/dist/lib/task/capabilities.js +239 -0
  77. package/dist/lib/task/commands/status.js +32 -0
  78. package/dist/lib/task/commit-intent.js +100 -0
  79. package/dist/lib/task/commit-operation.js +207 -102
  80. package/dist/lib/task/control-authority.js +8 -5
  81. package/dist/lib/task/create-service.js +8 -8
  82. package/dist/lib/task/create.js +33 -7
  83. package/dist/lib/task/current-contract.js +5 -1
  84. package/dist/lib/task/decision-details.js +3 -48
  85. package/dist/lib/task/delivery-target.js +97 -0
  86. package/dist/lib/task/delivery.js +153 -0
  87. package/dist/lib/task/events.js +297 -17
  88. package/dist/lib/task/finalization.js +37 -13
  89. package/dist/lib/task/human-override.js +61 -36
  90. package/dist/lib/task/invalidation-command.js +86 -0
  91. package/dist/lib/task/invalidation.js +260 -0
  92. package/dist/lib/task/ledger-intents.js +37 -0
  93. package/dist/lib/task/lifecycle.js +106 -9
  94. package/dist/lib/task/local-artifact-finalization.js +374 -0
  95. package/dist/lib/task/orchestration.js +100 -151
  96. package/dist/lib/task/pr-delivery-fact.js +183 -0
  97. package/dist/lib/task/review-finalization.js +4 -8
  98. package/dist/lib/task/review-fingerprint.js +32 -2
  99. package/dist/lib/task/rework-intent.js +138 -0
  100. package/dist/lib/task/skill-scope.js +23 -0
  101. package/dist/lib/task/task-execution-lock.js +27 -17
  102. package/dist/lib/task/verification-engine.js +89 -38
  103. package/dist/lib/task/verification-review.js +1 -1
  104. package/dist/lib/task/verification.js +3 -3
  105. package/dist/lib/task/workflow-warning-intents.js +1 -1
  106. package/dist/lib/task/write.js +11 -0
  107. package/dist/lib/update.js +9 -0
  108. package/dist/lib/version.js +11 -1
  109. package/dist/lifecycle-build-manifest.json +3 -3
  110. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
  111. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
  112. package/lib/agent-clients/next-steps.ts +6 -1
  113. package/lib/decide.ts +3 -3
  114. package/lib/defaults.json +4 -0
  115. package/lib/git/workflow.ts +38 -1
  116. package/lib/init.ts +4 -1
  117. package/lib/internal/git-workflow.ts +7 -1
  118. package/lib/internal/platform-checks.ts +3 -3
  119. package/lib/internal/platform-comment.ts +8 -8
  120. package/lib/internal/platform-context.ts +2 -2
  121. package/lib/internal/platform-issue.ts +7 -8
  122. package/lib/internal/platform-pr-review.ts +9 -9
  123. package/lib/internal/platform-pr.ts +53 -21
  124. package/lib/internal/platform-release-notes.ts +3 -3
  125. package/lib/internal/pr-review-grade.ts +10 -8
  126. package/lib/internal/release-workflow.ts +4 -4
  127. package/lib/internal/sandbox-control.ts +27 -3
  128. package/lib/internal/task-activity.ts +3 -3
  129. package/lib/internal/task-artifact.ts +65 -22
  130. package/lib/internal/task-create.ts +2 -2
  131. package/lib/internal/task-delivery.ts +53 -0
  132. package/lib/internal/task-event.ts +6 -3
  133. package/lib/internal/task-finalization.ts +3 -3
  134. package/lib/internal/task-invalidation.ts +39 -0
  135. package/lib/internal/task-ledger.ts +11 -8
  136. package/lib/internal/task-lifecycle.ts +2 -2
  137. package/lib/internal/task-override.ts +6 -6
  138. package/lib/internal/task-review.ts +3 -3
  139. package/lib/internal/task-verify.ts +6 -6
  140. package/lib/internal/task-warning.ts +3 -3
  141. package/lib/platform/adapters.ts +3 -2
  142. package/lib/platform/change-request-git-evidence.ts +36 -9
  143. package/lib/platform/completion-backfill.ts +9 -8
  144. package/lib/platform/context.ts +97 -134
  145. package/lib/platform/github-client.ts +10 -3
  146. package/lib/platform/github-provider.ts +910 -0
  147. package/lib/platform/in-label-sync.ts +208 -0
  148. package/lib/platform/issue-comments.ts +97 -47
  149. package/lib/platform/issues.ts +344 -174
  150. package/lib/platform/metadata-labels.ts +5 -2
  151. package/lib/platform/none-provider.ts +35 -0
  152. package/lib/platform/pr-change-report.ts +645 -0
  153. package/lib/platform/pr-checks.ts +129 -107
  154. package/lib/platform/pr-review.ts +88 -85
  155. package/lib/platform/pr-summary.ts +562 -52
  156. package/lib/platform/provider-bridge.ts +82 -0
  157. package/lib/platform/provider-contract.ts +536 -0
  158. package/lib/platform/provider-loader.ts +307 -0
  159. package/lib/platform/provider-validation.ts +529 -0
  160. package/lib/platform/pull-request-metadata.ts +10 -7
  161. package/lib/platform/pull-requests.ts +1126 -505
  162. package/lib/platform/release-notes.ts +76 -59
  163. package/lib/platform/releases.ts +77 -43
  164. package/lib/platform/resource-identity.ts +121 -0
  165. package/lib/platform/task-identities.ts +40 -0
  166. package/lib/platform/types.ts +4 -2
  167. package/lib/platform/verification-required.ts +21 -13
  168. package/lib/platform/verification-sync.ts +118 -293
  169. package/lib/pr-review/evidence-grading.ts +31 -11
  170. package/lib/run/index.ts +57 -22
  171. package/lib/sandbox/config.ts +7 -0
  172. package/lib/sandbox/control/client.ts +105 -15
  173. package/lib/sandbox/control/executor.ts +4 -4
  174. package/lib/sandbox/control/lifecycle.ts +30 -8
  175. package/lib/sandbox/control/protocol.ts +113 -0
  176. package/lib/sandbox/control/server.ts +431 -23
  177. package/lib/sandbox/control/state.ts +251 -1
  178. package/lib/server/process-state.ts +2 -2
  179. package/lib/server/protocol.ts +36 -1
  180. package/lib/task/activity-log.ts +8 -0
  181. package/lib/task/artifact-lifecycle.ts +34 -8
  182. package/lib/task/capabilities.ts +262 -0
  183. package/lib/task/commands/status.ts +38 -0
  184. package/lib/task/commit-intent.ts +137 -0
  185. package/lib/task/commit-operation.ts +186 -88
  186. package/lib/task/control-authority.ts +11 -8
  187. package/lib/task/create-service.ts +8 -8
  188. package/lib/task/create.ts +34 -9
  189. package/lib/task/current-contract.ts +6 -1
  190. package/lib/task/decision-details.ts +8 -64
  191. package/lib/task/delivery-target.ts +130 -0
  192. package/lib/task/delivery.ts +160 -0
  193. package/lib/task/events.ts +296 -16
  194. package/lib/task/finalization.ts +50 -14
  195. package/lib/task/human-override.ts +73 -46
  196. package/lib/task/invalidation-command.ts +86 -0
  197. package/lib/task/invalidation.ts +332 -0
  198. package/lib/task/ledger-intents.ts +35 -1
  199. package/lib/task/lifecycle.ts +93 -4
  200. package/lib/task/local-artifact-finalization.ts +563 -0
  201. package/lib/task/orchestration.ts +110 -146
  202. package/lib/task/pr-delivery-fact.ts +237 -0
  203. package/lib/task/review-finalization.ts +4 -14
  204. package/lib/task/review-fingerprint.ts +33 -2
  205. package/lib/task/rework-intent.ts +165 -0
  206. package/lib/task/skill-scope.ts +24 -0
  207. package/lib/task/task-execution-lock.ts +23 -14
  208. package/lib/task/verification-engine.ts +95 -43
  209. package/lib/task/verification-review.ts +2 -0
  210. package/lib/task/verification.ts +4 -4
  211. package/lib/task/workflow-warning-intents.ts +1 -1
  212. package/lib/task/write.ts +19 -0
  213. package/lib/update.ts +11 -1
  214. package/lib/version.ts +15 -1
  215. package/package.json +7 -1
  216. package/templates/.agents/rules/README.en.md +4 -2
  217. package/templates/.agents/rules/README.zh-CN.md +4 -2
  218. package/templates/.agents/rules/create-issue.en.md +11 -2
  219. package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
  220. package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
  221. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  222. package/templates/.agents/rules/issue-sync.en.md +28 -5
  223. package/templates/.agents/rules/issue-sync.zh-CN.md +64 -5
  224. package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
  225. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
  226. package/templates/.agents/rules/local-artifact-repair.en.md +53 -0
  227. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +53 -0
  228. package/templates/.agents/rules/milestone-inference.en.md +12 -2
  229. package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
  230. package/templates/.agents/rules/pr-sync.en.md +28 -13
  231. package/templates/.agents/rules/pr-sync.zh-CN.md +39 -5
  232. package/templates/.agents/rules/release-commands.en.md +42 -3
  233. package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
  234. package/templates/.agents/rules/security-alerts.en.md +17 -2
  235. package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
  236. package/templates/.agents/rules/sync-content-generation.en.md +8 -0
  237. package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
  238. package/templates/.agents/rules/task-management.en.md +1 -1
  239. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  240. package/templates/.agents/rules/task-short-id.en.md +1 -4
  241. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  242. package/templates/.agents/scripts/security-alerts.github.sh +151 -0
  243. package/templates/.agents/scripts/security-alerts.sh +31 -0
  244. package/templates/.agents/skills/analyze-task/SKILL.en.md +14 -3
  245. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -3
  246. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  247. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  248. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  249. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  250. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  251. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  252. package/templates/.agents/skills/code-task/SKILL.en.md +32 -10
  253. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +35 -13
  254. package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
  255. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
  256. package/templates/.agents/skills/commit/SKILL.en.md +3 -3
  257. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
  258. package/templates/.agents/skills/commit/config/verify.json +1 -1
  259. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
  260. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
  261. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  262. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  263. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +4 -4
  264. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +4 -4
  265. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  266. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  267. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +7 -4
  268. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +7 -4
  269. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  270. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  271. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +5 -4
  272. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +5 -4
  273. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -13
  274. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +30 -13
  275. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
  276. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
  277. package/templates/.agents/skills/create-pr/SKILL.en.md +11 -10
  278. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +11 -11
  279. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  280. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  281. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  282. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  283. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  284. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  285. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  286. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  287. package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
  288. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
  289. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  290. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  291. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  292. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  293. package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
  294. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
  295. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
  296. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
  297. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
  298. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
  299. package/templates/.agents/skills/plan-task/SKILL.en.md +14 -3
  300. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +11 -3
  301. package/templates/.agents/skills/review-analysis/SKILL.en.md +6 -5
  302. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +6 -5
  303. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
  304. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
  305. package/templates/.agents/skills/review-code/SKILL.en.md +10 -9
  306. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +10 -9
  307. package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
  308. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
  309. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
  310. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
  311. package/templates/.agents/skills/review-plan/SKILL.en.md +6 -5
  312. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +6 -5
  313. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
  314. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
  315. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
  316. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
  317. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
  318. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
  319. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -2
  320. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -2
  321. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  322. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  323. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
  324. package/templates/.agents/skills/watch-pr/SKILL.en.md +6 -6
  325. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +6 -6
  326. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  327. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  328. package/templates/.agents/templates/task.en.md +23 -2
  329. package/templates/.agents/templates/task.zh-CN.md +23 -2
  330. package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
  331. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
  332. package/templates/.agents/workflows/feature-development.en.yaml +9 -8
  333. package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
  334. package/templates/.agents/workflows/refactoring.en.yaml +9 -8
  335. package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
  336. package/templates/.github/workflows/pr-label.yml +7 -26
  337. package/templates/.agents/rules/create-issue.github.en.md +0 -14
  338. package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
  339. package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
  340. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
  341. package/templates/.agents/rules/issue-sync.github.en.md +0 -40
  342. package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
  343. package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
  344. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
  345. package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
  346. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
  347. package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
  348. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
  349. package/templates/.agents/rules/pr-sync.github.en.md +0 -43
  350. package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
  351. package/templates/.agents/rules/release-commands.github.en.md +0 -44
  352. package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
  353. package/templates/.agents/rules/security-alerts.github.en.md +0 -43
  354. package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
  355. /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
  356. /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
@@ -1,18 +1,25 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { execFileSync } from 'node:child_process';
3
- import { parseTaskFrontmatter } from '../task/frontmatter.js';
4
+ import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
4
5
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
6
  import { extractSection } from '../task/sections.js';
6
7
  import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
7
- import { inspectPlatformChangeRequest, inspectPlatformIssueClosingChangeRequests, registerPlatformCapabilities } from './adapters.js';
8
- import { parseGitHubRemote, resolvePlatformContext } from './context.js';
8
+ import { parseGitHubRemote, resolvePlatformContext, resolvePlatformProviderContext } from './context.js';
9
9
  import { createGitHubClient } from './github-client.js';
10
10
  import { inspectPlatformIssue } from './issues.js';
11
+ import { inspectGitHubIssue } from './issues.js';
11
12
  import { planPullRequestMetadata } from './pull-request-metadata.js';
13
+ import { extractPullRequestFileNames, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
12
14
  import { platformResult } from './types.js';
13
15
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
14
16
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
17
+ import { resolveDeliveryTarget } from '../task/delivery-target.js';
15
18
  import { mergeOperationWarnings } from '../task/operation-outcome.js';
19
+ import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDeliveryFact } from '../task/pr-delivery-fact.js';
20
+ import { providerError, providerOperationContext, providerStatus, providerResourceToken, providerResourceIdentity, resourceIdentity, unsupportedProviderOperation } from './provider-bridge.js';
21
+ import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber } from './resource-identity.js';
22
+ import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
16
23
  function warningResultForPrimary(primaryResult) {
17
24
  if (primaryResult === 'pr_created')
18
25
  return 'pr_created_with_warnings';
@@ -26,6 +33,7 @@ function result(status, taskId, issueNumber, prNumber, overrides = {}) {
26
33
  task: { id: taskId, issueNumber, prNumber },
27
34
  pullRequest: null,
28
35
  result: null,
36
+ creation: null,
29
37
  warnings: [],
30
38
  ...overrides
31
39
  };
@@ -42,6 +50,12 @@ function externalResult(base, overrides = {}) {
42
50
  ...overrides
43
51
  };
44
52
  }
53
+ function withCreation(output, creation) {
54
+ return { ...output, creation };
55
+ }
56
+ const PRECONDITION_NOT_CREATED = {
57
+ kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
58
+ };
45
59
  function normalizePullRequest(remote, repository) {
46
60
  const number = Number(remote.number);
47
61
  const headRepository = remote.head?.repo?.full_name;
@@ -95,9 +109,15 @@ function normalizeClosingPullRequest(node, repository) {
95
109
  function repositoryKey(repository) {
96
110
  return repository.trim().toLowerCase();
97
111
  }
112
+ function isUsableResourceIdentity(identity) {
113
+ return isResourceIdentity(identity);
114
+ }
115
+ function identityLabel(identity) {
116
+ return identity.kind === 'number' ? `PR #${identity.value}` : `PR ${identity.kind}:${identity.value}`;
117
+ }
98
118
  function identitySignature(pullRequest) {
99
119
  return [
100
- pullRequest.repository, pullRequest.number, pullRequest.nodeId, pullRequest.url,
120
+ pullRequest.repository, JSON.stringify(pullRequestIdentity(pullRequest)), pullRequest.nodeId, pullRequest.url,
101
121
  pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
102
122
  pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
103
123
  pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
@@ -109,47 +129,52 @@ function hasCompleteExternalIdentity(pullRequest) {
109
129
  pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
110
130
  pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
111
131
  ];
112
- return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
113
- required.every((value) => Boolean(value?.trim())) &&
114
- Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
132
+ return isUsableResourceIdentity(pullRequestIdentity(pullRequest)) && required.every((value) => Boolean(value?.trim())) &&
133
+ (pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
115
134
  }
116
- function selectExternalPullRequest(candidates, repository, existingPrNumber, explicitPrNumber) {
135
+ function selectExternalPullRequest(candidates, repository, existingPrIdentity, explicitPrIdentity) {
136
+ const existingIdentity = typeof existingPrIdentity === 'number'
137
+ ? resourceIdentity(existingPrIdentity) : existingPrIdentity;
138
+ const explicitIdentity = typeof explicitPrIdentity === 'number'
139
+ ? resourceIdentity(explicitPrIdentity) : explicitPrIdentity;
117
140
  const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
118
141
  if (invalid)
119
142
  return {
120
143
  status: 'failed', code: 'PR_IDENTITY_INVALID',
121
144
  message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
122
145
  };
123
- const byNumber = new Map();
146
+ const byIdentity = new Map();
124
147
  for (const candidate of candidates) {
125
148
  const signature = identitySignature(candidate);
126
- const prior = byNumber.get(candidate.number);
149
+ const identityKey = JSON.stringify(pullRequestIdentity(candidate));
150
+ const prior = byIdentity.get(identityKey);
127
151
  if (prior && prior !== signature)
128
152
  return {
129
153
  status: 'failed', code: 'PR_IDENTITY_INVALID',
130
154
  message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
131
155
  };
132
- byNumber.set(candidate.number, signature);
156
+ byIdentity.set(identityKey, signature);
133
157
  }
134
- const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => other.number === candidate.number) === index);
158
+ const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => resourceIdentityEquals(pullRequestIdentity(other), pullRequestIdentity(candidate))) === index);
135
159
  const wantedRepository = repositoryKey(repository);
136
160
  const eligible = unique.filter((candidate) => repositoryKey(candidate.repository) === wantedRepository &&
137
161
  repositoryKey(candidate.base.repository) === wantedRepository &&
162
+ candidate.state === 'closed' &&
138
163
  Boolean(candidate.mergedAt && candidate.mergeCommitSha));
139
- if (explicitPrNumber !== null) {
140
- const selected = unique.find((candidate) => candidate.number === explicitPrNumber);
164
+ if (explicitIdentity !== null) {
165
+ const selected = unique.find((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity));
141
166
  if (!selected)
142
- return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR #${explicitPrNumber} is not a closing PR`, candidates, eligible };
143
- if (!eligible.some((candidate) => candidate.number === explicitPrNumber)) {
144
- return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR #${explicitPrNumber} is not an eligible merged PR`, candidates, eligible };
167
+ return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR ${identityLabel(explicitIdentity)} is not a closing PR`, candidates, eligible };
168
+ if (!eligible.some((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity))) {
169
+ return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR ${identityLabel(explicitIdentity)} is not an eligible merged PR`, candidates, eligible };
145
170
  }
146
- if (existingPrNumber !== null && existingPrNumber !== explicitPrNumber) {
147
- return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
171
+ if (existingIdentity !== null && !resourceIdentityEquals(existingIdentity, explicitIdentity)) {
172
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
148
173
  }
149
174
  return { status: 'selected', source: 'explicit', selected, candidates, eligible };
150
175
  }
151
- if (existingPrNumber !== null && eligible.length === 1 && existingPrNumber !== eligible[0].number) {
152
- return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
176
+ if (existingIdentity !== null && eligible.length === 1 && !resourceIdentityEquals(existingIdentity, pullRequestIdentity(eligible[0]))) {
177
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
153
178
  }
154
179
  if (eligible.length === 0)
155
180
  return { status: 'normal', candidates, eligible: [] };
@@ -227,64 +252,6 @@ function selectPullRequest(remotes, repository, head, base, expectedSha) {
227
252
  return { status: 'resolved', pullRequest: matches[0] };
228
253
  return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
229
254
  }
230
- function repositoryHead(cwd) {
231
- return execFileSync('git', ['rev-parse', 'HEAD'], {
232
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
233
- }).trim();
234
- }
235
- function remoteBranchHead(cwd, repository, head) {
236
- const wanted = expectedHead(repository, head);
237
- if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref))
238
- return {
239
- ok: false,
240
- error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
241
- };
242
- const remotes = configuredGitRemotes(cwd);
243
- const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
244
- ?? remotes.find((item) => item.name === 'origin');
245
- if (!remote)
246
- return {
247
- ok: false,
248
- error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
249
- };
250
- try {
251
- const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
252
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
253
- }).trim();
254
- const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
255
- const sha = match?.split(/\s+/)[0] || null;
256
- if (!sha || !/^[a-f0-9]{40}$/i.test(sha))
257
- return {
258
- ok: false,
259
- error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
260
- };
261
- return { ok: true, value: sha };
262
- }
263
- catch (error) {
264
- return {
265
- ok: false,
266
- error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
267
- };
268
- }
269
- }
270
- function verifyCreateHead(base, head) {
271
- let localHead;
272
- try {
273
- localHead = repositoryHead(base.resolved.repoRoot);
274
- }
275
- catch (error) {
276
- return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
277
- }
278
- const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository, head);
279
- if (!remote.ok)
280
- return remote;
281
- if (remote.value !== localHead)
282
- return {
283
- ok: false,
284
- error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
285
- };
286
- return { ok: true, value: localHead };
287
- }
288
255
  function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha) {
289
256
  if (!pullRequest)
290
257
  return {
@@ -303,26 +270,157 @@ function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha)
303
270
  };
304
271
  return { ok: true };
305
272
  }
306
- function resolvedContext(taskRef, options) {
273
+ function resolveTaskDeliveryTarget(repoRoot, frontmatter) {
274
+ const existing = {
275
+ ...(typeof frontmatter.delivery_remote === 'string' && frontmatter.delivery_remote
276
+ ? { remote: frontmatter.delivery_remote } : {}),
277
+ ...(typeof frontmatter.delivery_base_ref === 'string' && frontmatter.delivery_base_ref
278
+ ? { baseRef: frontmatter.delivery_base_ref } : {})
279
+ };
280
+ const target = resolveDeliveryTarget(repoRoot, existing);
281
+ return target.ok
282
+ ? { ok: true, value: target.value }
283
+ : { ok: false, error: { code: target.code, message: target.message, retryable: false } };
284
+ }
285
+ function taskDeliveryTarget(base) {
286
+ return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
287
+ }
288
+ function validateIdentityAgainstTarget(repository, target, pullRequest, options = {}) {
289
+ if (!pullRequest || !hasCompleteExternalIdentity(pullRequest))
290
+ return {
291
+ ok: false,
292
+ error: { code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity is incomplete', retryable: false }
293
+ };
294
+ const wantedHead = options.expectedHead ? expectedHead(repository, options.expectedHead) : null;
295
+ const matches = repositoryKey(pullRequest.repository) === repositoryKey(repository)
296
+ && repositoryKey(pullRequest.base.repository) === repositoryKey(repository)
297
+ && pullRequest.base.ref === target.baseRef
298
+ && (!wantedHead || (repositoryKey(pullRequest.head.repository) === repositoryKey(wantedHead.repository) && pullRequest.head.ref === wantedHead.ref))
299
+ && (!options.expectedHeadSha || pullRequest.head.sha === options.expectedHeadSha);
300
+ if (!matches)
301
+ return {
302
+ ok: false,
303
+ error: {
304
+ code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH',
305
+ message: 'Pull request identity does not match the task branch and delivery target',
306
+ retryable: false
307
+ }
308
+ };
309
+ return { ok: true };
310
+ }
311
+ function validateWriterIdentity(base, pullRequest, options = {}) {
312
+ const target = taskDeliveryTarget(base);
313
+ if (!target.ok)
314
+ return target;
315
+ return validateIdentityAgainstTarget(base.context.platform.repository, target.value, pullRequest, options);
316
+ }
317
+ function sameDeliveryIdentity(left, right) {
318
+ return JSON.stringify(deliveryIdentity(left)) === JSON.stringify(deliveryIdentity(right));
319
+ }
320
+ function validateExternalMergedEvidence(initial, rechecked) {
321
+ if (initial.state !== 'closed'
322
+ || !initial.mergedAt
323
+ || !initial.mergeCommitSha
324
+ || rechecked.state !== 'closed'
325
+ || !rechecked.mergedAt
326
+ || !rechecked.mergeCommitSha
327
+ || initial.mergedAt !== rechecked.mergedAt
328
+ || initial.mergeCommitSha !== rechecked.mergeCommitSha)
329
+ return {
330
+ ok: false,
331
+ error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request merged evidence changed before task binding', retryable: false }
332
+ };
333
+ return { ok: true };
334
+ }
335
+ async function resolvedContext(taskRef, options) {
307
336
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
308
337
  if (!resolved.ok)
309
338
  return { ok: false, output: result('failed', resolved.taskId, null, null, {
310
339
  error: { code: resolved.code, message: resolved.message, retryable: false }
311
340
  }) };
312
341
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
313
- const frontmatter = parseTaskFrontmatter(content);
314
- const issue = Number(frontmatter.issue_number);
315
- const pr = Number(frontmatter.pr_number);
316
- const issueNumber = Number.isInteger(issue) && issue > 0 ? issue : null;
317
- const prNumber = Number.isInteger(pr) && pr > 0 ? pr : null;
318
- const client = options.client || createGitHubClient();
319
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client });
342
+ const frontmatter = parseTypedTaskFrontmatter(content);
343
+ const factRead = readPrDeliveryFact(frontmatter, options.runtimeVersion);
344
+ if (factRead.status === 'invalid')
345
+ return { ok: false, output: result('failed', resolved.taskId, null, null, {
346
+ error: { code: factRead.error.code, message: factRead.error.message, retryable: false }
347
+ }) };
348
+ const fact = factRead.status === 'valid' ? factRead.fact : null;
349
+ let issueIdentity;
350
+ try {
351
+ issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
352
+ }
353
+ catch (error) {
354
+ return { ok: false, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
355
+ }
356
+ const issueNumber = resourceIdentityNumber(issueIdentity);
357
+ const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
358
+ const prNumber = resourceIdentityNumber(prIdentity);
359
+ const client = options.client;
360
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client });
361
+ const context = loaded.ok ? loaded.value.context : loaded.context;
320
362
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
321
- if (!usable)
363
+ if (!usable || !loaded.ok)
322
364
  return { ok: false, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
323
365
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
324
366
  }) };
325
- return { ok: true, resolved, content, frontmatter, issueNumber, prNumber, client, context };
367
+ return { ok: true, resolved, content, frontmatter, fact, issueIdentity, issueNumber, prIdentity, prNumber, client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
368
+ }
369
+ function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallback) {
370
+ const head = remote.head || fallback?.head || { repository, ref: '', sha: remote.headSha || '' };
371
+ const base = remote.base || fallback?.base || { repository, ref: '', sha: remote.baseSha || '' };
372
+ const identity = remote.identity || fallback?.identity || (remote.number ? { kind: 'number', value: remote.number } : { kind: 'id', value: remote.id });
373
+ const snapshot = {
374
+ repository,
375
+ number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber,
376
+ nodeId: remote.id,
377
+ url: remote.displayUrl || fallback?.url || '',
378
+ state: remote.state === 'closed' ? 'closed' : 'open',
379
+ title: remote.title,
380
+ body: remote.body,
381
+ draft: remote.draft ?? fallback?.draft ?? false,
382
+ head: { ...head, sha: remote.headSha || head.sha },
383
+ base: { ...base, sha: remote.baseSha || base.sha },
384
+ mergedAt: remote.mergedAt ?? fallback?.mergedAt ?? null,
385
+ mergeCommitSha: remote.mergeCommitSha ?? fallback?.mergeCommitSha ?? null,
386
+ labels: [...(remote.labels || fallback?.labels || [])].sort(),
387
+ assignees: [...(remote.assignees || fallback?.assignees || [])].sort(),
388
+ milestone: remote.milestone ?? fallback?.milestone ?? null,
389
+ mergeability: remote.mergeability || fallback?.mergeability || { state: 'unknown', detail: null }
390
+ };
391
+ Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
392
+ return snapshot;
393
+ }
394
+ function pullRequestIdentity(pullRequest) {
395
+ return pullRequest.identity || { kind: 'number', value: pullRequest.number };
396
+ }
397
+ function providerPullRequestError(base, error, prNumber) {
398
+ return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, base.issueNumber, prNumber, {
399
+ changed: error.code.startsWith('IN_LABEL_SYNC'),
400
+ platform: base.context.platform,
401
+ capabilities: base.context.capabilities,
402
+ resource: { kind: 'pull-request', number: prNumber },
403
+ error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
404
+ });
405
+ }
406
+ async function inspectExternalPullRequest(base, prIdentity) {
407
+ const prNumber = resourceIdentityNumber(prIdentity);
408
+ const fallback = base.fact?.state === 'bound' && resourceIdentityEquals(base.fact.identity.resource, prIdentity)
409
+ ? base.fact.identity
410
+ : null;
411
+ const inspected = base.provider.changeRequests?.inspect
412
+ ? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: prIdentity })
413
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
414
+ if (!inspected.ok)
415
+ return providerPullRequestError(base, inspected.error, prNumber);
416
+ const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
417
+ return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
418
+ platform: base.context.platform,
419
+ capabilities: base.context.capabilities,
420
+ resource: { kind: 'pull-request', number: prNumber, identity: prIdentity },
421
+ pullRequest,
422
+ error: null
423
+ });
326
424
  }
327
425
  function inspectGitHubPullRequest(client, repository, number, cwd) {
328
426
  const fetched = client.json(['api', `repos/${repository}/pulls/${number}`], { cwd });
@@ -333,191 +431,369 @@ function inspectGitHubPullRequest(client, repository, number, cwd) {
333
431
  ? { ok: true, value: pullRequest }
334
432
  : { ok: false, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
335
433
  }
336
- function configuredGitRemotes(cwd) {
434
+ async function inspectPlatformPullRequest(taskRef, options = {}) {
435
+ const base = await resolvedContext(taskRef, options);
436
+ if (!base.ok)
437
+ return base.output;
438
+ if (!base.fact)
439
+ return result('no-op', base.resolved.taskId, base.issueNumber, null, {
440
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
441
+ });
442
+ if (!base.prIdentity)
443
+ return result('no-op', base.resolved.taskId, base.issueNumber, null, {
444
+ platform: base.context.platform, capabilities: base.context.capabilities,
445
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
446
+ });
447
+ return inspectExternalPullRequest(base, base.prIdentity);
448
+ }
449
+ async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
450
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
451
+ const context = loaded.ok ? loaded.value.context : loaded.context;
452
+ const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
453
+ if (!usable) {
454
+ return result(context.status, null, null, null, {
455
+ platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
456
+ });
457
+ }
458
+ let identity;
337
459
  try {
338
- return execFileSync('git', ['remote'], {
339
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
340
- }).split(/\r?\n/).filter(Boolean).flatMap((name) => {
341
- try {
342
- const url = execFileSync('git', ['config', '--get', `remote.${name}.url`], {
343
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
344
- }).trim();
345
- const repository = parseGitHubRemote(url);
346
- return repository ? [{ name, url, repository }] : [];
347
- }
348
- catch {
349
- return [];
350
- }
460
+ identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)) : resourceIdentity(prNumber);
461
+ }
462
+ catch (error) {
463
+ return result('failed', null, null, null, {
464
+ platform: context.platform, capabilities: context.capabilities,
465
+ error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
351
466
  });
352
467
  }
353
- catch {
354
- return [];
468
+ if (loaded.ok) {
469
+ const inspected = loaded.value.provider.changeRequests?.inspect
470
+ ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: identity })
471
+ : unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
472
+ const number = resourceIdentityNumber(identity);
473
+ if (!inspected.ok)
474
+ return result(providerStatus(inspected.error), null, null, number, {
475
+ platform: context.platform, capabilities: context.capabilities,
476
+ resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
477
+ });
478
+ return result('no-op', null, null, number, {
479
+ platform: context.platform, capabilities: context.capabilities,
480
+ resource: { kind: 'pull-request', number, identity },
481
+ pullRequest: normalizeProviderPullRequest(inspected.value, context.platform.repository, number || 0), error: null
482
+ });
355
483
  }
484
+ return result('failed', null, null, null, {
485
+ platform: context.platform, capabilities: context.capabilities,
486
+ resource: { kind: 'pull-request', number: null },
487
+ error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
488
+ });
356
489
  }
357
- function rewriteGitHubRemote(remote, repository) {
358
- const match = remote.trim().match(/^(https?:\/\/github\.com\/|ssh:\/\/(?:git@)?github\.com\/|git@github\.com:)[^/\s]+\/[^/\s]+(?:\.git)?$/i);
359
- return match ? `${match[1]}${repository}.git` : null;
490
+ const CLOSING_ISSUES_QUERY = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number} pageInfo{hasNextPage endCursor}}}}}';
491
+ function readInLabelMapping(repoRoot) {
492
+ try {
493
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
494
+ const mapping = validateInLabelMapping(config.labels?.in);
495
+ return mapping.ok ? { ok: true, value: mapping.value } : mapping;
496
+ }
497
+ catch (error) {
498
+ return {
499
+ ok: false,
500
+ error: { code: 'IN_LABEL_SYNC_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
501
+ };
502
+ }
360
503
  }
361
- function resolveGitHubChangeRequestGitEvidence({ cwd, repository, pullRequest }) {
362
- if (pullRequest.repository !== repository ||
363
- pullRequest.base.repository !== repository ||
364
- pullRequest.number <= 0) {
504
+ function taskInLabelTarget(repoRoot, frontmatter, currentLabels, repositoryLabels) {
505
+ const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
506
+ if (!baseRef)
365
507
  return {
366
508
  ok: false,
367
- error: {
368
- code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
369
- message: 'Pull request repository identity is inconsistent',
370
- retryable: false
371
- }
509
+ error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
372
510
  };
511
+ let changedFiles;
512
+ try {
513
+ changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
514
+ cwd: repoRoot, encoding: 'utf8'
515
+ }).trim().split(/\r?\n/).filter(Boolean);
373
516
  }
374
- const reviewedHeadRef = `refs/pull/${pullRequest.number}/head`;
375
- const targetHeadRef = `refs/heads/${pullRequest.base.ref}`;
376
- const refValid = (ref) => {
377
- try {
378
- execFileSync('git', ['check-ref-format', ref], {
379
- cwd, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore']
380
- });
381
- return true;
382
- }
383
- catch {
384
- return false;
385
- }
386
- };
387
- if (!refValid(reviewedHeadRef) || !refValid(targetHeadRef)) {
517
+ catch (error) {
388
518
  return {
389
519
  ok: false,
390
- error: {
391
- code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
392
- message: 'Pull request Git refs are invalid',
393
- retryable: false
394
- }
520
+ error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
395
521
  };
396
522
  }
397
- const remotes = configuredGitRemotes(cwd);
398
- const exact = remotes
399
- .filter((remote) => remote.repository === repository)
400
- .sort((left, right) => Number(right.name === 'origin') - Number(left.name === 'origin') ||
401
- left.name.localeCompare(right.name))[0];
402
- const origin = remotes.find((remote) => remote.name === 'origin');
403
- const remoteUrl = exact?.url || (origin ? rewriteGitHubRemote(origin.url, repository) : null);
404
- if (!remoteUrl) {
523
+ const mapping = readInLabelMapping(repoRoot);
524
+ if (!mapping.ok)
525
+ return mapping;
526
+ const planned = planInLabelUpdate({ changedFiles, currentLabels, mapping: mapping.value, repositoryLabels });
527
+ if (planned.error)
528
+ return { ok: false, error: planned.error };
529
+ return { ok: true, value: planned.target };
530
+ }
531
+ function inspectClosingIssueNumbers(client, repository, pullRequest, cwd) {
532
+ const [owner, name] = repository.split('/');
533
+ if (!owner || !name)
405
534
  return {
406
535
  ok: false,
407
- error: {
408
- code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
409
- message: `No GitHub remote can provide evidence for ${repository}`,
410
- retryable: false
411
- }
536
+ error: { code: 'IN_LABEL_SYNC_REPOSITORY_INVALID', message: 'Repository identity is invalid', retryable: false }
412
537
  };
538
+ let cursor = null;
539
+ const numbers = [];
540
+ for (;;) {
541
+ const args = ['api', 'graphql', '-f', `query=${CLOSING_ISSUES_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${pullRequest}`];
542
+ if (cursor)
543
+ args.push('-F', `cursor=${cursor}`);
544
+ const response = client.json(args, { cwd });
545
+ if (!response.ok)
546
+ return response;
547
+ const connection = response.value?.data?.repository?.pullRequest?.closingIssuesReferences;
548
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo)
549
+ return {
550
+ ok: false,
551
+ error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue response is incomplete', retryable: false }
552
+ };
553
+ for (const node of connection.nodes) {
554
+ const issue = Number(node?.number);
555
+ if (!Number.isSafeInteger(issue) || issue <= 0)
556
+ return {
557
+ ok: false,
558
+ error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue identity is invalid', retryable: false }
559
+ };
560
+ numbers.push(issue);
561
+ }
562
+ if (!connection.pageInfo.hasNextPage)
563
+ return { ok: true, value: [...new Set(numbers)] };
564
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor)
565
+ return {
566
+ ok: false,
567
+ error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue pagination cursor is invalid', retryable: false }
568
+ };
569
+ cursor = connection.pageInfo.endCursor;
413
570
  }
414
- return {
415
- ok: true,
416
- value: { remoteUrl, reviewedHeadRef, targetHeadRef }
417
- };
418
571
  }
419
- registerPlatformCapabilities('github', {
420
- inspectChangeRequest({ client, repository, number, cwd }) {
421
- return inspectGitHubPullRequest(client || createGitHubClient(), repository, number, cwd);
422
- },
423
- inspectIssueClosingChangeRequests({ client, repository, issueNumber, cwd }) {
424
- return inspectGitHubIssueClosingChangeRequests(client || createGitHubClient(), repository, issueNumber, cwd);
425
- },
426
- resolveChangeRequestGitEvidence(context) {
427
- return resolveGitHubChangeRequestGitEvidence(context);
428
- }
429
- });
430
- function locatePullRequest(base, head, target, expectedSha) {
431
- const repository = base.context.platform.repository;
432
- const listed = base.client.json([
433
- 'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
434
- ], { cwd: base.resolved.repoRoot });
435
- if (!listed.ok)
436
- return listed;
437
- const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
438
- return selected.status === 'resolved'
439
- ? { ok: true, value: selected.pullRequest }
440
- : { ok: false, error: {
441
- code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
442
- : selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
443
- message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
444
- : selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
445
- : 'No pull request matches the exact head/base identity',
446
- retryable: false
447
- } };
572
+ function inspectRepositoryLabels(client, repository, cwd) {
573
+ const response = client.json(['api', '--paginate', '--slurp', `repos/${repository}/labels?per_page=100`], { cwd });
574
+ if (!response.ok)
575
+ return response;
576
+ const labels = validateRepositoryLabelPayload(response.value);
577
+ return labels.ok ? { ok: true, value: new Set(labels.value) } : labels;
448
578
  }
449
- function inspectPlatformPullRequest(taskRef, options = {}) {
450
- const base = resolvedContext(taskRef, options);
451
- if (!base.ok)
452
- return base.output;
453
- if (!base.prNumber)
454
- return result('no-op', base.resolved.taskId, base.issueNumber, null, {
455
- platform: base.context.platform, capabilities: base.context.capabilities,
456
- error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
457
- });
458
- const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
459
- cwd: base.resolved.repoRoot,
460
- repository: base.context.platform.repository,
461
- number: base.prNumber,
462
- client: options.client || base.client
463
- });
464
- if (!fetched.ok || !fetched.value) {
465
- const error = fetched.error || {
466
- code: 'PR_INSPECTION_INVALID',
467
- message: 'Platform adapter returned no change-request snapshot',
468
- retryable: false
579
+ function inspectPullRequestFiles(client, repository, pullRequest, cwd) {
580
+ const response = client.json(['api', '--paginate', '--slurp', `repos/${repository}/pulls/${pullRequest}/files?per_page=100`], { cwd });
581
+ if (!response.ok)
582
+ return response;
583
+ const files = extractPullRequestFileNames(response.value);
584
+ return files ? { ok: true, value: files } : {
585
+ ok: false,
586
+ error: { code: 'IN_LABEL_SYNC_FILES_INVALID', message: 'Pull request files response is incomplete', retryable: false }
587
+ };
588
+ }
589
+ function inspectTaskInLabelTarget(repoRoot, frontmatter, pullRequest, repository, client) {
590
+ const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
591
+ if (!baseRef)
592
+ return {
593
+ ok: false,
594
+ error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
595
+ };
596
+ let changedFiles;
597
+ try {
598
+ changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
599
+ cwd: repoRoot, encoding: 'utf8'
600
+ }).trim().split(/\r?\n/).filter(Boolean);
601
+ }
602
+ catch (error) {
603
+ return {
604
+ ok: false,
605
+ error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
469
606
  };
470
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
471
- platform: base.context.platform, capabilities: base.context.capabilities,
472
- resource: { kind: 'pull-request', number: base.prNumber }, error
473
- });
474
607
  }
475
- return result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
476
- platform: base.context.platform, capabilities: base.context.capabilities,
477
- resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, error: null
608
+ const mapping = readInLabelMapping(repoRoot);
609
+ if (!mapping.ok)
610
+ return mapping;
611
+ const labels = inspectRepositoryLabels(client, repository, repoRoot);
612
+ if (!labels.ok)
613
+ return labels;
614
+ const planned = planInLabelUpdate({
615
+ changedFiles, currentLabels: pullRequest.labels, mapping: mapping.value, repositoryLabels: labels.value
478
616
  });
617
+ if (planned.error)
618
+ return { ok: false, error: planned.error };
619
+ return { ok: true, value: { changedFiles, target: planned.target } };
479
620
  }
480
- function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
621
+ function inLabelResource(kind, number, before, expected, after, effect) {
622
+ return { kind, number, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
623
+ }
624
+ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
625
+ const cwd = path.resolve(options.cwd || process.cwd());
481
626
  const client = options.client || createGitHubClient();
482
- const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
627
+ const context = await resolvePlatformContext({ cwd, client });
483
628
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
484
- if (!usable) {
485
- return result(context.status, null, null, null, {
629
+ if (!usable)
630
+ return result(context.status, null, null, prNumber, {
486
631
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
487
632
  });
488
- }
489
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
490
- return result('failed', null, null, null, {
633
+ if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
634
+ return result('failed', null, null, prNumber, {
491
635
  platform: context.platform, capabilities: context.capabilities,
492
636
  error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
493
637
  });
494
- }
495
- const fetched = inspectPlatformChangeRequest(context.platform.type, {
496
- cwd: options.cwd || process.cwd(),
497
- repository: context.platform.repository,
498
- number: prNumber,
499
- client
638
+ const repository = context.platform.repository;
639
+ const fetched = inspectGitHubPullRequest(client, repository, prNumber, cwd);
640
+ if (!fetched.ok)
641
+ return result(fetched.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
642
+ platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: fetched.error
643
+ });
644
+ const files = inspectPullRequestFiles(client, repository, prNumber, cwd);
645
+ if (!files.ok)
646
+ return result(files.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
647
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
648
+ resource: { kind: 'pull-request', number: prNumber }, error: files.error
649
+ });
650
+ const mapping = readInLabelMapping(cwd);
651
+ if (!mapping.ok)
652
+ return result('failed', null, null, prNumber, {
653
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
654
+ resource: { kind: 'pull-request', number: prNumber }, error: mapping.error
655
+ });
656
+ const repositoryLabels = inspectRepositoryLabels(client, repository, cwd);
657
+ if (!repositoryLabels.ok)
658
+ return result(repositoryLabels.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
659
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
660
+ resource: { kind: 'pull-request', number: prNumber }, error: repositoryLabels.error
661
+ });
662
+ const plannedTarget = planInLabelUpdate({
663
+ changedFiles: files.value, currentLabels: fetched.value.labels,
664
+ mapping: mapping.value, repositoryLabels: repositoryLabels.value
500
665
  });
501
- if (!fetched.ok || !fetched.value) {
502
- const error = fetched.error || {
503
- code: 'PR_INSPECTION_INVALID',
504
- message: 'Platform adapter returned no change-request snapshot',
505
- retryable: false
506
- };
507
- return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
508
- platform: context.platform, capabilities: context.capabilities,
509
- resource: { kind: 'pull-request', number: prNumber }, error
666
+ if (plannedTarget.error)
667
+ return result('failed', null, null, prNumber, {
668
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
669
+ resource: { kind: 'pull-request', number: prNumber }, error: plannedTarget.error
670
+ });
671
+ const target = plannedTarget.target;
672
+ const association = inspectClosingIssueNumbers(client, repository, prNumber, cwd);
673
+ if (!association.ok)
674
+ return result(association.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
675
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
676
+ resource: { kind: 'pull-request', number: prNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value }, error: association.error
510
677
  });
678
+ const issueNumber = association.value.length === 1 ? association.value[0] : null;
679
+ let issue = null;
680
+ if (issueNumber) {
681
+ const fetchedIssue = inspectGitHubIssue(client, repository, issueNumber, cwd);
682
+ if (!fetchedIssue.ok)
683
+ return result(fetchedIssue.error.retryable ? 'blocked' : 'failed', null, issueNumber, prNumber, {
684
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
685
+ resource: { kind: 'issue', number: issueNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: fetchedIssue.error
686
+ });
687
+ issue = fetchedIssue.value;
511
688
  }
512
- return result('no-op', null, null, prNumber, {
513
- platform: context.platform, capabilities: context.capabilities,
514
- resource: { kind: 'pull-request', number: prNumber }, pullRequest: fetched.value, error: null
689
+ const resources = issue
690
+ ? [
691
+ { kind: 'issue', number: issue.number, snapshot: issue },
692
+ { kind: 'pull-request', number: prNumber, snapshot: fetched.value }
693
+ ]
694
+ : [{ kind: 'pull-request', number: prNumber, snapshot: fetched.value }];
695
+ const plans = resources.map((resource) => ({
696
+ ...resource,
697
+ plan: planInLabelUpdate({
698
+ changedFiles: files.value, currentLabels: resource.snapshot.labels,
699
+ mapping: mapping.value, repositoryLabels: repositoryLabels.value
700
+ })
701
+ }));
702
+ const operations = [
703
+ { name: 'closing-issues', status: 'no-op', reasonCode: association.value.length === 1 ? null : association.value.length === 0 ? 'ISSUE_ASSOCIATION_NONE' : 'ISSUE_ASSOCIATION_AMBIGUOUS' },
704
+ ...plans.map((item) => ({
705
+ name: `labels:in:${item.kind}`,
706
+ status: !context.capabilities.triage ? 'skipped' : item.plan.changed ? 'planned' : 'no-op',
707
+ reasonCode: !context.capabilities.triage ? 'TRIAGE_REQUIRED' : null
708
+ }))
709
+ ];
710
+ const willWrite = context.capabilities.triage && !options.dryRun;
711
+ const resourcesState = plans.map((item) => inLabelResource(item.kind, item.number, item.snapshot.labels, target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
712
+ if (options.dryRun || !context.capabilities.triage)
713
+ return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
714
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
715
+ resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
716
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
717
+ });
718
+ let currentPullRequest = fetched.value;
719
+ let successfulWrites = 0;
720
+ for (const item of plans) {
721
+ if (!item.plan.changed)
722
+ continue;
723
+ const synced = syncLabelDelta(client, repository, item.number, cwd, item.snapshot.labels, item.plan.target);
724
+ if (synced.status === 'failed' || synced.status === 'blocked') {
725
+ const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number);
726
+ state.effect = synced.status === 'blocked' ? 'unknown' : 'no-op';
727
+ state.after = synced.status === 'blocked' ? null : item.snapshot.labels;
728
+ const partial = successfulWrites > 0 || synced.status === 'blocked';
729
+ const error = partial && synced.error
730
+ ? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
731
+ : synced.error;
732
+ return result(partial ? 'blocked' : synced.status, null, issueNumber, prNumber, {
733
+ changed: successfulWrites > 0 || synced.changed, platform: context.platform, capabilities: context.capabilities,
734
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
735
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error
736
+ });
737
+ }
738
+ successfulWrites += synced.changed ? 1 : 0;
739
+ const reread = item.kind === 'issue'
740
+ ? inspectGitHubIssue(client, repository, item.number, cwd)
741
+ : inspectGitHubPullRequest(client, repository, item.number, cwd);
742
+ if (!reread.ok)
743
+ return result('blocked', null, issueNumber, prNumber, {
744
+ changed: true, platform: context.platform, capabilities: context.capabilities,
745
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
746
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
747
+ error: { ...reread.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
748
+ });
749
+ const after = reread.value.labels;
750
+ const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number);
751
+ state.after = after;
752
+ state.effect = after.filter((label) => label.startsWith('in:')).sort().join('\0') === target.join('\0') ? 'applied' : 'unknown';
753
+ if (state.effect === 'unknown')
754
+ return result('blocked', null, issueNumber, prNumber, {
755
+ changed: true, platform: context.platform, capabilities: context.capabilities,
756
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
757
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
758
+ error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} #${item.number} did not converge to the expected in: labels`, retryable: true }
759
+ });
760
+ if (item.kind === 'pull-request')
761
+ currentPullRequest = reread.value;
762
+ }
763
+ return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
764
+ changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
765
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber },
766
+ operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
767
+ resources: resourcesState, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
515
768
  });
516
769
  }
517
770
  function appendActivity(content, line) {
518
771
  const body = extractSection(content, ['活动日志', 'Activity Log']);
519
772
  return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
520
773
  }
774
+ function deliveryIdentity(pullRequest) {
775
+ return {
776
+ resource: pullRequestIdentity(pullRequest),
777
+ repository: pullRequest.repository,
778
+ url: pullRequest.url,
779
+ head: { ...pullRequest.head },
780
+ base: { ...pullRequest.base }
781
+ };
782
+ }
783
+ function factTimestamp(timestamp) {
784
+ return new Date(timestamp.replace(' ', 'T')).toISOString();
785
+ }
786
+ function boundFactFor(pullRequest, source, issue, verifiedAt) {
787
+ return buildBoundFact({
788
+ identity: deliveryIdentity(pullRequest),
789
+ source,
790
+ ...(typeof issue === 'object' ? { issueIdentity: issue } : { issueNumber: issue }),
791
+ verifiedAt: factTimestamp(verifiedAt),
792
+ remoteState: pullRequest.state,
793
+ mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
794
+ mergeCommitSha: pullRequest.state === 'closed' ? pullRequest.mergeCommitSha : null
795
+ });
796
+ }
521
797
  function writeCreateStarted(base, agent) {
522
798
  const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
523
799
  const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
@@ -530,60 +806,93 @@ function writeCreateStarted(base, agent) {
530
806
  mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — **Create PR [started]** by ${agent} — started`) }]
531
807
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
532
808
  }
533
- function bindIdentity(base, pullRequest, agent, dryRun = false) {
534
- if (base.prNumber && base.prNumber !== pullRequest.number)
809
+ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expectedHead, expectedHeadSha) {
810
+ const identity = validateWriterIdentity(base, pullRequest, {
811
+ expectedHead: source === 'explicit-bind' ? String(base.frontmatter.branch || '') : expectedHead,
812
+ expectedHeadSha,
813
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
814
+ });
815
+ if (!identity.ok)
816
+ return { status: 'failed', error: identity.error };
817
+ if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)))
535
818
  return { status: 'failed', error: {
536
- code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`
819
+ code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity'
537
820
  } };
538
- if (base.prNumber === pullRequest.number)
821
+ if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)) && base.fact?.state === 'bound') {
822
+ if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest)))
823
+ return { status: 'failed', error: {
824
+ code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
825
+ } };
539
826
  return { status: 'no-op', error: null };
827
+ }
540
828
  const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
541
829
  const metadata = captureTaskWriteMetadata();
830
+ const fact = boundFactFor(pullRequest, source, taskIssueIdentity(base.frontmatter), metadata.timestamp);
831
+ const factMutation = factFrontmatterMutation(fact);
542
832
  return writeTask({
543
833
  taskRef: base.resolved.taskId,
544
834
  expectedState: 'active',
545
835
  dryRun,
546
836
  mutations: [
547
- { kind: 'frontmatter', set: { pr_number: pullRequest.number, pr_status: 'created', assigned_to: agent } },
837
+ { kind: 'frontmatter', set: { ...factMutation.set, assigned_to: agent } },
548
838
  { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(current, `- ${metadata.timestamp} — **Create PR** by ${agent} — PR #${pullRequest.number} created → ${pullRequest.url}`) }
549
839
  ]
550
840
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
551
841
  }
552
- function bindPlatformPullRequest(taskRef, options) {
553
- const base = resolvedContext(taskRef, options);
842
+ async function bindPlatformPullRequest(taskRef, options) {
843
+ const base = await resolvedContext(taskRef, options);
554
844
  if (!base.ok)
555
845
  return base.output;
556
- if (!Number.isInteger(options.pr) || options.pr <= 0)
557
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
558
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
846
+ if (!base.fact)
847
+ return result('failed', base.resolved.taskId, base.issueNumber, null, {
848
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
559
849
  });
560
- if (base.prNumber && base.prNumber !== options.pr)
850
+ let prIdentity;
851
+ try {
852
+ prIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
853
+ }
854
+ catch (error) {
855
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
856
+ }
857
+ const requestedNumber = resourceIdentityNumber(prIdentity);
858
+ if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, prIdentity))
561
859
  return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
562
- error: { code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`, retryable: false }
860
+ error: { code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity', retryable: false }
563
861
  });
564
- const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository, options.pr, base.resolved.repoRoot);
565
- if (!fetched.ok)
566
- return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
567
- const written = bindIdentity(base, fetched.value, options.agent, options.dryRun);
862
+ const inspected = await inspectExternalPullRequest(base, prIdentity);
863
+ if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
864
+ return inspected;
865
+ const initialIdentity = validateWriterIdentity(base, inspected.pullRequest, {
866
+ expectedHead: String(base.frontmatter.branch || ''),
867
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
868
+ });
869
+ if (!initialIdentity.ok)
870
+ return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
871
+ pullRequest: inspected.pullRequest, error: initialIdentity.error
872
+ });
873
+ const written = bindIdentity(base, inspected.pullRequest, options.agent, 'explicit-bind', options.dryRun);
568
874
  if (written.status === 'failed')
569
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
570
- pullRequest: fetched.value, error: { code: written.error.code, message: written.error.message, retryable: false }
875
+ return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
876
+ pullRequest: inspected.pullRequest, error: { code: written.error.code, message: written.error.message, retryable: false }
571
877
  });
572
- return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : fetched.value.number, {
878
+ return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : inspected.pullRequest.number, {
573
879
  changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
574
- resource: { kind: 'pull-request', number: fetched.value.number }, pullRequest: fetched.value,
880
+ resource: { kind: 'pull-request', number: inspected.pullRequest.number, identity: pullRequestIdentity(inspected.pullRequest) }, pullRequest: inspected.pullRequest,
575
881
  operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
576
882
  });
577
883
  }
578
- function externalEvidenceNote(issueNumber, source, pullRequest) {
884
+ function externalEvidenceNote(issueIdentity, source, pullRequest) {
579
885
  return [
580
886
  `authorization=${source}`,
581
- externalIdentityEvidenceNote(issueNumber, pullRequest)
887
+ externalIdentityEvidenceNote(issueIdentity, pullRequest)
582
888
  ].join('; ');
583
889
  }
584
- function externalIdentityEvidenceNote(issueNumber, pullRequest) {
890
+ function externalIdentityEvidenceNote(issueIdentity, pullRequest) {
891
+ const issueLabel = issueIdentity
892
+ ? issueIdentity.kind === 'number' ? `#${issueIdentity.value}` : `${issueIdentity.kind}:${issueIdentity.value}`
893
+ : 'unknown';
585
894
  return [
586
- `issue=#${issueNumber}`,
895
+ `issue=${issueLabel}`,
587
896
  `pr=#${pullRequest.number}`,
588
897
  `url=${pullRequest.url}`,
589
898
  `base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
@@ -592,10 +901,14 @@ function externalIdentityEvidenceNote(issueNumber, pullRequest) {
592
901
  `mergeCommitSha=${pullRequest.mergeCommitSha}`
593
902
  ].join('; ');
594
903
  }
595
- function resolveExternalPullRequest(taskRef, options) {
596
- const base = resolvedContext(taskRef, options);
904
+ async function resolveExternalPullRequest(taskRef, options) {
905
+ const base = await resolvedContext(taskRef, options);
597
906
  if (!base.ok)
598
907
  return externalResult(base.output);
908
+ if (!base.fact)
909
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
910
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
911
+ }));
599
912
  const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
600
913
  if (inventory.status === 'failed')
601
914
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
@@ -617,25 +930,40 @@ function resolveExternalPullRequest(taskRef, options) {
617
930
  error: null
618
931
  }), { mode: 'normal' });
619
932
  }
620
- if (!base.issueNumber)
933
+ if (!base.issueIdentity)
621
934
  return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
622
935
  platform: base.context.platform,
623
936
  capabilities: base.context.capabilities,
624
937
  error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
625
938
  }));
626
- const inspected = inspectPlatformIssueClosingChangeRequests(base.context.platform.type, {
627
- cwd: base.resolved.repoRoot,
628
- repository: base.context.platform.repository,
629
- issueNumber: base.issueNumber,
630
- client: base.client
631
- });
939
+ const inspected = base.provider.changeRequests?.listClosing
940
+ ? await base.provider.changeRequests.listClosing({
941
+ context: providerOperationContext(base.loadedContext),
942
+ issue: base.issueIdentity
943
+ })
944
+ : unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
632
945
  if (!inspected.ok || !inspected.value) {
633
- const error = inspected.error || { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
946
+ const error = 'error' in inspected && inspected.error
947
+ ? inspected.error
948
+ : { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
634
949
  return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
635
950
  platform: base.context.platform, capabilities: base.context.capabilities, error
636
951
  }));
637
952
  }
638
- const selected = selectExternalPullRequest(inspected.value, base.context.platform.repository, base.prNumber, options.pr ?? null);
953
+ const candidates = inspected.value.map((item) => normalizeProviderPullRequest(item, base.context.platform.repository, item.number || 0));
954
+ let explicitPrIdentity = null;
955
+ if (options.pr !== undefined) {
956
+ try {
957
+ explicitPrIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
958
+ }
959
+ catch (error) {
960
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
961
+ platform: base.context.platform, capabilities: base.context.capabilities,
962
+ error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
963
+ }));
964
+ }
965
+ }
966
+ const selected = selectExternalPullRequest(candidates, base.context.platform.repository, base.prIdentity, explicitPrIdentity);
639
967
  if (selected.status === 'normal')
640
968
  return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
641
969
  platform: base.context.platform,
@@ -649,8 +977,47 @@ function resolveExternalPullRequest(taskRef, options) {
649
977
  capabilities: base.context.capabilities,
650
978
  error: { code: selected.code, message: selected.message, retryable: false }
651
979
  }), { candidates: selected.candidates, eligible: selected.eligible });
652
- const note = externalEvidenceNote(base.issueNumber, selected.source, selected.selected);
653
- const identityNote = externalIdentityEvidenceNote(base.issueNumber, selected.selected);
980
+ const initialIdentity = validateWriterIdentity(base, selected.selected, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
981
+ if (!initialIdentity.ok)
982
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
983
+ platform: base.context.platform, capabilities: base.context.capabilities,
984
+ pullRequest: selected.selected, error: initialIdentity.error
985
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
986
+ const rechecked = base.provider.changeRequests?.inspect
987
+ ? await base.provider.changeRequests.inspect({
988
+ context: providerOperationContext(base.loadedContext),
989
+ target: pullRequestIdentity(selected.selected)
990
+ }).then((response) => response.ok
991
+ ? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
992
+ : response)
993
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
994
+ if (!rechecked.ok || !rechecked.value)
995
+ return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
996
+ platform: base.context.platform, capabilities: base.context.capabilities,
997
+ pullRequest: selected.selected,
998
+ error: rechecked.ok ? { code: 'PR_EXTERNAL_RECHECK_FAILED', message: 'Selected pull request recheck returned no identity', retryable: false } : rechecked.error
999
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
1000
+ if (!sameDeliveryIdentity(selected.selected, rechecked.value))
1001
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1002
+ platform: base.context.platform, capabilities: base.context.capabilities,
1003
+ pullRequest: rechecked.value,
1004
+ error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request identity changed before task binding', retryable: false }
1005
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1006
+ const finalIdentity = validateWriterIdentity(base, rechecked.value, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
1007
+ if (!finalIdentity.ok)
1008
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1009
+ platform: base.context.platform, capabilities: base.context.capabilities,
1010
+ pullRequest: rechecked.value, error: finalIdentity.error
1011
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1012
+ const finalEvidence = validateExternalMergedEvidence(selected.selected, rechecked.value);
1013
+ if (!finalEvidence.ok)
1014
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1015
+ platform: base.context.platform, capabilities: base.context.capabilities,
1016
+ pullRequest: rechecked.value, error: finalEvidence.error
1017
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1018
+ const selectedPullRequest = rechecked.value;
1019
+ const note = externalEvidenceNote(base.issueIdentity, selected.source, selectedPullRequest);
1020
+ const identityNote = externalIdentityEvidenceNote(base.issueIdentity, selectedPullRequest);
654
1021
  const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
655
1022
  const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
656
1023
  const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
@@ -658,25 +1025,25 @@ function resolveExternalPullRequest(taskRef, options) {
658
1025
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
659
1026
  platform: base.context.platform,
660
1027
  capabilities: base.context.capabilities,
661
- pullRequest: selected.selected,
1028
+ pullRequest: selectedPullRequest,
662
1029
  error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
663
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
664
- if (alreadyAudited && base.prNumber !== selected.selected.number)
1030
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1031
+ if (alreadyAudited && base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)))
665
1032
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
666
1033
  platform: base.context.platform,
667
1034
  capabilities: base.context.capabilities,
668
- pullRequest: selected.selected,
1035
+ pullRequest: selectedPullRequest,
669
1036
  error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
670
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
671
- if (base.prNumber === selected.selected.number && alreadyAudited) {
1037
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1038
+ if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)) && alreadyAudited) {
672
1039
  return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
673
1040
  platform: base.context.platform,
674
1041
  capabilities: base.context.capabilities,
675
- resource: { kind: 'pull-request', number: selected.selected.number },
676
- pullRequest: selected.selected,
1042
+ resource: { kind: 'pull-request', number: selectedPullRequest.number },
1043
+ pullRequest: selectedPullRequest,
677
1044
  operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
678
1045
  error: null
679
- }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
1046
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
680
1047
  }
681
1048
  const metadata = captureTaskWriteMetadata();
682
1049
  const write = writeTask({
@@ -684,7 +1051,7 @@ function resolveExternalPullRequest(taskRef, options) {
684
1051
  expectedState: 'active',
685
1052
  dryRun: options.dryRun,
686
1053
  mutations: [
687
- { kind: 'frontmatter', set: { pr_number: selected.selected.number, pr_status: 'created', assigned_to: options.agent } },
1054
+ { kind: 'frontmatter', set: { ...factFrontmatterMutation(boundFactFor(selectedPullRequest, selected.source === 'unique' ? 'external-unique' : 'external-explicit', base.issueNumber, metadata.timestamp)).set, assigned_to: options.agent } },
688
1055
  { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — ${activityEntry}`) }
689
1056
  ]
690
1057
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
@@ -692,185 +1059,41 @@ function resolveExternalPullRequest(taskRef, options) {
692
1059
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
693
1060
  platform: base.context.platform,
694
1061
  capabilities: base.context.capabilities,
695
- pullRequest: selected.selected,
1062
+ pullRequest: selectedPullRequest,
696
1063
  error: { code: write.error.code, message: write.error.message, retryable: false }
697
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
1064
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
698
1065
  const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
699
- return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selected.selected.number, {
1066
+ return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selectedPullRequest.number, {
700
1067
  changed: !options.dryRun && write.changed,
701
1068
  platform: base.context.platform,
702
1069
  capabilities: base.context.capabilities,
703
- resource: { kind: 'pull-request', number: selected.selected.number },
704
- pullRequest: selected.selected,
1070
+ resource: { kind: 'pull-request', number: selectedPullRequest.number },
1071
+ pullRequest: selectedPullRequest,
705
1072
  operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
706
1073
  error: null
707
- }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
708
- }
709
- function createPlatformPullRequestUnlocked(taskRef, options) {
710
- const base = resolvedContext(taskRef, options);
711
- if (!base.ok)
712
- return base.output;
713
- if (!options.base || !options.head || !options.title.trim() || !options.body.trim())
714
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
715
- error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
716
- });
717
- if (base.prNumber) {
718
- const headCheck = verifyCreateHead(base, options.head);
719
- if (!headCheck.ok)
720
- return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
721
- platform: base.context.platform,
722
- capabilities: base.context.capabilities,
723
- error: headCheck.error
724
- });
725
- const inspected = inspectPlatformPullRequest(taskRef, options);
726
- const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
727
- if (!identity.ok)
728
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
729
- platform: base.context.platform,
730
- capabilities: base.context.capabilities,
731
- pullRequest: inspected.pullRequest,
732
- error: identity.error
733
- });
734
- return { ...inspected, result: 'no_op' };
735
- }
736
- const headCheck = verifyCreateHead(base, options.head);
737
- if (!headCheck.ok)
738
- return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
739
- platform: base.context.platform,
740
- capabilities: base.context.capabilities,
741
- error: headCheck.error
742
- });
743
- const expectedHeadSha = headCheck.value;
744
- const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
745
- if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
746
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
747
- platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
748
- });
749
- if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH')
750
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
751
- platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
752
- });
753
- if (options.dryRun)
754
- return result('planned', base.resolved.taskId, base.issueNumber, null, {
755
- platform: base.context.platform, capabilities: base.context.capabilities,
756
- operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
757
- result: located.ok ? 'pr_reused' : 'pr_created', error: null
758
- });
759
- const started = writeCreateStarted(base, options.agent);
760
- if (started.status === 'failed')
761
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
762
- error: { code: started.error.code, message: started.error.message, retryable: false }
763
- });
764
- let pullRequest = located.ok ? located.value : null;
765
- let created = false;
766
- if (!pullRequest) {
767
- const repository = base.context.platform.repository;
768
- const response = base.client.json(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
769
- cwd: base.resolved.repoRoot,
770
- method: 'POST',
771
- input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
772
- });
773
- if (response.ok)
774
- pullRequest = normalizePullRequest(response.value, repository);
775
- else if (response.error.retryable) {
776
- const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
777
- if (recovered.ok)
778
- pullRequest = recovered.value;
779
- else
780
- return result('blocked', base.resolved.taskId, base.issueNumber, null, {
781
- platform: base.context.platform, capabilities: base.context.capabilities,
782
- operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
783
- error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
784
- });
785
- }
786
- else
787
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
788
- platform: base.context.platform, capabilities: base.context.capabilities,
789
- operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
790
- });
791
- if (!pullRequest)
792
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
793
- error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
794
- });
795
- if (pullRequest.head.sha !== expectedHeadSha)
796
- return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
797
- platform: base.context.platform, capabilities: base.context.capabilities,
798
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
799
- error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
800
- });
801
- created = true;
802
- }
803
- const bindHeadCheck = verifyCreateHead(base, options.head);
804
- if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
805
- const error = bindHeadCheck.ok
806
- ? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
807
- : bindHeadCheck.error;
808
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
809
- platform: base.context.platform, capabilities: base.context.capabilities,
810
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
811
- });
812
- }
813
- const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository, pullRequest.number, base.resolved.repoRoot);
814
- if (!beforeBind.ok || !beforeBind.value) {
815
- const error = beforeBind.ok
816
- ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
817
- : beforeBind.error;
818
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
819
- platform: base.context.platform, capabilities: base.context.capabilities,
820
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
821
- });
822
- }
823
- if (beforeBind.value.head.sha !== expectedHeadSha
824
- || beforeBind.value.head.ref !== expectedHead(base.context.platform.repository, options.head).ref
825
- || beforeBind.value.base.ref !== options.base
826
- || beforeBind.value.base.repository !== base.context.platform.repository)
827
- return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
828
- platform: base.context.platform, capabilities: base.context.capabilities,
829
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
830
- error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
831
- });
832
- pullRequest = beforeBind.value;
833
- const refreshed = resolvedContext(taskRef, options);
834
- if (!refreshed.ok)
835
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
836
- pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
837
- });
838
- const bound = bindIdentity(refreshed, pullRequest, options.agent);
839
- if (bound.status === 'failed')
840
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
841
- platform: base.context.platform, capabilities: base.context.capabilities,
842
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
843
- operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
844
- error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
845
- });
846
- return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
847
- changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
848
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
849
- operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
850
- result: created ? 'pr_created' : 'pr_reused', error: null
851
- });
1074
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
852
1075
  }
853
- function createPlatformPullRequest(taskRef, options) {
854
- const base = resolvedContext(taskRef, options);
1076
+ async function createPlatformPullRequest(taskRef, options) {
1077
+ const base = await resolvedContext(taskRef, options);
855
1078
  if (!base.ok)
856
- return base.output;
1079
+ return withCreation(base.output, PRECONDITION_NOT_CREATED);
857
1080
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
858
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1081
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
859
1082
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
860
- });
1083
+ }), PRECONDITION_NOT_CREATED);
861
1084
  }
862
1085
  try {
863
- return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, options));
1086
+ return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createExternalPullRequest(taskRef, options, base));
864
1087
  }
865
1088
  catch (error) {
866
1089
  if (error instanceof TaskExecutionLockError) {
867
- return result('blocked', base.resolved.taskId, base.issueNumber, null, {
1090
+ return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
868
1091
  platform: base.context.platform,
869
1092
  capabilities: base.context.capabilities,
870
1093
  error: { code: error.code, message: error.message, retryable: true }
871
- });
1094
+ }), PRECONDITION_NOT_CREATED);
872
1095
  }
873
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
1096
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
874
1097
  platform: base.context.platform,
875
1098
  capabilities: base.context.capabilities,
876
1099
  error: {
@@ -878,13 +1101,147 @@ function createPlatformPullRequest(taskRef, options) {
878
1101
  message: error instanceof Error ? error.message : String(error),
879
1102
  retryable: false
880
1103
  }
1104
+ }), PRECONDITION_NOT_CREATED);
1105
+ }
1106
+ }
1107
+ async function createExternalPullRequest(taskRef, options, base) {
1108
+ if (!base.fact)
1109
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1110
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1111
+ }), PRECONDITION_NOT_CREATED);
1112
+ const target = taskDeliveryTarget(base);
1113
+ if (!target.ok)
1114
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1115
+ error: target.error
1116
+ }), PRECONDITION_NOT_CREATED);
1117
+ if (options.base !== target.value.baseRef)
1118
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1119
+ platform: base.context.platform,
1120
+ capabilities: base.context.capabilities,
1121
+ error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
1122
+ }), PRECONDITION_NOT_CREATED);
1123
+ let verifiedHeadSha;
1124
+ if (base.provider.changeRequests?.verifyHead) {
1125
+ const verified = await base.provider.changeRequests.verifyHead({
1126
+ context: providerOperationContext(base.loadedContext),
1127
+ head: options.head
881
1128
  });
1129
+ if (!verified.ok)
1130
+ return withCreation(providerPullRequestError(base, verified.error, base.prNumber), PRECONDITION_NOT_CREATED);
1131
+ verifiedHeadSha = verified.value.sha;
1132
+ }
1133
+ if (base.prIdentity) {
1134
+ const inspected = await inspectExternalPullRequest(base, base.prIdentity);
1135
+ return inspected.status === 'failed' || inspected.status === 'blocked'
1136
+ ? withCreation(inspected, PRECONDITION_NOT_CREATED)
1137
+ : withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
882
1138
  }
1139
+ if (options.dryRun)
1140
+ return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
1141
+ platform: base.context.platform,
1142
+ capabilities: base.context.capabilities,
1143
+ operations: [{ name: 'pr:create', status: 'planned', reasonCode: null }],
1144
+ result: 'pr_created',
1145
+ error: null
1146
+ }), { kind: 'planned', action: 'create', createdByCurrentOperation: false });
1147
+ const started = writeCreateStarted(base, options.agent);
1148
+ if (started.status === 'failed')
1149
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1150
+ error: { code: started.error.code, message: started.error.message, retryable: false }
1151
+ }), PRECONDITION_NOT_CREATED);
1152
+ const created = base.provider.changeRequests?.create
1153
+ ? await base.provider.changeRequests.create({
1154
+ context: providerOperationContext(base.loadedContext),
1155
+ base: options.base,
1156
+ head: options.head,
1157
+ title: options.title,
1158
+ body: options.body,
1159
+ draft: Boolean(options.draft),
1160
+ mutation: { idempotencyKey: `pull-request:create:${base.resolved.taskId}` }
1161
+ })
1162
+ : unsupportedProviderOperation(base.provider, 'changeRequests.create');
1163
+ if (!created.ok)
1164
+ return withCreation(providerPullRequestError(base, created.error, null), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
1165
+ const remoteId = String(created.value.remoteId || '');
1166
+ let createdIdentity;
1167
+ try {
1168
+ createdIdentity = providerResourceIdentity(base.provider, 'pull-request', remoteId);
1169
+ }
1170
+ catch (error) {
1171
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1172
+ platform: base.context.platform, capabilities: base.context.capabilities,
1173
+ error: { code: 'PR_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1174
+ }), { kind: 'created', createdByCurrentOperation: true });
1175
+ }
1176
+ const number = resourceIdentityNumber(createdIdentity);
1177
+ const fallbackNumber = number ?? 0;
1178
+ const fallback = normalizeProviderPullRequest({
1179
+ id: remoteId,
1180
+ identity: createdIdentity,
1181
+ number: number || undefined,
1182
+ state: 'open',
1183
+ title: options.title,
1184
+ body: options.body,
1185
+ headSha: '',
1186
+ baseSha: '',
1187
+ head: { repository: base.context.platform.repository, ref: options.head, sha: '' },
1188
+ base: { repository: base.context.platform.repository, ref: options.base, sha: '' },
1189
+ displayUrl: `${base.provider.type}:${remoteId}`
1190
+ }, base.context.platform.repository, fallbackNumber);
1191
+ const inspected = base.provider.changeRequests?.inspect
1192
+ ? await base.provider.changeRequests.inspect({
1193
+ context: providerOperationContext(base.loadedContext),
1194
+ target: createdIdentity
1195
+ })
1196
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1197
+ if (!inspected.ok)
1198
+ return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
1199
+ const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
1200
+ if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
1201
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1202
+ platform: base.context.platform,
1203
+ capabilities: base.context.capabilities,
1204
+ error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'Provider create response lacks validated identity', retryable: false }
1205
+ }), { kind: 'created', createdByCurrentOperation: true });
1206
+ const identity = validateWriterIdentity(base, pullRequest, {
1207
+ expectedHead: options.head,
1208
+ expectedHeadSha: verifiedHeadSha,
1209
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
1210
+ });
1211
+ if (!identity.ok)
1212
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1213
+ platform: base.context.platform,
1214
+ capabilities: base.context.capabilities,
1215
+ resource: { kind: 'pull-request', number: pullRequest.number },
1216
+ pullRequest,
1217
+ error: identity.error
1218
+ }), { kind: 'created', createdByCurrentOperation: true });
1219
+ const bound = bindIdentity(base, pullRequest, options.agent, 'created', false, options.head);
1220
+ if (bound.status === 'failed')
1221
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1222
+ platform: base.context.platform,
1223
+ capabilities: base.context.capabilities,
1224
+ resource: { kind: 'pull-request', number: pullRequest.number },
1225
+ pullRequest,
1226
+ error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
1227
+ }), { kind: 'created', createdByCurrentOperation: true });
1228
+ return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1229
+ changed: true,
1230
+ platform: base.context.platform,
1231
+ capabilities: base.context.capabilities,
1232
+ resource: { kind: 'pull-request', number: pullRequest.number },
1233
+ pullRequest,
1234
+ operations: [{ name: 'pr:create', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
1235
+ result: 'pr_created',
1236
+ error: null
1237
+ }), { kind: 'created', createdByCurrentOperation: true });
883
1238
  }
884
- function syncPlatformPullRequest(taskRef, options) {
1239
+ async function syncPlatformPullRequest(taskRef, options) {
885
1240
  const warningResult = warningResultForPrimary(options.primaryResult);
886
1241
  const softenFailure = (output) => {
1242
+ const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
887
1243
  const warning = output.error && output.task.prNumber !== null
1244
+ && !authorityError
888
1245
  && !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
889
1246
  ? {
890
1247
  code: output.error.code,
@@ -899,77 +1256,268 @@ function syncPlatformPullRequest(taskRef, options) {
899
1256
  ? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
900
1257
  : output;
901
1258
  };
902
- const base = resolvedContext(taskRef, options);
1259
+ const base = await resolvedContext(taskRef, options);
903
1260
  if (!base.ok)
904
1261
  return softenFailure(base.output);
905
- if (!base.prNumber)
1262
+ if (!base.fact)
906
1263
  return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
907
- error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
908
- }));
909
- const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository, base.prNumber, base.resolved.repoRoot);
910
- if (!fetched.ok)
911
- return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
912
- if (!options.metadata && !options.closingIssue)
913
- return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
914
- error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
915
- }));
916
- if (!base.issueNumber)
917
- return softenFailure(result('degraded', base.resolved.taskId, null, base.prNumber, {
918
- pullRequest: fetched.value, error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no linked Issue to copy metadata from', retryable: false }
919
- }));
920
- const issue = inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
921
- if (!issue.issue)
922
- return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
923
- platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, error: issue.error
1264
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
924
1265
  }));
925
- const planned = planPullRequestMetadata({
926
- pullRequest: fetched.value,
927
- issue: issue.issue,
928
- taskType: base.frontmatter.type || 'task',
929
- issueNumber: base.issueNumber,
930
- capabilities: base.context.capabilities
931
- }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
932
- .filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
933
- const operations = planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }));
934
- if (options.dryRun)
935
- return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
936
- platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, operations, error: null
1266
+ if (!base.prIdentity)
1267
+ return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1268
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
937
1269
  }));
938
- const payload = {};
939
- for (const operation of planned) {
940
- if (operation.status !== 'planned')
941
- continue;
942
- if (operation.name === 'labels')
943
- payload.labels = operation.value;
944
- if (operation.name === 'assignees')
945
- payload.assignees = operation.value;
946
- if (operation.name === 'closing-issue')
947
- payload.body = operation.value;
948
- if (operation.name === 'milestone') {
949
- const milestones = base.client.json(['api', '--paginate', '--slurp', `repos/${base.context.platform.repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
1270
+ {
1271
+ const inspected = await inspectExternalPullRequest(base, base.prIdentity);
1272
+ if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
1273
+ return softenFailure(inspected);
1274
+ if (!options.metadata && !options.closingIssue)
1275
+ return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1276
+ error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
1277
+ }));
1278
+ const issue = await inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
1279
+ if (!issue.issue)
1280
+ return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
1281
+ platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, error: issue.error
1282
+ }));
1283
+ let inLabels = inspected.pullRequest.labels.filter((label) => label.startsWith('in:')).sort();
1284
+ let issueInLabelOperation = null;
1285
+ if (options.metadata && base.provider.issues?.listLabels) {
1286
+ const labels = await base.provider.issues.listLabels({ context: providerOperationContext(base.loadedContext) });
1287
+ if (!labels.ok)
1288
+ return softenFailure(providerPullRequestError(base, labels.error, base.prNumber));
1289
+ const target = taskInLabelTarget(base.resolved.repoRoot, base.frontmatter, inspected.pullRequest.labels, new Set(labels.value));
1290
+ if (!target.ok)
1291
+ return softenFailure(providerPullRequestError(base, target.error, base.prNumber));
1292
+ inLabels = target.value;
1293
+ const currentIssueLabels = issue.issue.labels.filter((label) => label.startsWith('in:')).sort();
1294
+ issueInLabelOperation = base.context.capabilities.triage
1295
+ ? currentIssueLabels.join('\0') === inLabels.join('\0')
1296
+ ? { name: 'labels:in:issue', status: 'no-op', reasonCode: null }
1297
+ : { name: 'labels:in:issue', status: 'planned', reasonCode: null }
1298
+ : { name: 'labels:in:issue', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' };
1299
+ }
1300
+ const planned = planPullRequestMetadata({
1301
+ pullRequest: inspected.pullRequest,
1302
+ issue: issue.issue,
1303
+ taskType: String(base.frontmatter.type || 'task'),
1304
+ issueNumber: base.issueNumber,
1305
+ capabilities: base.context.capabilities,
1306
+ inLabels
1307
+ }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
1308
+ .filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
1309
+ const operations = [
1310
+ ...(issueInLabelOperation ? [issueInLabelOperation] : []),
1311
+ ...planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }))
1312
+ ];
1313
+ const milestoneOperation = planned.find((operation) => operation.name === 'milestone' && operation.status === 'planned');
1314
+ if (milestoneOperation && base.provider.issues?.listMilestones) {
1315
+ const milestones = await base.provider.issues.listMilestones({ context: providerOperationContext(base.loadedContext) });
950
1316
  if (!milestones.ok)
951
- return softenFailure(result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error }));
952
- const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
953
- payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
1317
+ return providerPullRequestError(base, milestones.error, base.prNumber);
954
1318
  }
955
- }
956
- if (Object.keys(payload).length === 0) {
957
- const degraded = planned.some((operation) => operation.status === 'skipped');
958
- return softenFailure(result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
959
- platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, operations, error: null
1319
+ if (options.dryRun)
1320
+ return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1321
+ platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, operations, error: null
1322
+ }));
1323
+ let issueChanged = false;
1324
+ if (issueInLabelOperation?.status === 'planned') {
1325
+ const desiredIssueLabels = [...new Set([
1326
+ ...issue.issue.labels.filter((label) => !label.startsWith('in:')),
1327
+ ...inLabels
1328
+ ])].sort();
1329
+ const updatedIssue = base.provider.issues?.update
1330
+ ? await base.provider.issues.update({
1331
+ context: providerOperationContext(base.loadedContext),
1332
+ target: base.issueIdentity,
1333
+ currentLabels: issue.issue.labels,
1334
+ patch: { labels: desiredIssueLabels },
1335
+ mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}:in-labels` }
1336
+ })
1337
+ : unsupportedProviderOperation(base.provider, 'issues.update');
1338
+ if (!updatedIssue.ok)
1339
+ return softenFailure(providerPullRequestError(base, updatedIssue.error, base.prNumber));
1340
+ issueChanged = updatedIssue.value.changed;
1341
+ const rereadIssue = base.provider.issues?.inspect
1342
+ ? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
1343
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
1344
+ if (!rereadIssue.ok)
1345
+ return softenFailure(providerPullRequestError(base, {
1346
+ ...rereadIssue.error,
1347
+ code: 'IN_LABEL_SYNC_PARTIAL',
1348
+ message: `In-label synchronization is partial or unknown: ${rereadIssue.error.message}`
1349
+ }, base.prNumber));
1350
+ const actualIssueLabels = rereadIssue.value.labels.filter((label) => label.startsWith('in:')).sort();
1351
+ if (actualIssueLabels.join('\0') !== inLabels.join('\0'))
1352
+ return softenFailure(providerPullRequestError(base, {
1353
+ code: 'IN_LABEL_SYNC_PARTIAL',
1354
+ message: 'Issue in: labels did not converge after update',
1355
+ retryable: true
1356
+ }, base.prNumber));
1357
+ }
1358
+ const patch = {};
1359
+ for (const operation of planned) {
1360
+ if (operation.status !== 'planned')
1361
+ continue;
1362
+ if (operation.name === 'labels')
1363
+ patch.labels = operation.value;
1364
+ if (operation.name === 'assignees')
1365
+ patch.assignees = operation.value;
1366
+ if (operation.name === 'closing-issue')
1367
+ patch.body = operation.value;
1368
+ if (operation.name === 'milestone')
1369
+ patch.milestone = operation.value;
1370
+ }
1371
+ if (Object.keys(patch).length === 0)
1372
+ return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1373
+ platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: inspected.pullRequest, operations, error: null
1374
+ }));
1375
+ const updated = base.provider.changeRequests?.update
1376
+ ? await base.provider.changeRequests.update({
1377
+ context: providerOperationContext(base.loadedContext),
1378
+ target: base.prIdentity,
1379
+ currentLabels: inspected.pullRequest.labels,
1380
+ patch: patch,
1381
+ mutation: { idempotencyKey: `pull-request:update:${base.resolved.taskId}` }
1382
+ })
1383
+ : unsupportedProviderOperation(base.provider, 'changeRequests.update');
1384
+ if (!updated.ok) {
1385
+ const error = issueChanged && !updated.error.code.startsWith('IN_LABEL_SYNC')
1386
+ ? { ...updated.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${updated.error.message}` }
1387
+ : updated.error;
1388
+ return softenFailure(providerPullRequestError(base, error, base.prNumber));
1389
+ }
1390
+ let finalPullRequest = inspected.pullRequest;
1391
+ if (planned.some((operation) => operation.name === 'labels' && operation.status === 'planned')) {
1392
+ const reread = base.provider.changeRequests?.inspect
1393
+ ? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: base.prIdentity })
1394
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1395
+ if (!reread.ok)
1396
+ return softenFailure(providerPullRequestError(base, {
1397
+ ...reread.error,
1398
+ code: 'IN_LABEL_SYNC_PARTIAL',
1399
+ message: `In-label synchronization is partial or unknown: ${reread.error.message}`
1400
+ }, base.prNumber));
1401
+ finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
1402
+ const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
1403
+ .filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
1404
+ const actual = finalPullRequest.labels.filter((label) => label.startsWith('in:') || label.startsWith('type:')).sort();
1405
+ if (actual.join('\0') !== expected.join('\0'))
1406
+ return softenFailure(providerPullRequestError(base, {
1407
+ code: 'IN_LABEL_SYNC_PARTIAL',
1408
+ message: 'Pull request labels did not converge after update',
1409
+ retryable: true
1410
+ }, base.prNumber));
1411
+ }
1412
+ return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
1413
+ changed: issueChanged || updated.value.changed,
1414
+ platform: base.context.platform,
1415
+ capabilities: base.context.capabilities,
1416
+ resource: { kind: 'pull-request', number: base.prNumber },
1417
+ pullRequest: finalPullRequest,
1418
+ operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
1419
+ error: null
960
1420
  }));
961
1421
  }
962
- const patched = base.client.json(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
963
- cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
1422
+ }
1423
+ function readProjectPrFlow(repoRoot) {
1424
+ try {
1425
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
1426
+ return config.prFlow === 'required' || config.prFlow === 'disabled' ? config.prFlow : undefined;
1427
+ }
1428
+ catch {
1429
+ return undefined;
1430
+ }
1431
+ }
1432
+ function skipPlatformPullRequestFactUnlocked(resolved, options) {
1433
+ let content;
1434
+ let frontmatter;
1435
+ try {
1436
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
1437
+ frontmatter = parseTypedTaskFrontmatter(content);
1438
+ }
1439
+ catch (error) {
1440
+ return result('failed', resolved.taskId, null, null, {
1441
+ error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1442
+ });
1443
+ }
1444
+ let issueIdentity;
1445
+ try {
1446
+ issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
1447
+ }
1448
+ catch (error) {
1449
+ return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } });
1450
+ }
1451
+ const issueNumber = resourceIdentityNumber(issueIdentity);
1452
+ const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
1453
+ if (existing.status === 'invalid')
1454
+ return result('failed', resolved.taskId, issueNumber, null, {
1455
+ error: { code: existing.error.code, message: existing.error.message, retryable: false }
1456
+ });
1457
+ if (existing.status === 'missing')
1458
+ return result('failed', resolved.taskId, issueNumber, null, {
1459
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1460
+ });
1461
+ if (existing.fact.state === 'skipped')
1462
+ return result('no-op', resolved.taskId, issueNumber, null, {
1463
+ operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
1464
+ error: null
1465
+ });
1466
+ if (existing.fact.state === 'bound') {
1467
+ const boundNumber = resourceIdentityNumber(existing.fact.identity.resource);
1468
+ return result('failed', resolved.taskId, issueNumber, boundNumber, {
1469
+ error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR ${boundNumber === null ? 'identity' : `#${boundNumber}`}`, retryable: false }
1470
+ });
1471
+ }
1472
+ if (readProjectPrFlow(resolved.repoRoot) === 'required')
1473
+ return result('failed', resolved.taskId, issueNumber, null, {
1474
+ error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
1475
+ });
1476
+ const metadata = captureTaskWriteMetadata();
1477
+ const fact = buildSkippedFact(factTimestamp(metadata.timestamp));
1478
+ const write = writeTask({
1479
+ taskRef: resolved.taskId,
1480
+ expectedState: 'active',
1481
+ dryRun: options.dryRun,
1482
+ mutations: [
1483
+ { kind: 'frontmatter', set: { ...factFrontmatterMutation(fact).set, assigned_to: options.agent } },
1484
+ { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(content, `- ${metadata.timestamp} — **Skip PR** by ${options.agent} — PR delivery explicitly skipped`) }
1485
+ ]
1486
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
1487
+ if (write.status === 'failed')
1488
+ return result('failed', resolved.taskId, issueNumber, null, {
1489
+ error: { code: write.error.code, message: write.error.message, retryable: false }
1490
+ });
1491
+ const status = options.dryRun ? 'planned' : write.status === 'no-op' ? 'no-op' : 'applied';
1492
+ return result(status, resolved.taskId, issueNumber, null, {
1493
+ changed: !options.dryRun && write.changed,
1494
+ operations: [{ name: 'task:skip-pr', status, reasonCode: null }],
1495
+ error: null
964
1496
  });
965
- if (!patched.ok)
966
- return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
967
- return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
968
- changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
969
- resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
970
- operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
971
- }));
972
1497
  }
973
- export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveGitHubChangeRequestGitEvidence, resolveExternalPullRequest, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest };
1498
+ async function skipPlatformPullRequestFact(taskRef, options) {
1499
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
1500
+ if (!resolved.ok)
1501
+ return result('failed', resolved.taskId, null, null, {
1502
+ error: { code: resolved.code, message: resolved.message, retryable: false }
1503
+ });
1504
+ if (resolved.state !== 'active')
1505
+ return result('failed', resolved.taskId, null, null, {
1506
+ error: { code: 'TASK_STATE_INVALID', message: 'platform-pr skip requires an active task', retryable: false }
1507
+ });
1508
+ try {
1509
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'platform-pr.skip', () => skipPlatformPullRequestFactUnlocked(resolved, options));
1510
+ }
1511
+ catch (error) {
1512
+ if (error instanceof TaskExecutionLockError)
1513
+ return result('blocked', resolved.taskId, null, null, {
1514
+ error: { code: error.code, message: error.message, retryable: true }
1515
+ });
1516
+ return result('failed', resolved.taskId, null, null, {
1517
+ error: { code: 'PR_SKIP_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
1518
+ });
1519
+ }
1520
+ }
1521
+ export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
974
1522
  export { warningResultForPrimary };
975
1523
  //# sourceMappingURL=pull-requests.js.map