@fitlab-ai/agent-infra 0.9.12 → 0.9.14

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