@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
@@ -6,17 +6,15 @@ import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
6
6
  import { resolveTaskRef } from '../task/resolve-ref.ts';
7
7
  import { extractSection } from '../task/sections.ts';
8
8
  import { captureTaskWriteMetadata, writeTask } from '../task/write.ts';
9
- import {
10
- inspectPlatformChangeRequest,
11
- inspectPlatformIssueClosingChangeRequests,
12
- registerPlatformCapabilities
13
- } from './adapters.ts';
14
9
  import type { PlatformChangeRequestSnapshot } from './adapters.ts';
15
- import { parseGitHubRemote, resolvePlatformContext } from './context.ts';
16
- import { createGitHubClient } from './github-client.ts';
17
- import type { GitHubClient } from './github-client.ts';
10
+ import { resolvePlatformProviderContext } from './context.ts';
11
+ import type { PlatformClient } from './context.ts';
18
12
  import { inspectPlatformIssue } from './issues.ts';
19
13
  import { planPullRequestMetadata } from './pull-request-metadata.ts';
14
+ import {
15
+ planInLabelUpdate,
16
+ validateInLabelMapping
17
+ } from './in-label-sync.ts';
20
18
  import { platformResult } from './types.ts';
21
19
  import type { PlatformOperation, PlatformResult } from './types.ts';
22
20
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
@@ -30,6 +28,19 @@ import {
30
28
  readPrDeliveryFact
31
29
  } from '../task/pr-delivery-fact.ts';
32
30
  import type { CreationOutcome, PrDeliveryBindingSource, PrDeliveryFact } from '../task/pr-delivery-fact.ts';
31
+ import {
32
+ providerError,
33
+ providerOperationContext,
34
+ providerStatus,
35
+ providerResourceToken,
36
+ providerResourceIdentity,
37
+ resourceIdentity,
38
+ unsupportedProviderOperation
39
+ } from './provider-bridge.ts';
40
+ import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, serializeResourceIdentity } from './resource-identity.ts';
41
+ import type { ResourceIdentity } from './resource-identity.ts';
42
+ import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
43
+ import type { ChangeRequestSnapshot as ProviderChangeRequestSnapshot, IssueSnapshot as ProviderIssueSnapshot } from './provider-contract.ts';
33
44
 
34
45
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
35
46
 
@@ -39,9 +50,20 @@ type PullRequestResult = PlatformResult & {
39
50
  result: 'pr_created' | 'pr_reused' | 'no_op' | 'pr_created_with_warnings' | 'pr_reused_with_warnings' | 'no_op_with_warnings' | 'failed' | 'blocked' | null;
40
51
  creation: CreationOutcome | null;
41
52
  warnings: readonly OperationWarning[];
53
+ evidence?: { kind: string; pullRequestFiles?: string[]; closingIssues?: ResourceIdentity[] };
54
+ resources?: Array<{
55
+ kind: 'issue' | 'pull-request';
56
+ number: number | null;
57
+ identity: ResourceIdentity;
58
+ before: string[];
59
+ expected: string[];
60
+ after: string[] | null;
61
+ effect: 'no-op' | 'applied' | 'unknown';
62
+ }>;
42
63
  };
43
- type InspectionOptions = { cwd?: string; client?: unknown };
44
- type SharedOptions = { cwd?: string; client?: GitHubClient };
64
+ type GitHubClient = PlatformClient;
65
+ type InspectionOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
66
+ type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
45
67
  type CreateOptions = SharedOptions & {
46
68
  agent: string;
47
69
  base: string;
@@ -52,11 +74,11 @@ type CreateOptions = SharedOptions & {
52
74
  dryRun?: boolean;
53
75
  phase?: { value: CreatePhase };
54
76
  };
55
- type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
77
+ type BindOptions = SharedOptions & { agent: string; pr: string | number; dryRun?: boolean };
56
78
  type PullRequestPrimaryResult = 'pr_created' | 'pr_reused' | 'no_op';
57
79
  type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean; primaryResult: PullRequestPrimaryResult };
58
- type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
59
- type SkipFactOptions = { cwd?: string; agent: string; dryRun?: boolean };
80
+ type ResolveExternalOptions = SharedOptions & { agent: string; pr?: string | number; dryRun?: boolean };
81
+ type SkipFactOptions = { cwd?: string; agent: string; dryRun?: boolean; runtimeVersion?: string };
60
82
  type ExternalPullRequestSelection =
61
83
  | { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
62
84
  | { status: 'selected'; source: 'unique' | 'explicit'; selected: PullRequestSnapshot; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] }
@@ -222,9 +244,17 @@ function repositoryKey(repository: string): string {
222
244
  return repository.trim().toLowerCase();
223
245
  }
224
246
 
247
+ function isUsableResourceIdentity(identity: ResourceIdentity): boolean {
248
+ return isResourceIdentity(identity);
249
+ }
250
+
251
+ function identityLabel(identity: ResourceIdentity): string {
252
+ return identity.kind === 'number' ? `PR #${identity.value}` : `PR ${identity.kind}:${identity.value}`;
253
+ }
254
+
225
255
  function identitySignature(pullRequest: PullRequestSnapshot): string {
226
256
  return [
227
- pullRequest.repository, pullRequest.number, pullRequest.nodeId, pullRequest.url,
257
+ pullRequest.repository, JSON.stringify(pullRequestIdentity(pullRequest)), pullRequest.nodeId, pullRequest.url,
228
258
  pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
229
259
  pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
230
260
  pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
@@ -237,34 +267,38 @@ function hasCompleteExternalIdentity(pullRequest: PullRequestSnapshot): boolean
237
267
  pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
238
268
  pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
239
269
  ];
240
- return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
241
- required.every((value) => Boolean(value?.trim())) &&
270
+ return isUsableResourceIdentity(pullRequestIdentity(pullRequest)) && required.every((value) => Boolean(value?.trim())) &&
242
271
  (pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
243
272
  }
244
273
 
245
274
  function selectExternalPullRequest(
246
275
  candidates: PullRequestSnapshot[],
247
276
  repository: string,
248
- existingPrNumber: number | null,
249
- explicitPrNumber: number | null
277
+ existingPrIdentity: ResourceIdentity | number | null,
278
+ explicitPrIdentity: ResourceIdentity | number | null
250
279
  ): ExternalPullRequestSelection {
280
+ const existingIdentity = typeof existingPrIdentity === 'number'
281
+ ? resourceIdentity(existingPrIdentity) : existingPrIdentity;
282
+ const explicitIdentity = typeof explicitPrIdentity === 'number'
283
+ ? resourceIdentity(explicitPrIdentity) : explicitPrIdentity;
251
284
  const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
252
285
  if (invalid) return {
253
286
  status: 'failed', code: 'PR_IDENTITY_INVALID',
254
287
  message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
255
288
  };
256
- const byNumber = new Map<number, string>();
289
+ const byIdentity = new Map<string, string>();
257
290
  for (const candidate of candidates) {
258
291
  const signature = identitySignature(candidate);
259
- const prior = byNumber.get(candidate.number);
292
+ const identityKey = JSON.stringify(pullRequestIdentity(candidate));
293
+ const prior = byIdentity.get(identityKey);
260
294
  if (prior && prior !== signature) return {
261
295
  status: 'failed', code: 'PR_IDENTITY_INVALID',
262
296
  message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
263
297
  };
264
- byNumber.set(candidate.number, signature);
298
+ byIdentity.set(identityKey, signature);
265
299
  }
266
300
  const unique = candidates.filter((candidate, index) =>
267
- candidates.findIndex((other) => other.number === candidate.number) === index
301
+ candidates.findIndex((other) => resourceIdentityEquals(pullRequestIdentity(other), pullRequestIdentity(candidate))) === index
268
302
  );
269
303
  const wantedRepository = repositoryKey(repository);
270
304
  const eligible = unique.filter((candidate) =>
@@ -273,19 +307,19 @@ function selectExternalPullRequest(
273
307
  candidate.state === 'closed' &&
274
308
  Boolean(candidate.mergedAt && candidate.mergeCommitSha)
275
309
  );
276
- if (explicitPrNumber !== null) {
277
- const selected = unique.find((candidate) => candidate.number === explicitPrNumber);
278
- if (!selected) return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR #${explicitPrNumber} is not a closing PR`, candidates, eligible };
279
- if (!eligible.some((candidate) => candidate.number === explicitPrNumber)) {
280
- return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR #${explicitPrNumber} is not an eligible merged PR`, candidates, eligible };
310
+ if (explicitIdentity !== null) {
311
+ const selected = unique.find((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity));
312
+ if (!selected) return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR ${identityLabel(explicitIdentity)} is not a closing PR`, candidates, eligible };
313
+ if (!eligible.some((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity))) {
314
+ return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR ${identityLabel(explicitIdentity)} is not an eligible merged PR`, candidates, eligible };
281
315
  }
282
- if (existingPrNumber !== null && existingPrNumber !== explicitPrNumber) {
283
- return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
316
+ if (existingIdentity !== null && !resourceIdentityEquals(existingIdentity, explicitIdentity)) {
317
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
284
318
  }
285
319
  return { status: 'selected', source: 'explicit', selected, candidates, eligible };
286
320
  }
287
- if (existingPrNumber !== null && eligible.length === 1 && existingPrNumber !== eligible[0]!.number) {
288
- return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${existingPrNumber}`, candidates, eligible };
321
+ if (existingIdentity !== null && eligible.length === 1 && !resourceIdentityEquals(existingIdentity, pullRequestIdentity(eligible[0]!))) {
322
+ return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
289
323
  }
290
324
  if (eligible.length === 0) return { status: 'normal', candidates, eligible: [] };
291
325
  if (eligible.length > 1) return {
@@ -364,64 +398,8 @@ function selectPullRequest(remotes: RemotePullRequest[], repository: string, hea
364
398
  return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
365
399
  }
366
400
 
367
- function repositoryHead(cwd: string): string {
368
- return execFileSync('git', ['rev-parse', 'HEAD'], {
369
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
370
- }).trim();
371
- }
372
-
373
- function remoteBranchHead(cwd: string, repository: string, head: string):
374
- | { ok: true; value: string }
375
- | { ok: false; error: { code: string; message: string; retryable: boolean } } {
376
- const wanted = expectedHead(repository, head);
377
- if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref)) return {
378
- ok: false,
379
- error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
380
- };
381
- const remotes = configuredGitRemotes(cwd);
382
- const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
383
- ?? remotes.find((item) => item.name === 'origin');
384
- if (!remote) return {
385
- ok: false,
386
- error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
387
- };
388
- try {
389
- const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
390
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
391
- }).trim();
392
- const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
393
- const sha = match?.split(/\s+/)[0] || null;
394
- if (!sha || !/^[a-f0-9]{40}$/i.test(sha)) return {
395
- ok: false,
396
- error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
397
- };
398
- return { ok: true, value: sha };
399
- } catch (error) {
400
- return {
401
- ok: false,
402
- error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
403
- };
404
- }
405
- }
406
-
407
- function verifyCreateHead(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string) {
408
- let localHead: string;
409
- try {
410
- localHead = repositoryHead(base.resolved.repoRoot);
411
- } catch (error) {
412
- return { ok: false as const, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
413
- }
414
- const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository!, head);
415
- if (!remote.ok) return remote;
416
- if (remote.value !== localHead) return {
417
- ok: false as const,
418
- error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
419
- };
420
- return { ok: true as const, value: localHead };
421
- }
422
-
423
401
  function validateBoundPullRequest(
424
- base: ReturnType<typeof resolvedContext> & { ok: true },
402
+ base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
425
403
  pullRequest: PullRequestSnapshot | null,
426
404
  head: string,
427
405
  baseRef: string,
@@ -464,7 +442,7 @@ function resolveTaskDeliveryTarget(repoRoot: string, frontmatter: Record<string,
464
442
  : { ok: false as const, error: { code: target.code, message: target.message, retryable: false } };
465
443
  }
466
444
 
467
- function taskDeliveryTarget(base: ReturnType<typeof resolvedContext> & { ok: true }) {
445
+ function taskDeliveryTarget(base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true }) {
468
446
  return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
469
447
  }
470
448
 
@@ -496,7 +474,7 @@ function validateIdentityAgainstTarget(
496
474
  }
497
475
 
498
476
  function validateWriterIdentity(
499
- base: ReturnType<typeof resolvedContext> & { ok: true },
477
+ base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
500
478
  pullRequest: PullRequestSnapshot | null,
501
479
  options: BindingIdentityOptions = {}
502
480
  ): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
@@ -529,29 +507,103 @@ function validateExternalMergedEvidence(
529
507
  return { ok: true };
530
508
  }
531
509
 
532
- function resolvedContext(taskRef: string, options: InspectionOptions) {
510
+ async function resolvedContext(taskRef: string, options: InspectionOptions) {
533
511
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
534
512
  if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
535
513
  error: { code: resolved.code, message: resolved.message, retryable: false }
536
514
  }) };
537
515
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
538
516
  const frontmatter = parseTypedTaskFrontmatter(content);
539
- const factRead = readPrDeliveryFact(frontmatter);
517
+ const factRead = readPrDeliveryFact(frontmatter, options.runtimeVersion);
540
518
  if (factRead.status === 'invalid') return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
541
- error: { code: 'PR_DELIVERY_FACT_INVALID', message: factRead.error.message, retryable: false }
519
+ error: { code: factRead.error.code, message: factRead.error.message, retryable: false }
542
520
  }) };
543
521
  const fact = factRead.status === 'valid' ? factRead.fact : null;
544
- const issue = Number(frontmatter.issue_number);
545
- const pr = fact?.state === 'bound' ? fact.identity.number : NaN;
546
- const issueNumber = Number.isInteger(issue) && issue > 0 ? issue : null;
547
- const prNumber = Number.isInteger(pr) && pr > 0 ? pr : null;
548
- const client = (options.client as GitHubClient | undefined) || createGitHubClient();
549
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client });
522
+ let issueIdentity: ResourceIdentity | null;
523
+ try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
524
+ catch (error) { return { ok: false as const, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) }; }
525
+ const issueNumber = resourceIdentityNumber(issueIdentity);
526
+ const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
527
+ const prNumber = resourceIdentityNumber(prIdentity);
528
+ const client = options.client;
529
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client });
530
+ const context = loaded.ok ? loaded.value.context : loaded.context;
550
531
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
551
- if (!usable) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
532
+ if (!usable || !loaded.ok) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
552
533
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
553
534
  }) };
554
- return { ok: true as const, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
535
+ return { ok: true as const, resolved, content, frontmatter, fact, issueIdentity, issueNumber, prIdentity, prNumber, client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
536
+ }
537
+
538
+ function normalizeProviderPullRequest(
539
+ remote: ProviderChangeRequestSnapshot,
540
+ repository: string,
541
+ fallbackNumber: number,
542
+ fallback?: PullRequestSnapshot | null
543
+ ): PullRequestSnapshot {
544
+ const head = remote.head || fallback?.head || { repository, ref: '', sha: remote.headSha || '' };
545
+ const base = remote.base || fallback?.base || { repository, ref: '', sha: remote.baseSha || '' };
546
+ const identity = remote.identity || fallback?.identity || (remote.number ? { kind: 'number' as const, value: remote.number } : { kind: 'id' as const, value: remote.id });
547
+ const snapshot: PullRequestSnapshot = {
548
+ repository,
549
+ number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber,
550
+ nodeId: remote.id,
551
+ url: remote.displayUrl || fallback?.url || '',
552
+ state: remote.state === 'closed' ? 'closed' : 'open',
553
+ title: remote.title,
554
+ body: remote.body,
555
+ draft: remote.draft ?? fallback?.draft ?? false,
556
+ head: { ...head, sha: remote.headSha || head.sha },
557
+ base: { ...base, sha: remote.baseSha || base.sha },
558
+ mergedAt: remote.mergedAt ?? fallback?.mergedAt ?? null,
559
+ mergeCommitSha: remote.mergeCommitSha ?? fallback?.mergeCommitSha ?? null,
560
+ labels: [...(remote.labels || fallback?.labels || [])].sort(),
561
+ assignees: [...(remote.assignees || fallback?.assignees || [])].sort(),
562
+ milestone: remote.milestone ?? fallback?.milestone ?? null,
563
+ mergeability: remote.mergeability || fallback?.mergeability || { state: 'unknown', detail: null }
564
+ };
565
+ Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
566
+ return snapshot;
567
+ }
568
+
569
+ function pullRequestIdentity(pullRequest: PullRequestSnapshot): ResourceIdentity {
570
+ return pullRequest.identity || { kind: 'number', value: pullRequest.number };
571
+ }
572
+
573
+ function providerPullRequestError(
574
+ base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
575
+ error: { code: string; message: string; retryable: boolean },
576
+ prNumber: number | null
577
+ ): PullRequestResult {
578
+ return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, base.issueNumber, prNumber, {
579
+ changed: error.code.startsWith('IN_LABEL_SYNC'),
580
+ platform: base.context.platform,
581
+ capabilities: base.context.capabilities,
582
+ resource: { kind: 'pull-request', number: prNumber },
583
+ error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
584
+ });
585
+ }
586
+
587
+ async function inspectExternalPullRequest(
588
+ base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
589
+ prIdentity: ResourceIdentity
590
+ ): Promise<PullRequestResult> {
591
+ const prNumber = resourceIdentityNumber(prIdentity);
592
+ const fallback = base.fact?.state === 'bound' && resourceIdentityEquals(base.fact.identity.resource, prIdentity)
593
+ ? base.fact.identity as unknown as PullRequestSnapshot
594
+ : null;
595
+ const inspected = base.provider.changeRequests?.inspect
596
+ ? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: prIdentity })
597
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
598
+ if (!inspected.ok) return providerPullRequestError(base, inspected.error, prNumber);
599
+ const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository!, prNumber || 0, fallback);
600
+ return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
601
+ platform: base.context.platform,
602
+ capabilities: base.context.capabilities,
603
+ resource: { kind: 'pull-request', number: prNumber, identity: prIdentity },
604
+ pullRequest,
605
+ error: null
606
+ });
555
607
  }
556
608
 
557
609
  function inspectGitHubPullRequest(client: GitHubClient, repository: string, number: number, cwd: string) {
@@ -563,203 +615,301 @@ function inspectGitHubPullRequest(client: GitHubClient, repository: string, numb
563
615
  : { ok: false as const, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
564
616
  }
565
617
 
566
- function configuredGitRemotes(cwd: string): Array<{ name: string; url: string; repository: string }> {
567
- try {
568
- return execFileSync('git', ['remote'], {
569
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
570
- }).split(/\r?\n/).filter(Boolean).flatMap((name) => {
571
- try {
572
- const url = execFileSync('git', ['config', '--get', `remote.${name}.url`], {
573
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
574
- }).trim();
575
- const repository = parseGitHubRemote(url);
576
- return repository ? [{ name, url, repository }] : [];
577
- } catch {
578
- return [];
579
- }
580
- });
581
- } catch {
582
- return [];
583
- }
618
+ async function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions = {}): Promise<PullRequestResult> {
619
+ const base = await resolvedContext(taskRef, options);
620
+ if (!base.ok) return base.output;
621
+ if (!base.fact) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
622
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
623
+ });
624
+ if (!base.prIdentity) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
625
+ platform: base.context.platform, capabilities: base.context.capabilities,
626
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
627
+ });
628
+ return inspectExternalPullRequest(base, base.prIdentity);
584
629
  }
585
630
 
586
- function rewriteGitHubRemote(remote: string, repository: string): string | null {
587
- const match = remote.trim().match(
588
- /^(https?:\/\/github\.com\/|ssh:\/\/(?:git@)?github\.com\/|git@github\.com:)[^/\s]+\/[^/\s]+(?:\.git)?$/i
589
- );
590
- return match ? `${match[1]}${repository}.git` : null;
631
+ async function inspectPlatformPullRequestByNumber(prNumber: string | number, options: InspectionOptions = {}): Promise<PullRequestResult> {
632
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
633
+ const context = loaded.ok ? loaded.value.context : loaded.context;
634
+ const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
635
+ if (!usable) {
636
+ return result(context.status, null, null, null, {
637
+ platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
638
+ });
639
+ }
640
+ let identity: ResourceIdentity;
641
+ try { identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)) : resourceIdentity(prNumber); }
642
+ catch (error) {
643
+ return result('failed', null, null, null, {
644
+ platform: context.platform, capabilities: context.capabilities,
645
+ error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
646
+ });
647
+ }
648
+ if (loaded.ok) {
649
+ const inspected = loaded.value.provider.changeRequests?.inspect
650
+ ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: identity })
651
+ : unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
652
+ const number = resourceIdentityNumber(identity);
653
+ if (!inspected.ok) return result(providerStatus(inspected.error), null, null, number, {
654
+ platform: context.platform, capabilities: context.capabilities,
655
+ resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
656
+ });
657
+ return result('no-op', null, null, number, {
658
+ platform: context.platform, capabilities: context.capabilities,
659
+ resource: { kind: 'pull-request', number, identity },
660
+ pullRequest: normalizeProviderPullRequest(inspected.value, context.platform.repository!, number || 0), error: null
661
+ });
662
+ }
663
+ return result('failed', null, null, null, {
664
+ platform: context.platform, capabilities: context.capabilities,
665
+ resource: { kind: 'pull-request', number: null },
666
+ error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
667
+ });
591
668
  }
592
669
 
593
- function resolveGitHubChangeRequestGitEvidence({
594
- cwd,
595
- repository,
596
- pullRequest
597
- }: {
598
- cwd: string;
599
- repository: string;
600
- pullRequest: PullRequestSnapshot;
601
- }) {
602
- if (
603
- pullRequest.repository !== repository ||
604
- pullRequest.base.repository !== repository ||
605
- pullRequest.number <= 0
606
- ) {
607
- return {
608
- ok: false as const,
609
- error: {
610
- code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
611
- message: 'Pull request repository identity is inconsistent',
612
- retryable: false
613
- }
670
+ function readInLabelMapping(repoRoot: string): { ok: true; value: Record<string, unknown> } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
671
+ try {
672
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as {
673
+ labels?: { in?: unknown };
614
674
  };
615
- }
616
- const reviewedHeadRef = `refs/pull/${pullRequest.number}/head`;
617
- const targetHeadRef = `refs/heads/${pullRequest.base.ref}`;
618
- const refValid = (ref: string) => {
619
- try {
620
- execFileSync('git', ['check-ref-format', ref], {
621
- cwd, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore']
622
- });
623
- return true;
624
- } catch {
625
- return false;
626
- }
627
- };
628
- if (!refValid(reviewedHeadRef) || !refValid(targetHeadRef)) {
675
+ const mapping = validateInLabelMapping(config.labels?.in);
676
+ return mapping.ok ? { ok: true, value: mapping.value } : mapping;
677
+ } catch (error) {
629
678
  return {
630
- ok: false as const,
631
- error: {
632
- code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
633
- message: 'Pull request Git refs are invalid',
634
- retryable: false
635
- }
679
+ ok: false,
680
+ error: { code: 'IN_LABEL_SYNC_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
636
681
  };
637
682
  }
683
+ }
638
684
 
639
- const remotes = configuredGitRemotes(cwd);
640
- const exact = remotes
641
- .filter((remote) => remote.repository === repository)
642
- .sort((left, right) => Number(right.name === 'origin') - Number(left.name === 'origin') ||
643
- left.name.localeCompare(right.name))[0];
644
- const origin = remotes.find((remote) => remote.name === 'origin');
645
- const remoteUrl = exact?.url || (origin ? rewriteGitHubRemote(origin.url, repository) : null);
646
- if (!remoteUrl) {
685
+ function taskInLabelTarget(
686
+ repoRoot: string,
687
+ frontmatter: Record<string, unknown>,
688
+ currentLabels: readonly string[],
689
+ repositoryLabels: Set<string>
690
+ ) {
691
+ const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
692
+ if (!baseRef) return {
693
+ ok: false as const,
694
+ error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
695
+ };
696
+ let changedFiles: string[];
697
+ try {
698
+ changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
699
+ cwd: repoRoot, encoding: 'utf8'
700
+ }).trim().split(/\r?\n/).filter(Boolean);
701
+ } catch (error) {
647
702
  return {
648
703
  ok: false as const,
649
- error: {
650
- code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
651
- message: `No GitHub remote can provide evidence for ${repository}`,
652
- retryable: false
653
- }
704
+ error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
654
705
  };
655
706
  }
656
- return {
657
- ok: true as const,
658
- value: { remoteUrl, reviewedHeadRef, targetHeadRef }
659
- };
707
+ const mapping = readInLabelMapping(repoRoot);
708
+ if (!mapping.ok) return mapping;
709
+ const planned = planInLabelUpdate({ changedFiles, currentLabels, mapping: mapping.value, repositoryLabels });
710
+ if (planned.error) return { ok: false as const, error: planned.error };
711
+ return { ok: true as const, value: planned.target };
660
712
  }
661
713
 
662
- registerPlatformCapabilities('github', {
663
- inspectChangeRequest({ client, repository, number, cwd }) {
664
- return inspectGitHubPullRequest((client as GitHubClient | undefined) || createGitHubClient(), repository, number, cwd);
665
- },
666
- inspectIssueClosingChangeRequests({ client, repository, issueNumber, cwd }) {
667
- return inspectGitHubIssueClosingChangeRequests(
668
- (client as GitHubClient | undefined) || createGitHubClient(), repository, issueNumber, cwd
669
- );
670
- },
671
- resolveChangeRequestGitEvidence(context) {
672
- return resolveGitHubChangeRequestGitEvidence(context);
673
- }
674
- });
675
-
676
- function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string, target: string, expectedSha?: string) {
677
- const repository = base.context.platform.repository!;
678
- const listed = base.client.json<RemotePullRequest[]>([
679
- 'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
680
- ], { cwd: base.resolved.repoRoot });
681
- if (!listed.ok) return listed;
682
- const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
683
- return selected.status === 'resolved'
684
- ? { ok: true as const, value: selected.pullRequest }
685
- : { ok: false as const, error: {
686
- code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
687
- : selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
688
- message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
689
- : selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
690
- : 'No pull request matches the exact head/base identity',
691
- retryable: false
692
- } };
714
+ function resourceIdentityLabel(identity: ResourceIdentity): string {
715
+ const number = resourceIdentityNumber(identity);
716
+ return number === null ? `${identity.kind} ${identity.value}` : `#${number}`;
693
717
  }
694
718
 
695
- function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions = {}): PullRequestResult {
696
- const base = resolvedContext(taskRef, options);
697
- if (!base.ok) return base.output;
698
- if (!base.fact) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
699
- error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
719
+ function inLabelResource(
720
+ kind: 'issue' | 'pull-request',
721
+ identity: ResourceIdentity,
722
+ before: string[],
723
+ expected: string[],
724
+ after: string[] | null,
725
+ effect: 'no-op' | 'applied' | 'unknown'
726
+ ) {
727
+ return { kind, number: resourceIdentityNumber(identity), identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
728
+ }
729
+
730
+ async function syncPlatformPullRequestInLabels(prNumber: number, options: SharedOptions & { dryRun?: boolean } = {}): Promise<PullRequestResult> {
731
+ const cwd = path.resolve(options.cwd || process.cwd());
732
+ const loaded = await resolvePlatformProviderContext({ cwd, client: options.client });
733
+ const context = loaded.ok ? loaded.value.context : loaded.context;
734
+ const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
735
+ if (!loaded.ok || !usable) return result(context.status, null, null, prNumber, {
736
+ platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
700
737
  });
701
- if (!base.prNumber) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
702
- platform: base.context.platform, capabilities: base.context.capabilities,
703
- error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
738
+ if (!Number.isSafeInteger(prNumber) || prNumber <= 0) return result('failed', null, null, prNumber, {
739
+ platform: context.platform, capabilities: context.capabilities,
740
+ error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
704
741
  });
705
- const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
706
- cwd: base.resolved.repoRoot,
707
- repository: base.context.platform.repository!,
708
- number: base.prNumber,
709
- client: options.client || base.client
742
+ const provider = loaded.value.provider;
743
+ const operationContext = providerOperationContext(loaded.value);
744
+ const repository = context.platform.repository!;
745
+ const pullRequestIdentity = providerResourceToken(provider, 'pull-request', String(prNumber));
746
+ const providerFailure = (error: { code: string; message: string; retryable: boolean }): PullRequestResult => result(
747
+ error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error),
748
+ null,
749
+ null,
750
+ prNumber,
751
+ {
752
+ platform: context.platform,
753
+ capabilities: context.capabilities,
754
+ resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity },
755
+ error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
756
+ }
757
+ );
758
+ const inspected = provider.changeRequests?.inspect
759
+ ? await provider.changeRequests.inspect({ context: operationContext, target: pullRequestIdentity })
760
+ : unsupportedProviderOperation(provider, 'changeRequests.inspect');
761
+ if (!inspected.ok) return providerFailure(inspected.error);
762
+ const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber);
763
+ const files = provider.changeRequests?.listFiles
764
+ ? await provider.changeRequests.listFiles({ context: operationContext, target: pullRequestIdentity })
765
+ : unsupportedProviderOperation(provider, 'changeRequests.listFiles');
766
+ if (!files.ok) return providerFailure(files.error);
767
+ const mapping = readInLabelMapping(cwd);
768
+ if (!mapping.ok) return result('failed', null, null, prNumber, {
769
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
770
+ resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, error: mapping.error
710
771
  });
711
- if (!fetched.ok || !fetched.value) {
712
- const error = fetched.error || {
713
- code: 'PR_INSPECTION_INVALID',
714
- message: 'Platform adapter returned no change-request snapshot',
715
- retryable: false
716
- };
717
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
718
- platform: base.context.platform, capabilities: base.context.capabilities,
719
- resource: { kind: 'pull-request', number: base.prNumber }, error
720
- });
772
+ let repositoryLabels = new Set<string>();
773
+ if (Object.keys(mapping.value).length > 0) {
774
+ const labels = provider.issues?.listLabels
775
+ ? await provider.issues.listLabels({ context: operationContext })
776
+ : unsupportedProviderOperation(provider, 'issues.listLabels');
777
+ if (!labels.ok) return providerFailure(labels.error);
778
+ repositoryLabels = new Set(labels.value);
721
779
  }
722
- return result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
723
- platform: base.context.platform, capabilities: base.context.capabilities,
724
- resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, error: null
780
+ const plannedTarget = planInLabelUpdate({
781
+ changedFiles: files.value, currentLabels: fetched.labels,
782
+ mapping: mapping.value, repositoryLabels
725
783
  });
726
- }
784
+ if (plannedTarget.error) return result('failed', null, null, prNumber, {
785
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
786
+ resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, error: plannedTarget.error
787
+ });
788
+ const association = provider.changeRequests?.listClosingIssues
789
+ ? await provider.changeRequests.listClosingIssues({ context: operationContext, target: pullRequestIdentity })
790
+ : unsupportedProviderOperation(provider, 'changeRequests.listClosingIssues');
791
+ if (!association.ok) return providerFailure(association.error);
727
792
 
728
- function inspectPlatformPullRequestByNumber(prNumber: number, options: InspectionOptions = {}): PullRequestResult {
729
- const client = (options.client as GitHubClient | undefined) || createGitHubClient();
730
- const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
731
- const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
732
- if (!usable) {
733
- return result(context.status, null, null, null, {
734
- platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
735
- });
793
+ const issueIdentity = association.value.length === 1 ? association.value[0]! : null;
794
+ const issueNumber = issueIdentity ? resourceIdentityNumber(issueIdentity) : null;
795
+ const closingIssueIdentities = association.value;
796
+ let issue: ProviderIssueSnapshot | null = null;
797
+ if (issueIdentity) {
798
+ const fetchedIssue = provider.issues?.inspect
799
+ ? await provider.issues.inspect({ context: operationContext, target: issueIdentity })
800
+ : unsupportedProviderOperation(provider, 'issues.inspect');
801
+ if (!fetchedIssue.ok) return providerFailure(fetchedIssue.error);
802
+ issue = fetchedIssue.value;
736
803
  }
737
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
738
- return result('failed', null, null, null, {
739
- platform: context.platform, capabilities: context.capabilities,
740
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
804
+
805
+ const resources = issue
806
+ ? [
807
+ { kind: 'issue' as const, identity: issueIdentity!, number: issueNumber, snapshot: issue },
808
+ { kind: 'pull-request' as const, identity: pullRequestIdentity, number: prNumber, snapshot: fetched }
809
+ ]
810
+ : [{ kind: 'pull-request' as const, identity: pullRequestIdentity, number: prNumber, snapshot: fetched }];
811
+ const plans = resources.map((resource) => ({
812
+ ...resource,
813
+ plan: planInLabelUpdate({
814
+ changedFiles: files.value, currentLabels: resource.snapshot.labels,
815
+ mapping: mapping.value, repositoryLabels
816
+ })
817
+ }));
818
+ const operations: PlatformOperation[] = [
819
+ { name: 'closing-issues', status: 'no-op', reasonCode: association.value.length === 1 ? null : association.value.length === 0 ? 'ISSUE_ASSOCIATION_NONE' : 'ISSUE_ASSOCIATION_AMBIGUOUS' },
820
+ ...plans.map((item) => ({
821
+ name: `labels:in:${item.kind}`,
822
+ status: !context.capabilities.triage ? 'skipped' as const : item.plan.changed ? 'planned' as const : 'no-op' as const,
823
+ reasonCode: !context.capabilities.triage ? 'TRIAGE_REQUIRED' : null
824
+ }))
825
+ ];
826
+ const willWrite = context.capabilities.triage && !options.dryRun;
827
+ const resourcesState = plans.map((item) => inLabelResource(
828
+ item.kind,
829
+ item.identity,
830
+ item.snapshot.labels,
831
+ item.plan.target,
832
+ item.plan.changed && willWrite ? null : item.snapshot.labels,
833
+ item.plan.changed && willWrite ? 'unknown' : 'no-op'
834
+ ));
835
+ if (options.dryRun || !context.capabilities.triage) return result(
836
+ !context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op',
837
+ null, issueNumber, prNumber, {
838
+ platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
839
+ resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
840
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities }, error: null
841
+ }
842
+ );
843
+
844
+ let currentPullRequest = fetched;
845
+ let successfulWrites = 0;
846
+ for (const item of plans) {
847
+ if (!item.plan.changed) continue;
848
+ const targetIdentity = item.kind === 'issue' ? issueIdentity! : pullRequestIdentity;
849
+ const synced = item.kind === 'issue'
850
+ ? provider.issues?.update
851
+ ? await provider.issues.update({
852
+ context: operationContext,
853
+ target: targetIdentity,
854
+ currentLabels: item.snapshot.labels,
855
+ patch: { labels: item.plan.labels },
856
+ mutation: { idempotencyKey: `issue:update:${serializeResourceIdentity(targetIdentity)}:in-labels`, target: targetIdentity }
857
+ })
858
+ : unsupportedProviderOperation(provider, 'issues.update')
859
+ : provider.changeRequests?.update
860
+ ? await provider.changeRequests.update({
861
+ context: operationContext,
862
+ target: targetIdentity,
863
+ currentLabels: item.snapshot.labels,
864
+ patch: { labels: item.plan.labels },
865
+ mutation: { idempotencyKey: `pull-request:update:${serializeResourceIdentity(targetIdentity)}:in-labels`, target: targetIdentity }
866
+ })
867
+ : unsupportedProviderOperation(provider, 'changeRequests.update');
868
+ if (!synced.ok) {
869
+ const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity))!;
870
+ state.effect = synced.error.retryable ? 'unknown' : 'no-op';
871
+ state.after = state.effect === 'unknown' ? null : item.snapshot.labels;
872
+ const partial = successfulWrites > 0 || synced.error.retryable;
873
+ const error = partial
874
+ ? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
875
+ : synced.error;
876
+ return result(partial ? 'blocked' : providerStatus(synced.error), null, issueNumber, prNumber, {
877
+ changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
878
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
879
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities }, error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
880
+ });
881
+ }
882
+ successfulWrites += synced.value.changed ? 1 : 0;
883
+ const reread = item.kind === 'issue'
884
+ ? provider.issues?.inspect
885
+ ? await provider.issues.inspect({ context: operationContext, target: targetIdentity })
886
+ : unsupportedProviderOperation(provider, 'issues.inspect')
887
+ : provider.changeRequests?.inspect
888
+ ? await provider.changeRequests.inspect({ context: operationContext, target: targetIdentity })
889
+ : unsupportedProviderOperation(provider, 'changeRequests.inspect');
890
+ if (!reread.ok) return result('blocked', null, issueNumber, prNumber, {
891
+ changed: true, platform: context.platform, capabilities: context.capabilities,
892
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
893
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
894
+ error: { ...providerError(reread.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
741
895
  });
742
- }
743
- const fetched = inspectPlatformChangeRequest(context.platform.type, {
744
- cwd: options.cwd || process.cwd(),
745
- repository: context.platform.repository!,
746
- number: prNumber,
747
- client
748
- });
749
- if (!fetched.ok || !fetched.value) {
750
- const error = fetched.error || {
751
- code: 'PR_INSPECTION_INVALID',
752
- message: 'Platform adapter returned no change-request snapshot',
753
- retryable: false
754
- };
755
- return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
756
- platform: context.platform, capabilities: context.capabilities,
757
- resource: { kind: 'pull-request', number: prNumber }, error
896
+ const after = reread.value.labels || [];
897
+ const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity))!;
898
+ state.after = after;
899
+ state.effect = after.filter((label: string) => label.startsWith('in:')).sort().join('\0') === item.plan.target.join('\0') ? 'applied' : 'unknown';
900
+ if (state.effect === 'unknown') return result('blocked', null, issueNumber, prNumber, {
901
+ changed: true, platform: context.platform, capabilities: context.capabilities,
902
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
903
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
904
+ error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} ${resourceIdentityLabel(item.identity)} did not converge to the expected in: labels`, retryable: true }
758
905
  });
906
+ if (item.kind === 'pull-request') currentPullRequest = normalizeProviderPullRequest(reread.value as ProviderChangeRequestSnapshot, repository, prNumber, currentPullRequest);
759
907
  }
760
- return result('no-op', null, null, prNumber, {
761
- platform: context.platform, capabilities: context.capabilities,
762
- resource: { kind: 'pull-request', number: prNumber }, pullRequest: fetched.value, error: null
908
+ return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
909
+ changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
910
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity },
911
+ operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' as const } : operation),
912
+ resources: resourcesState, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities }, error: null
763
913
  });
764
914
  }
765
915
 
@@ -770,9 +920,8 @@ function appendActivity(content: string, line: string): string {
770
920
 
771
921
  function deliveryIdentity(pullRequest: PullRequestSnapshot) {
772
922
  return {
923
+ resource: pullRequestIdentity(pullRequest),
773
924
  repository: pullRequest.repository,
774
- number: pullRequest.number,
775
- nodeId: pullRequest.nodeId,
776
925
  url: pullRequest.url,
777
926
  head: { ...pullRequest.head },
778
927
  base: { ...pullRequest.base }
@@ -786,13 +935,13 @@ function factTimestamp(timestamp: string): string {
786
935
  function boundFactFor(
787
936
  pullRequest: PullRequestSnapshot,
788
937
  source: PrDeliveryBindingSource,
789
- issueNumber: number | null,
938
+ issue: number | ResourceIdentity | null,
790
939
  verifiedAt: string
791
940
  ): PrDeliveryFact {
792
941
  return buildBoundFact({
793
942
  identity: deliveryIdentity(pullRequest),
794
943
  source,
795
- issueNumber,
944
+ ...(typeof issue === 'object' ? { issueIdentity: issue } : { issueNumber: issue }),
796
945
  verifiedAt: factTimestamp(verifiedAt),
797
946
  remoteState: pullRequest.state,
798
947
  mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
@@ -800,7 +949,7 @@ function boundFactFor(
800
949
  });
801
950
  }
802
951
 
803
- function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: true }, agent: string) {
952
+ function writeCreateStarted(base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true }, agent: string) {
804
953
  const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
805
954
  const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
806
955
  if (starts > dones) return { status: 'no-op' as const };
@@ -815,7 +964,7 @@ function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: tru
815
964
  }
816
965
 
817
966
  function bindIdentity(
818
- base: ReturnType<typeof resolvedContext> & { ok: true },
967
+ base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
819
968
  pullRequest: PullRequestSnapshot,
820
969
  agent: string,
821
970
  source: PrDeliveryBindingSource,
@@ -829,10 +978,10 @@ function bindIdentity(
829
978
  errorCode: 'PR_BIND_IDENTITY_MISMATCH'
830
979
  });
831
980
  if (!identity.ok) return { status: 'failed' as const, error: identity.error };
832
- if (base.prNumber && base.prNumber !== pullRequest.number) return { status: 'failed' as const, error: {
833
- code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`
981
+ if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest))) return { status: 'failed' as const, error: {
982
+ code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity'
834
983
  } };
835
- if (base.prNumber === pullRequest.number && base.fact?.state === 'bound') {
984
+ if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)) && base.fact?.state === 'bound') {
836
985
  if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest))) return { status: 'failed' as const, error: {
837
986
  code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
838
987
  } };
@@ -840,7 +989,7 @@ function bindIdentity(
840
989
  }
841
990
  const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
842
991
  const metadata = captureTaskWriteMetadata();
843
- const fact = boundFactFor(pullRequest, source, base.issueNumber, metadata.timestamp);
992
+ const fact = boundFactFor(pullRequest, source, taskIssueIdentity(base.frontmatter), metadata.timestamp);
844
993
  const factMutation = factFrontmatterMutation(fact);
845
994
  return writeTask({
846
995
  taskRef: base.resolved.taskId,
@@ -855,64 +1004,59 @@ function bindIdentity(
855
1004
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
856
1005
  }
857
1006
 
858
- function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullRequestResult {
859
- const base = resolvedContext(taskRef, options);
1007
+ async function bindPlatformPullRequest(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
1008
+ const base = await resolvedContext(taskRef, options);
860
1009
  if (!base.ok) return base.output;
861
1010
  if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
862
1011
  error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
863
1012
  });
864
- if (!Number.isInteger(options.pr) || options.pr <= 0) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
865
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
866
- });
867
- if (base.prNumber && base.prNumber !== options.pr) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
868
- error: { code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`, retryable: false }
1013
+ let prIdentity: ResourceIdentity;
1014
+ try { prIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr)); }
1015
+ catch (error) { 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 } }); }
1016
+ const requestedNumber = resourceIdentityNumber(prIdentity);
1017
+ if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, prIdentity)) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1018
+ error: { code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity', retryable: false }
869
1019
  });
870
- const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, options.pr, base.resolved.repoRoot);
871
- if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
872
- const initialIdentity = validateWriterIdentity(base, fetched.value, {
1020
+ const inspected = await inspectExternalPullRequest(base, prIdentity);
1021
+ if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest) return inspected;
1022
+ const initialIdentity = validateWriterIdentity(base, inspected.pullRequest, {
873
1023
  expectedHead: String(base.frontmatter.branch || ''),
874
1024
  errorCode: 'PR_BIND_IDENTITY_MISMATCH'
875
1025
  });
876
- if (!initialIdentity.ok) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
877
- pullRequest: fetched.value, error: initialIdentity.error
878
- });
879
- const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository!, options.pr, base.resolved.repoRoot);
880
- if (!rechecked.ok || !rechecked.value) return result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
881
- pullRequest: fetched.value,
882
- error: rechecked.ok ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false } : rechecked.error
883
- });
884
- if (!sameDeliveryIdentity(fetched.value, rechecked.value)) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
885
- pullRequest: rechecked.value,
886
- error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
1026
+ if (!initialIdentity.ok) return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
1027
+ pullRequest: inspected.pullRequest, error: initialIdentity.error
887
1028
  });
888
- const written = bindIdentity(base, rechecked.value, options.agent, 'explicit-bind', options.dryRun);
889
- if (written.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
890
- pullRequest: fetched.value, error: { code: written.error.code, message: written.error.message, retryable: false }
1029
+ const written = bindIdentity(base, inspected.pullRequest, options.agent, 'explicit-bind', options.dryRun);
1030
+ if (written.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
1031
+ pullRequest: inspected.pullRequest, error: { code: written.error.code, message: written.error.message, retryable: false }
891
1032
  });
892
- return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : rechecked.value.number, {
1033
+ return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : inspected.pullRequest.number, {
893
1034
  changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
894
- resource: { kind: 'pull-request', number: rechecked.value.number }, pullRequest: rechecked.value,
1035
+ resource: { kind: 'pull-request', number: inspected.pullRequest.number, identity: pullRequestIdentity(inspected.pullRequest) }, pullRequest: inspected.pullRequest,
895
1036
  operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
896
1037
  });
897
1038
  }
898
1039
 
899
1040
  function externalEvidenceNote(
900
- issueNumber: number,
1041
+ issueIdentity: ResourceIdentity | null,
901
1042
  source: 'unique' | 'explicit',
902
1043
  pullRequest: PullRequestSnapshot
903
1044
  ): string {
904
1045
  return [
905
1046
  `authorization=${source}`,
906
- externalIdentityEvidenceNote(issueNumber, pullRequest)
1047
+ externalIdentityEvidenceNote(issueIdentity, pullRequest)
907
1048
  ].join('; ');
908
1049
  }
909
1050
 
910
1051
  function externalIdentityEvidenceNote(
911
- issueNumber: number,
1052
+ issueIdentity: ResourceIdentity | null,
912
1053
  pullRequest: PullRequestSnapshot
913
1054
  ): string {
1055
+ const issueLabel = issueIdentity
1056
+ ? issueIdentity.kind === 'number' ? `#${issueIdentity.value}` : `${issueIdentity.kind}:${issueIdentity.value}`
1057
+ : 'unknown';
914
1058
  return [
915
- `issue=#${issueNumber}`,
1059
+ `issue=${issueLabel}`,
916
1060
  `pr=#${pullRequest.number}`,
917
1061
  `url=${pullRequest.url}`,
918
1062
  `base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
@@ -922,8 +1066,8 @@ function externalIdentityEvidenceNote(
922
1066
  ].join('; ');
923
1067
  }
924
1068
 
925
- function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): ExternalPullRequestResult {
926
- const base = resolvedContext(taskRef, options);
1069
+ async function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): Promise<ExternalPullRequestResult> {
1070
+ const base = await resolvedContext(taskRef, options);
927
1071
  if (!base.ok) return externalResult(base.output);
928
1072
  if (!base.fact) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
929
1073
  error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
@@ -947,28 +1091,41 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
947
1091
  error: null
948
1092
  }), { mode: 'normal' });
949
1093
  }
950
- if (!base.issueNumber) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
1094
+ if (!base.issueIdentity) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
951
1095
  platform: base.context.platform,
952
1096
  capabilities: base.context.capabilities,
953
1097
  error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
954
1098
  }));
955
- const inspected = inspectPlatformIssueClosingChangeRequests(base.context.platform.type, {
956
- cwd: base.resolved.repoRoot,
957
- repository: base.context.platform.repository!,
958
- issueNumber: base.issueNumber,
959
- client: base.client
960
- });
1099
+ const inspected = base.provider.changeRequests?.listClosing
1100
+ ? await base.provider.changeRequests.listClosing({
1101
+ context: providerOperationContext(base.loadedContext),
1102
+ issue: base.issueIdentity
1103
+ })
1104
+ : unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
961
1105
  if (!inspected.ok || !inspected.value) {
962
- const error = inspected.error || { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
1106
+ const error = 'error' in inspected && inspected.error
1107
+ ? inspected.error
1108
+ : { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
963
1109
  return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
964
1110
  platform: base.context.platform, capabilities: base.context.capabilities, error
965
1111
  }));
966
1112
  }
1113
+ const candidates = (inspected.value as ProviderChangeRequestSnapshot[]).map((item) => normalizeProviderPullRequest(item, base.context.platform.repository!, item.number || 0));
1114
+ let explicitPrIdentity: ResourceIdentity | null = null;
1115
+ if (options.pr !== undefined) {
1116
+ try { explicitPrIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr)); }
1117
+ catch (error) {
1118
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1119
+ platform: base.context.platform, capabilities: base.context.capabilities,
1120
+ error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1121
+ }));
1122
+ }
1123
+ }
967
1124
  const selected = selectExternalPullRequest(
968
- inspected.value,
1125
+ candidates,
969
1126
  base.context.platform.repository!,
970
- base.prNumber,
971
- options.pr ?? null
1127
+ base.prIdentity,
1128
+ explicitPrIdentity
972
1129
  );
973
1130
  if (selected.status === 'normal') return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
974
1131
  platform: base.context.platform,
@@ -987,7 +1144,14 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
987
1144
  platform: base.context.platform, capabilities: base.context.capabilities,
988
1145
  pullRequest: selected.selected, error: initialIdentity.error
989
1146
  }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
990
- const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository!, selected.selected.number, base.resolved.repoRoot);
1147
+ const rechecked = base.provider.changeRequests?.inspect
1148
+ ? await base.provider.changeRequests.inspect({
1149
+ context: providerOperationContext(base.loadedContext),
1150
+ target: pullRequestIdentity(selected.selected)
1151
+ }).then((response) => response.ok
1152
+ ? { ok: true as const, value: normalizeProviderPullRequest(response.value, base.context.platform.repository!, selected.selected.number, selected.selected) }
1153
+ : response)
1154
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
991
1155
  if (!rechecked.ok || !rechecked.value) return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
992
1156
  platform: base.context.platform, capabilities: base.context.capabilities,
993
1157
  pullRequest: selected.selected,
@@ -1009,8 +1173,8 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
1009
1173
  pullRequest: rechecked.value, error: finalEvidence.error
1010
1174
  }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1011
1175
  const selectedPullRequest = rechecked.value;
1012
- const note = externalEvidenceNote(base.issueNumber, selected.source, selectedPullRequest);
1013
- const identityNote = externalIdentityEvidenceNote(base.issueNumber, selectedPullRequest);
1176
+ const note = externalEvidenceNote(base.issueIdentity, selected.source, selectedPullRequest);
1177
+ const identityNote = externalIdentityEvidenceNote(base.issueIdentity, selectedPullRequest);
1014
1178
  const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
1015
1179
  const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
1016
1180
  const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
@@ -1020,13 +1184,13 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
1020
1184
  pullRequest: selectedPullRequest,
1021
1185
  error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
1022
1186
  }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1023
- if (alreadyAudited && base.prNumber !== selectedPullRequest.number) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1187
+ if (alreadyAudited && base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest))) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1024
1188
  platform: base.context.platform,
1025
1189
  capabilities: base.context.capabilities,
1026
1190
  pullRequest: selectedPullRequest,
1027
1191
  error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
1028
1192
  }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1029
- if (base.prNumber === selectedPullRequest.number && alreadyAudited) {
1193
+ if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)) && alreadyAudited) {
1030
1194
  return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1031
1195
  platform: base.context.platform,
1032
1196
  capabilities: base.context.capabilities,
@@ -1066,167 +1230,20 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
1066
1230
  }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1067
1231
  }
1068
1232
 
1069
- function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptions): PullRequestResult {
1070
- const base = resolvedContext(taskRef, options);
1071
- if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
1072
- if (!base.fact) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1073
- error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1074
- }), PRECONDITION_NOT_CREATED);
1075
- if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1076
- error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
1077
- }), PRECONDITION_NOT_CREATED);
1078
- const target = taskDeliveryTarget(base);
1079
- if (!target.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1080
- error: target.error
1081
- }), PRECONDITION_NOT_CREATED);
1082
- if (options.base !== target.value.baseRef) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1083
- platform: base.context.platform, capabilities: base.context.capabilities,
1084
- error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
1085
- }), PRECONDITION_NOT_CREATED);
1086
- if (base.prNumber) {
1087
- const headCheck = verifyCreateHead(base, options.head);
1088
- if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1089
- platform: base.context.platform,
1090
- capabilities: base.context.capabilities,
1091
- error: headCheck.error
1092
- }), PRECONDITION_NOT_CREATED);
1093
- const inspected = inspectPlatformPullRequest(taskRef, options);
1094
- const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
1095
- if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1096
- platform: base.context.platform,
1097
- capabilities: base.context.capabilities,
1098
- pullRequest: inspected.pullRequest,
1099
- error: identity.error
1100
- }), PRECONDITION_NOT_CREATED);
1101
- return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
1102
- }
1103
- const headCheck = verifyCreateHead(base, options.head);
1104
- if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
1105
- platform: base.context.platform,
1106
- capabilities: base.context.capabilities,
1107
- error: headCheck.error
1108
- }), PRECONDITION_NOT_CREATED);
1109
- const expectedHeadSha = headCheck.value;
1110
- const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
1111
- if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1112
- platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
1113
- }), PRECONDITION_NOT_CREATED);
1114
- if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1115
- platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
1116
- }), PRECONDITION_NOT_CREATED);
1117
- if (!located.ok && located.error.code !== 'PR_NOT_FOUND') return withCreation(result(located.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
1118
- platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
1119
- }), PRECONDITION_NOT_CREATED);
1120
- if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
1121
- platform: base.context.platform, capabilities: base.context.capabilities,
1122
- operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
1123
- result: located.ok ? 'pr_reused' : 'pr_created', error: null
1124
- }), { kind: 'planned', action: located.ok ? 'reuse' : 'create', createdByCurrentOperation: false });
1125
- const started = writeCreateStarted(base, options.agent);
1126
- if (started.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1127
- error: { code: started.error.code, message: started.error.message, retryable: false }
1128
- }), PRECONDITION_NOT_CREATED);
1129
- let pullRequest = located.ok ? located.value : null;
1130
- let created = false;
1131
- if (!pullRequest) {
1132
- const repository = base.context.platform.repository!;
1133
- if (options.phase) options.phase.value = 'post-dispatched';
1134
- const response = base.client.json<RemotePullRequest>(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
1135
- cwd: base.resolved.repoRoot,
1136
- method: 'POST',
1137
- input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
1138
- });
1139
- if (response.ok) {
1140
- // The remote POST is accepted before any normalization, recheck, or task write.
1141
- // From this point on the current call has created a remote resource even if
1142
- // local post-processing later fails.
1143
- created = true;
1144
- if (options.phase) options.phase.value = 'post-accepted';
1145
- pullRequest = normalizePullRequest(response.value, repository);
1146
- } else if (response.error.retryable) {
1147
- return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
1148
- platform: base.context.platform, capabilities: base.context.capabilities,
1149
- operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
1150
- error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
1151
- }), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
1152
- } else return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1153
- platform: base.context.platform, capabilities: base.context.capabilities,
1154
- operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
1155
- }), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
1156
- if (!pullRequest) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1157
- error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
1158
- }), { kind: 'created', createdByCurrentOperation: true });
1159
- if (pullRequest.head.sha !== expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1160
- platform: base.context.platform, capabilities: base.context.capabilities,
1161
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
1162
- error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
1163
- }), { kind: 'created', createdByCurrentOperation: true });
1164
- }
1165
- const bindHeadCheck = verifyCreateHead(base, options.head);
1166
- if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
1167
- const error = bindHeadCheck.ok
1168
- ? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
1169
- : bindHeadCheck.error;
1170
- return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1171
- platform: base.context.platform, capabilities: base.context.capabilities,
1172
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
1173
- }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
1174
- }
1175
- const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository!, pullRequest.number, base.resolved.repoRoot);
1176
- if (!beforeBind.ok || !beforeBind.value) {
1177
- const error = beforeBind.ok
1178
- ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
1179
- : beforeBind.error;
1180
- return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1181
- platform: base.context.platform, capabilities: base.context.capabilities,
1182
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
1183
- }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
1184
- }
1185
- if (
1186
- beforeBind.value.head.sha !== expectedHeadSha
1187
- || beforeBind.value.head.ref !== expectedHead(base.context.platform.repository!, options.head).ref
1188
- || beforeBind.value.base.ref !== options.base
1189
- || beforeBind.value.base.repository !== base.context.platform.repository
1190
- ) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1191
- platform: base.context.platform, capabilities: base.context.capabilities,
1192
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
1193
- error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
1194
- }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
1195
- pullRequest = beforeBind.value;
1196
- const refreshed = resolvedContext(taskRef, options);
1197
- if (!refreshed.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1198
- pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
1199
- }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
1200
- const bound = bindIdentity(refreshed, pullRequest, options.agent, created ? 'created' : 'reused', false, options.head, expectedHeadSha);
1201
- if (bound.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1202
- platform: base.context.platform, capabilities: base.context.capabilities,
1203
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
1204
- operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
1205
- error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
1206
- }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
1207
- return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1208
- changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
1209
- resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
1210
- operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
1211
- result: created ? 'pr_created' : 'pr_reused', error: null
1212
- }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
1213
- }
1214
-
1215
- function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
1216
- const base = resolvedContext(taskRef, options);
1233
+ async function createPlatformPullRequest(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
1234
+ const base = await resolvedContext(taskRef, options);
1217
1235
  if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
1218
1236
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
1219
1237
  return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1220
1238
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
1221
1239
  }), PRECONDITION_NOT_CREATED);
1222
1240
  }
1223
- const phase = { value: 'before-post' as CreatePhase };
1224
1241
  try {
1225
1242
  return withTaskExecutionLock(
1226
1243
  base.resolved.repoRoot,
1227
1244
  base.resolved.taskId,
1228
1245
  'platform-pr.create',
1229
- () => createPlatformPullRequestUnlocked(taskRef, { ...options, phase })
1246
+ () => createExternalPullRequest(taskRef, options, base)
1230
1247
  );
1231
1248
  } catch (error) {
1232
1249
  if (error instanceof TaskExecutionLockError) {
@@ -1236,27 +1253,154 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
1236
1253
  error: { code: error.code, message: error.message, retryable: true }
1237
1254
  }), PRECONDITION_NOT_CREATED);
1238
1255
  }
1239
- const creation: CreationOutcome = phase.value === 'post-accepted'
1240
- ? { kind: 'created', createdByCurrentOperation: true }
1241
- : phase.value === 'post-dispatched'
1242
- ? { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' }
1243
- : PRECONDITION_NOT_CREATED;
1244
- return withCreation(result(phase.value === 'post-dispatched' ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
1256
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1245
1257
  platform: base.context.platform,
1246
1258
  capabilities: base.context.capabilities,
1247
1259
  error: {
1248
- code: phase.value === 'post-dispatched' ? 'PR_CREATE_OUTCOME_UNKNOWN' : 'PR_CREATE_FAILED',
1260
+ code: 'PR_CREATE_FAILED',
1249
1261
  message: error instanceof Error ? error.message : String(error),
1250
- retryable: phase.value === 'post-dispatched'
1262
+ retryable: false
1251
1263
  }
1252
- }), creation);
1264
+ }), PRECONDITION_NOT_CREATED);
1253
1265
  }
1254
1266
  }
1255
1267
 
1256
- function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullRequestResult {
1268
+ async function createExternalPullRequest(
1269
+ taskRef: string,
1270
+ options: CreateOptions,
1271
+ base: Extract<Awaited<ReturnType<typeof resolvedContext>>, { ok: true }>
1272
+ ): Promise<PullRequestResult> {
1273
+ if (!base.fact) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1274
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1275
+ }), PRECONDITION_NOT_CREATED);
1276
+ const target = taskDeliveryTarget(base);
1277
+ if (!target.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1278
+ error: target.error
1279
+ }), PRECONDITION_NOT_CREATED);
1280
+ if (options.base !== target.value.baseRef) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1281
+ platform: base.context.platform,
1282
+ capabilities: base.context.capabilities,
1283
+ error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
1284
+ }), PRECONDITION_NOT_CREATED);
1285
+ let verifiedHeadSha: string | undefined;
1286
+ if (base.provider.changeRequests?.verifyHead) {
1287
+ const verified = await base.provider.changeRequests.verifyHead({
1288
+ context: providerOperationContext(base.loadedContext),
1289
+ head: options.head
1290
+ });
1291
+ if (!verified.ok) return withCreation(providerPullRequestError(base, verified.error, base.prNumber), PRECONDITION_NOT_CREATED);
1292
+ verifiedHeadSha = verified.value.sha;
1293
+ }
1294
+ if (base.prIdentity) {
1295
+ const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
1296
+ return inspected.status === 'failed' || inspected.status === 'blocked'
1297
+ ? withCreation(inspected, PRECONDITION_NOT_CREATED)
1298
+ : withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
1299
+ }
1300
+ if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
1301
+ platform: base.context.platform,
1302
+ capabilities: base.context.capabilities,
1303
+ operations: [{ name: 'pr:create', status: 'planned', reasonCode: null }],
1304
+ result: 'pr_created',
1305
+ error: null
1306
+ }), { kind: 'planned', action: 'create', createdByCurrentOperation: false });
1307
+
1308
+ const started = writeCreateStarted(base, options.agent);
1309
+ if (started.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1310
+ error: { code: started.error.code, message: started.error.message, retryable: false }
1311
+ }), PRECONDITION_NOT_CREATED);
1312
+ const created = base.provider.changeRequests?.create
1313
+ ? await base.provider.changeRequests.create({
1314
+ context: providerOperationContext(base.loadedContext),
1315
+ base: options.base,
1316
+ head: options.head,
1317
+ title: options.title,
1318
+ body: options.body,
1319
+ draft: Boolean(options.draft),
1320
+ mutation: { idempotencyKey: `pull-request:create:${base.resolved.taskId}` }
1321
+ })
1322
+ : unsupportedProviderOperation(base.provider, 'changeRequests.create');
1323
+ if (!created.ok) return withCreation(providerPullRequestError(base, created.error, null), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
1324
+ const remoteId = String(created.value.remoteId || '');
1325
+ let createdIdentity: ResourceIdentity;
1326
+ try { createdIdentity = providerResourceIdentity(base.provider, 'pull-request', remoteId); }
1327
+ catch (error) {
1328
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1329
+ platform: base.context.platform, capabilities: base.context.capabilities,
1330
+ error: { code: 'PR_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1331
+ }), { kind: 'created', createdByCurrentOperation: true });
1332
+ }
1333
+ const number = resourceIdentityNumber(createdIdentity);
1334
+ const fallbackNumber = number ?? 0;
1335
+ const fallback = normalizeProviderPullRequest({
1336
+ id: remoteId,
1337
+ identity: createdIdentity,
1338
+ number: number || undefined,
1339
+ state: 'open',
1340
+ title: options.title,
1341
+ body: options.body,
1342
+ headSha: '',
1343
+ baseSha: '',
1344
+ head: { repository: base.context.platform.repository!, ref: options.head, sha: '' },
1345
+ base: { repository: base.context.platform.repository!, ref: options.base, sha: '' },
1346
+ displayUrl: `${base.provider.type}:${remoteId}`
1347
+ }, base.context.platform.repository!, fallbackNumber);
1348
+ const inspected = base.provider.changeRequests?.inspect
1349
+ ? await base.provider.changeRequests.inspect({
1350
+ context: providerOperationContext(base.loadedContext),
1351
+ target: createdIdentity
1352
+ })
1353
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1354
+ if (!inspected.ok) return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
1355
+ const pullRequest = normalizeProviderPullRequest(
1356
+ inspected.value,
1357
+ base.context.platform.repository!,
1358
+ inspected.value.number ?? fallback.number,
1359
+ fallback
1360
+ );
1361
+ if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest))) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1362
+ platform: base.context.platform,
1363
+ capabilities: base.context.capabilities,
1364
+ error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'Provider create response lacks validated identity', retryable: false }
1365
+ }), { kind: 'created', createdByCurrentOperation: true });
1366
+ const identity = validateWriterIdentity(base, pullRequest, {
1367
+ expectedHead: options.head,
1368
+ expectedHeadSha: verifiedHeadSha,
1369
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
1370
+ });
1371
+ if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1372
+ platform: base.context.platform,
1373
+ capabilities: base.context.capabilities,
1374
+ resource: { kind: 'pull-request', number: pullRequest.number },
1375
+ pullRequest,
1376
+ error: identity.error
1377
+ }), { kind: 'created', createdByCurrentOperation: true });
1378
+ const bound = bindIdentity(base, pullRequest, options.agent, 'created', false, options.head);
1379
+ if (bound.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1380
+ platform: base.context.platform,
1381
+ capabilities: base.context.capabilities,
1382
+ resource: { kind: 'pull-request', number: pullRequest.number },
1383
+ pullRequest,
1384
+ error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
1385
+ }), { kind: 'created', createdByCurrentOperation: true });
1386
+ return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1387
+ changed: true,
1388
+ platform: base.context.platform,
1389
+ capabilities: base.context.capabilities,
1390
+ resource: { kind: 'pull-request', number: pullRequest.number },
1391
+ pullRequest,
1392
+ operations: [{ name: 'pr:create', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
1393
+ result: 'pr_created',
1394
+ error: null
1395
+ }), { kind: 'created', createdByCurrentOperation: true });
1396
+ }
1397
+
1398
+ async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
1257
1399
  const warningResult = warningResultForPrimary(options.primaryResult);
1258
1400
  const softenFailure = (output: PullRequestResult): PullRequestResult => {
1401
+ const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
1259
1402
  const warning = output.error && output.task.prNumber !== null
1403
+ && !authorityError
1260
1404
  && !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
1261
1405
  ? {
1262
1406
  code: output.error.code,
@@ -1271,66 +1415,148 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
1271
1415
  ? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
1272
1416
  : output;
1273
1417
  };
1274
- const base = resolvedContext(taskRef, options);
1418
+ const base = await resolvedContext(taskRef, options);
1275
1419
  if (!base.ok) return softenFailure(base.output);
1276
1420
  if (!base.fact) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1277
1421
  error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1278
1422
  }));
1279
- if (!base.prNumber) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1423
+ if (!base.prIdentity) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1280
1424
  error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
1281
1425
  }));
1282
- const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, base.prNumber, base.resolved.repoRoot);
1283
- if (!fetched.ok) return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
1284
- if (!options.metadata && !options.closingIssue) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1285
- error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
1286
- }));
1287
- if (!base.issueNumber) return softenFailure(result('degraded', base.resolved.taskId, null, base.prNumber, {
1288
- pullRequest: fetched.value, error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no linked Issue to copy metadata from', retryable: false }
1289
- }));
1290
- const issue = inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
1291
- if (!issue.issue) return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
1292
- platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, error: issue.error
1293
- }));
1294
- const planned = planPullRequestMetadata({
1295
- pullRequest: fetched.value,
1296
- issue: issue.issue,
1297
- taskType: String(base.frontmatter.type || 'task'),
1298
- issueNumber: base.issueNumber,
1299
- capabilities: base.context.capabilities
1300
- }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
1301
- .filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
1302
- const operations: PlatformOperation[] = planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }));
1303
- if (options.dryRun) return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1304
- platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, operations, error: null
1305
- }));
1306
- const payload: Record<string, unknown> = {};
1307
- for (const operation of planned) {
1308
- if (operation.status !== 'planned') continue;
1309
- if (operation.name === 'labels') payload.labels = operation.value;
1310
- if (operation.name === 'assignees') payload.assignees = operation.value;
1311
- if (operation.name === 'closing-issue') payload.body = operation.value;
1312
- if (operation.name === 'milestone') {
1313
- const milestones = base.client.json<unknown[]>(['api', '--paginate', '--slurp', `repos/${base.context.platform.repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
1314
- if (!milestones.ok) return softenFailure(result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error }));
1315
- const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) as Array<{ title?: string; number?: number }>;
1316
- payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
1426
+ {
1427
+ const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
1428
+ if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest) return softenFailure(inspected);
1429
+ if (!options.metadata && !options.closingIssue) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1430
+ error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
1431
+ }));
1432
+ const issue = await inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
1433
+ if (!issue.issue) return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
1434
+ platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, error: issue.error
1435
+ }));
1436
+ let inLabels = inspected.pullRequest.labels.filter((label) => label.startsWith('in:')).sort();
1437
+ let issueInLabelOperation: PlatformOperation | null = null;
1438
+ if (options.metadata && base.provider.issues?.listLabels) {
1439
+ const labels = await base.provider.issues.listLabels({ context: providerOperationContext(base.loadedContext) });
1440
+ if (!labels.ok) return softenFailure(providerPullRequestError(base, labels.error, base.prNumber));
1441
+ const target = taskInLabelTarget(base.resolved.repoRoot, base.frontmatter, inspected.pullRequest.labels, new Set(labels.value));
1442
+ if (!target.ok) return softenFailure(providerPullRequestError(base, target.error, base.prNumber));
1443
+ inLabels = target.value;
1444
+ const currentIssueLabels = issue.issue.labels.filter((label) => label.startsWith('in:')).sort();
1445
+ issueInLabelOperation = base.context.capabilities.triage
1446
+ ? currentIssueLabels.join('\0') === inLabels.join('\0')
1447
+ ? { name: 'labels:in:issue', status: 'no-op', reasonCode: null }
1448
+ : { name: 'labels:in:issue', status: 'planned', reasonCode: null }
1449
+ : { name: 'labels:in:issue', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' };
1317
1450
  }
1318
- }
1319
- if (Object.keys(payload).length === 0) {
1320
- const degraded = planned.some((operation) => operation.status === 'skipped');
1321
- return softenFailure(result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1322
- platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, operations, error: null
1451
+ const planned = planPullRequestMetadata({
1452
+ pullRequest: inspected.pullRequest,
1453
+ issue: issue.issue,
1454
+ taskType: String(base.frontmatter.type || 'task'),
1455
+ issueNumber: base.issueNumber,
1456
+ capabilities: base.context.capabilities,
1457
+ inLabels
1458
+ }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
1459
+ .filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
1460
+ const operations: PlatformOperation[] = [
1461
+ ...(issueInLabelOperation ? [issueInLabelOperation] : []),
1462
+ ...planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }))
1463
+ ];
1464
+ const milestoneOperation = planned.find((operation) => operation.name === 'milestone' && operation.status === 'planned');
1465
+ if (milestoneOperation && base.provider.issues?.listMilestones) {
1466
+ const milestones = await base.provider.issues.listMilestones({ context: providerOperationContext(base.loadedContext) });
1467
+ if (!milestones.ok) return providerPullRequestError(base, milestones.error, base.prNumber);
1468
+ }
1469
+ if (options.dryRun) return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1470
+ platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, operations, error: null
1471
+ }));
1472
+ let issueChanged = false;
1473
+ if (issueInLabelOperation?.status === 'planned') {
1474
+ const desiredIssueLabels = [...new Set([
1475
+ ...issue.issue.labels.filter((label) => !label.startsWith('in:')),
1476
+ ...inLabels
1477
+ ])].sort();
1478
+ const updatedIssue = base.provider.issues?.update
1479
+ ? await base.provider.issues.update({
1480
+ context: providerOperationContext(base.loadedContext),
1481
+ target: base.issueIdentity!,
1482
+ currentLabels: issue.issue.labels,
1483
+ patch: { labels: desiredIssueLabels },
1484
+ mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}:in-labels` }
1485
+ })
1486
+ : unsupportedProviderOperation(base.provider, 'issues.update');
1487
+ if (!updatedIssue.ok) return softenFailure(providerPullRequestError(base, updatedIssue.error, base.prNumber));
1488
+ issueChanged = updatedIssue.value.changed;
1489
+ const rereadIssue = base.provider.issues?.inspect
1490
+ ? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity! })
1491
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
1492
+ if (!rereadIssue.ok) return softenFailure(providerPullRequestError(base, {
1493
+ ...rereadIssue.error,
1494
+ code: 'IN_LABEL_SYNC_PARTIAL',
1495
+ message: `In-label synchronization is partial or unknown: ${rereadIssue.error.message}`
1496
+ }, base.prNumber));
1497
+ const actualIssueLabels = rereadIssue.value.labels.filter((label) => label.startsWith('in:')).sort();
1498
+ if (actualIssueLabels.join('\0') !== inLabels.join('\0')) return softenFailure(providerPullRequestError(base, {
1499
+ code: 'IN_LABEL_SYNC_PARTIAL',
1500
+ message: 'Issue in: labels did not converge after update',
1501
+ retryable: true
1502
+ }, base.prNumber));
1503
+ }
1504
+ const patch: Record<string, unknown> = {};
1505
+ for (const operation of planned) {
1506
+ if (operation.status !== 'planned') continue;
1507
+ if (operation.name === 'labels') patch.labels = operation.value;
1508
+ if (operation.name === 'assignees') patch.assignees = operation.value;
1509
+ if (operation.name === 'closing-issue') patch.body = operation.value;
1510
+ if (operation.name === 'milestone') patch.milestone = operation.value;
1511
+ }
1512
+ if (Object.keys(patch).length === 0) return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1513
+ platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: inspected.pullRequest, operations, error: null
1514
+ }));
1515
+ const updated = base.provider.changeRequests?.update
1516
+ ? await base.provider.changeRequests.update({
1517
+ context: providerOperationContext(base.loadedContext),
1518
+ target: base.prIdentity,
1519
+ currentLabels: inspected.pullRequest.labels,
1520
+ patch: patch as never,
1521
+ mutation: { idempotencyKey: `pull-request:update:${base.resolved.taskId}` }
1522
+ })
1523
+ : unsupportedProviderOperation(base.provider, 'changeRequests.update');
1524
+ if (!updated.ok) {
1525
+ const error = issueChanged && !updated.error.code.startsWith('IN_LABEL_SYNC')
1526
+ ? { ...updated.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${updated.error.message}` }
1527
+ : updated.error;
1528
+ return softenFailure(providerPullRequestError(base, error, base.prNumber));
1529
+ }
1530
+ let finalPullRequest = inspected.pullRequest;
1531
+ if (planned.some((operation) => operation.name === 'labels' && operation.status === 'planned')) {
1532
+ const reread = base.provider.changeRequests?.inspect
1533
+ ? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: base.prIdentity })
1534
+ : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1535
+ if (!reread.ok) return softenFailure(providerPullRequestError(base, {
1536
+ ...reread.error,
1537
+ code: 'IN_LABEL_SYNC_PARTIAL',
1538
+ message: `In-label synchronization is partial or unknown: ${reread.error.message}`
1539
+ }, base.prNumber));
1540
+ finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository!, base.prNumber || 0, inspected.pullRequest);
1541
+ const expected = ((planned.find((operation) => operation.name === 'labels')?.value as string[] || [])
1542
+ .filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
1543
+ const actual = finalPullRequest.labels.filter((label) => label.startsWith('in:') || label.startsWith('type:')).sort();
1544
+ if (actual.join('\0') !== expected.join('\0')) return softenFailure(providerPullRequestError(base, {
1545
+ code: 'IN_LABEL_SYNC_PARTIAL',
1546
+ message: 'Pull request labels did not converge after update',
1547
+ retryable: true
1548
+ }, base.prNumber));
1549
+ }
1550
+ return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
1551
+ changed: issueChanged || updated.value.changed,
1552
+ platform: base.context.platform,
1553
+ capabilities: base.context.capabilities,
1554
+ resource: { kind: 'pull-request', number: base.prNumber },
1555
+ pullRequest: finalPullRequest,
1556
+ operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
1557
+ error: null
1323
1558
  }));
1324
1559
  }
1325
- const patched = base.client.json<RemotePullRequest>(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
1326
- cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
1327
- });
1328
- if (!patched.ok) return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
1329
- return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
1330
- changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
1331
- resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
1332
- operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
1333
- }));
1334
1560
  }
1335
1561
 
1336
1562
  function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
@@ -1356,11 +1582,13 @@ function skipPlatformPullRequestFactUnlocked(
1356
1582
  error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1357
1583
  });
1358
1584
  }
1359
- const issue = Number(frontmatter.issue_number);
1360
- const issueNumber = Number.isSafeInteger(issue) && issue > 0 ? issue : null;
1361
- const existing = readPrDeliveryFact(frontmatter);
1585
+ let issueIdentity: ResourceIdentity | null;
1586
+ try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
1587
+ catch (error) { return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }); }
1588
+ const issueNumber = resourceIdentityNumber(issueIdentity);
1589
+ const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
1362
1590
  if (existing.status === 'invalid') return result('failed', resolved.taskId, issueNumber, null, {
1363
- error: { code: 'PR_DELIVERY_FACT_INVALID', message: existing.error.message, retryable: false }
1591
+ error: { code: existing.error.code, message: existing.error.message, retryable: false }
1364
1592
  });
1365
1593
  if (existing.status === 'missing') return result('failed', resolved.taskId, issueNumber, null, {
1366
1594
  error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
@@ -1369,9 +1597,12 @@ function skipPlatformPullRequestFactUnlocked(
1369
1597
  operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
1370
1598
  error: null
1371
1599
  });
1372
- if (existing.fact.state === 'bound') return result('failed', resolved.taskId, issueNumber, existing.fact.identity.number, {
1373
- error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR #${existing.fact.identity.number}`, retryable: false }
1374
- });
1600
+ if (existing.fact.state === 'bound') {
1601
+ const boundNumber = resourceIdentityNumber(existing.fact.identity.resource);
1602
+ return result('failed', resolved.taskId, issueNumber, boundNumber, {
1603
+ error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR ${boundNumber === null ? 'identity' : `#${boundNumber}`}`, retryable: false }
1604
+ });
1605
+ }
1375
1606
  if (readProjectPrFlow(resolved.repoRoot) === 'required') return result('failed', resolved.taskId, issueNumber, null, {
1376
1607
  error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
1377
1608
  });
@@ -1400,7 +1631,7 @@ function skipPlatformPullRequestFactUnlocked(
1400
1631
  });
1401
1632
  }
1402
1633
 
1403
- function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOptions): PullRequestResult {
1634
+ async function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOptions): Promise<PullRequestResult> {
1404
1635
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
1405
1636
  if (!resolved.ok) return result('failed', resolved.taskId, null, null, {
1406
1637
  error: { code: resolved.code, message: resolved.message, retryable: false }
@@ -1433,12 +1664,12 @@ export {
1433
1664
  inspectPlatformPullRequest,
1434
1665
  inspectPlatformPullRequestByNumber,
1435
1666
  normalizePullRequest,
1436
- resolveGitHubChangeRequestGitEvidence,
1437
1667
  resolveExternalPullRequest,
1438
1668
  skipPlatformPullRequestFact,
1439
1669
  selectExternalPullRequest,
1440
1670
  selectPullRequest,
1441
- syncPlatformPullRequest
1671
+ syncPlatformPullRequest,
1672
+ syncPlatformPullRequestInLabels
1442
1673
  };
1443
1674
  export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
1444
1675
  export { warningResultForPrimary };