@fitlab-ai/agent-infra 0.9.10 → 0.9.12

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 (375) hide show
  1. package/README.md +19 -9
  2. package/README.zh-CN.md +19 -9
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +55 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
  8. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  9. package/dist/lib/agent-clients/adapters/traecli.js +7 -8
  10. package/dist/lib/agent-clients/config.js +14 -99
  11. package/dist/lib/agent-clients/reconcile.js +1 -11
  12. package/dist/lib/agent-clients/schema.js +5 -4
  13. package/dist/lib/agent-clients/tokens.js +5 -15
  14. package/dist/lib/decide.js +14 -30
  15. package/dist/lib/defaults.json +8 -0
  16. package/dist/lib/git/workflow.js +43 -1
  17. package/dist/lib/init.js +3 -2
  18. package/dist/lib/internal/git-workflow.js +8 -1
  19. package/dist/lib/internal/platform-pr.js +8 -2
  20. package/dist/lib/internal/release-workflow.js +0 -1
  21. package/dist/lib/internal/task-delivery.js +59 -0
  22. package/dist/lib/internal/task-finalization.js +19 -9
  23. package/dist/lib/internal/task-ledger.js +8 -2
  24. package/dist/lib/internal/task-lifecycle.js +17 -93
  25. package/dist/lib/internal/task-orchestration.js +30 -216
  26. package/dist/lib/internal/task-validate.js +2 -1
  27. package/dist/lib/merge.js +205 -103
  28. package/dist/lib/platform/github-client.js +7 -1
  29. package/dist/lib/platform/pr-checks.js +8 -5
  30. package/dist/lib/platform/pr-summary.js +12 -8
  31. package/dist/lib/platform/pull-requests.js +393 -91
  32. package/dist/lib/platform/verification-required.js +13 -2
  33. package/dist/lib/platform/verification-sync.js +9 -4
  34. package/dist/lib/pr-review/evidence-grading.js +12 -4
  35. package/dist/lib/process-data/normalize.js +2 -9
  36. package/dist/lib/run/host.js +46 -16
  37. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  38. package/dist/lib/sandbox/commands/create.js +3 -3
  39. package/dist/lib/sandbox/commands/enter.js +18 -4
  40. package/dist/lib/sandbox/config.js +8 -12
  41. package/dist/lib/sandbox/control/executor.js +131 -39
  42. package/dist/lib/sandbox/control/lifecycle.js +55 -24
  43. package/dist/lib/sandbox/control/server.js +18 -16
  44. package/dist/lib/sandbox/mounts.js +3 -2
  45. package/dist/lib/sandbox/recovery.js +123 -22
  46. package/dist/lib/sandbox/workspace-identity.js +14 -0
  47. package/dist/lib/sandbox/workspace-view.js +8 -9
  48. package/dist/lib/self-update.js +278 -0
  49. package/dist/lib/server/process-state.js +2 -2
  50. package/dist/lib/server/protocol.js +5 -5
  51. package/dist/lib/task/activity-log.js +4 -1
  52. package/dist/lib/task/artifact-lifecycle.js +6 -4
  53. package/dist/lib/task/codex-orchestration.js +1 -1
  54. package/dist/lib/task/command-options.js +1 -3
  55. package/dist/lib/task/commands/cat.js +0 -5
  56. package/dist/lib/task/commands/decisions.js +76 -92
  57. package/dist/lib/task/commands/files.js +6 -6
  58. package/dist/lib/task/commands/grep.js +25 -61
  59. package/dist/lib/task/commands/issue-body.js +6 -6
  60. package/dist/lib/task/commands/log.js +25 -10
  61. package/dist/lib/task/commands/show.js +5 -6
  62. package/dist/lib/task/commands/status.js +8 -8
  63. package/dist/lib/task/commit-intent.js +100 -0
  64. package/dist/lib/task/commit-operation.js +181 -91
  65. package/dist/lib/task/commit-policy.js +1 -1
  66. package/dist/lib/task/control-authority.js +431 -0
  67. package/dist/lib/task/create.js +39 -8
  68. package/dist/lib/task/current-contract.js +35 -0
  69. package/dist/lib/task/decision-details.js +218 -0
  70. package/dist/lib/task/decision-intents.js +5 -2
  71. package/dist/lib/task/delivery-target.js +97 -0
  72. package/dist/lib/task/delivery.js +153 -0
  73. package/dist/lib/task/events.js +11 -5
  74. package/dist/lib/task/human-override.js +5 -2
  75. package/dist/lib/task/index.js +19 -20
  76. package/dist/lib/task/ledger-intents.js +8 -9
  77. package/dist/lib/task/ledger.js +13 -6
  78. package/dist/lib/task/lifecycle.js +122 -9
  79. package/dist/lib/task/orchestration.js +105 -75
  80. package/dist/lib/task/pr-delivery-fact.js +182 -0
  81. package/dist/lib/task/review-artifacts.js +31 -12
  82. package/dist/lib/task/review-finalization.js +13 -4
  83. package/dist/lib/task/review-fingerprint.js +8 -0
  84. package/dist/lib/task/verification-engine.js +86 -39
  85. package/dist/lib/task/verification-review.js +2 -2
  86. package/dist/lib/task/workspace-snapshot.js +5 -11
  87. package/dist/lib/task/write.js +11 -1
  88. package/dist/lib/update.js +13 -35
  89. package/dist/lib/version.js +5 -1
  90. package/dist/lifecycle-build-manifest.json +3 -3
  91. package/lib/agent-client.ts +0 -1
  92. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
  93. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
  94. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  95. package/lib/agent-clients/adapters/traecli.ts +7 -8
  96. package/lib/agent-clients/config.ts +14 -144
  97. package/lib/agent-clients/reconcile.ts +1 -12
  98. package/lib/agent-clients/schema.ts +5 -4
  99. package/lib/agent-clients/tokens.ts +5 -18
  100. package/lib/decide.ts +12 -22
  101. package/lib/defaults.json +8 -0
  102. package/lib/git/workflow.ts +38 -1
  103. package/lib/init.ts +5 -2
  104. package/lib/internal/git-workflow.ts +7 -1
  105. package/lib/internal/platform-pr.ts +8 -1
  106. package/lib/internal/release-workflow.ts +0 -1
  107. package/lib/internal/task-delivery.ts +53 -0
  108. package/lib/internal/task-finalization.ts +27 -13
  109. package/lib/internal/task-ledger.ts +8 -2
  110. package/lib/internal/task-lifecycle.ts +33 -76
  111. package/lib/internal/task-orchestration.ts +33 -190
  112. package/lib/internal/task-validate.ts +3 -2
  113. package/lib/merge.ts +236 -120
  114. package/lib/platform/github-client.ts +8 -1
  115. package/lib/platform/pr-checks.ts +7 -5
  116. package/lib/platform/pr-summary.ts +10 -7
  117. package/lib/platform/pull-requests.ts +425 -87
  118. package/lib/platform/verification-required.ts +12 -2
  119. package/lib/platform/verification-sync.ts +9 -4
  120. package/lib/pr-review/evidence-grading.ts +7 -4
  121. package/lib/process-data/normalize.ts +2 -8
  122. package/lib/run/host.ts +63 -14
  123. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  124. package/lib/sandbox/commands/create.ts +4 -4
  125. package/lib/sandbox/commands/enter.ts +24 -4
  126. package/lib/sandbox/config.ts +9 -14
  127. package/lib/sandbox/control/container-identity.ts +2 -2
  128. package/lib/sandbox/control/executor.ts +148 -51
  129. package/lib/sandbox/control/lifecycle.ts +59 -31
  130. package/lib/sandbox/control/protocol.ts +1 -3
  131. package/lib/sandbox/control/server.ts +15 -12
  132. package/lib/sandbox/control/state.ts +7 -7
  133. package/lib/sandbox/mounts.ts +4 -3
  134. package/lib/sandbox/recovery.ts +162 -34
  135. package/lib/sandbox/tools.ts +1 -1
  136. package/lib/sandbox/workspace-identity.ts +28 -0
  137. package/lib/sandbox/workspace-view.ts +14 -10
  138. package/lib/self-update.ts +341 -0
  139. package/lib/server/process-state.ts +2 -2
  140. package/lib/server/protocol.ts +5 -5
  141. package/lib/task/activity-log.ts +6 -1
  142. package/lib/task/artifact-lifecycle.ts +7 -5
  143. package/lib/task/codex-orchestration.ts +2 -1
  144. package/lib/task/command-options.ts +1 -2
  145. package/lib/task/commands/cat.ts +0 -4
  146. package/lib/task/commands/decisions.ts +80 -75
  147. package/lib/task/commands/files.ts +6 -6
  148. package/lib/task/commands/grep.ts +23 -51
  149. package/lib/task/commands/issue-body.ts +6 -6
  150. package/lib/task/commands/log.ts +22 -10
  151. package/lib/task/commands/show.ts +5 -6
  152. package/lib/task/commands/status.ts +8 -8
  153. package/lib/task/commit-intent.ts +137 -0
  154. package/lib/task/commit-operation.ts +161 -79
  155. package/lib/task/commit-policy.ts +1 -2
  156. package/lib/task/control-authority.ts +563 -0
  157. package/lib/task/create.ts +40 -10
  158. package/lib/task/current-contract.ts +49 -0
  159. package/lib/task/decision-details.ts +280 -0
  160. package/lib/task/decision-intents.ts +7 -2
  161. package/lib/task/delivery-target.ts +130 -0
  162. package/lib/task/delivery.ts +160 -0
  163. package/lib/task/events.ts +10 -5
  164. package/lib/task/human-override.ts +5 -2
  165. package/lib/task/index.ts +19 -20
  166. package/lib/task/ledger-intents.ts +6 -8
  167. package/lib/task/ledger.ts +17 -7
  168. package/lib/task/lifecycle.ts +113 -4
  169. package/lib/task/orchestration.ts +102 -80
  170. package/lib/task/pr-delivery-fact.ts +254 -0
  171. package/lib/task/review-artifacts.ts +53 -15
  172. package/lib/task/review-finalization.ts +21 -5
  173. package/lib/task/review-fingerprint.ts +10 -0
  174. package/lib/task/verification-engine.ts +78 -46
  175. package/lib/task/verification-review.ts +3 -1
  176. package/lib/task/workspace-snapshot.ts +4 -13
  177. package/lib/task/write.ts +20 -1
  178. package/lib/update.ts +14 -38
  179. package/lib/version.ts +6 -1
  180. package/package.json +1 -1
  181. package/templates/.agents/README.en.md +5 -3
  182. package/templates/.agents/README.zh-CN.md +5 -3
  183. package/templates/.agents/rules/README.en.md +4 -2
  184. package/templates/.agents/rules/README.zh-CN.md +4 -2
  185. package/templates/.agents/rules/compatibility-policy.en.md +5 -4
  186. package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
  187. package/templates/.agents/rules/create-issue.en.md +11 -2
  188. package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
  189. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  190. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  191. package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +1 -1
  192. package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +1 -1
  193. package/templates/.agents/rules/issue-pr-commands.en.md +5 -18
  194. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  195. package/templates/.agents/rules/issue-sync.en.md +26 -5
  196. package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
  197. package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
  198. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
  199. package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
  200. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
  201. package/templates/.agents/rules/milestone-inference.en.md +12 -2
  202. package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
  203. package/templates/.agents/rules/next-step-output.en.md +3 -3
  204. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  205. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  206. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  207. package/templates/.agents/rules/pr-sync.en.md +23 -14
  208. package/templates/.agents/rules/pr-sync.zh-CN.md +23 -2
  209. package/templates/.agents/rules/release-commands.en.md +42 -3
  210. package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
  211. package/templates/.agents/rules/review-handshake.en.md +1 -1
  212. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  213. package/templates/.agents/rules/security-alerts.en.md +17 -2
  214. package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
  215. package/templates/.agents/rules/sync-content-generation.en.md +8 -0
  216. package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
  217. package/templates/.agents/rules/task-management.en.md +2 -3
  218. package/templates/.agents/rules/task-management.zh-CN.md +2 -3
  219. package/templates/.agents/scripts/security-alerts.github.sh +151 -0
  220. package/templates/.agents/scripts/security-alerts.sh +31 -0
  221. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -3
  222. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -3
  223. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  224. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  225. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  226. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  227. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -3
  230. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -3
  231. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -3
  232. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -3
  233. package/templates/.agents/skills/code-task/SKILL.en.md +8 -5
  234. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +9 -6
  235. package/templates/.agents/skills/commit/SKILL.en.md +30 -10
  236. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -7
  237. package/templates/.agents/skills/commit/config/verify.json +1 -1
  238. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
  239. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
  240. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  241. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  242. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  243. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  244. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +7 -6
  245. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +7 -6
  246. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  247. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  248. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  249. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  250. package/templates/.agents/skills/complete-task/SKILL.en.md +13 -13
  251. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +13 -13
  252. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
  253. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
  254. package/templates/.agents/skills/create-pr/SKILL.en.md +9 -11
  255. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +9 -11
  256. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
  257. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
  258. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  259. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  260. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  261. package/templates/.agents/skills/create-task/SKILL.en.md +2 -1
  262. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +2 -1
  263. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  264. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  265. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  266. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  267. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  268. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  269. package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
  270. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
  271. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
  272. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
  273. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
  274. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
  275. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -3
  276. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -3
  277. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  278. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  279. package/templates/.agents/skills/review-analysis/SKILL.en.md +8 -5
  280. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +8 -5
  281. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  282. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  283. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
  284. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
  285. package/templates/.agents/skills/review-code/SKILL.en.md +11 -8
  286. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -8
  287. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  288. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  289. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
  290. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
  291. package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
  292. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
  293. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
  294. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
  295. package/templates/.agents/skills/review-plan/SKILL.en.md +8 -5
  296. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +8 -5
  297. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  298. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  299. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
  300. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
  301. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
  302. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
  303. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
  304. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
  305. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -0
  306. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -0
  307. package/templates/.agents/skills/run-task/SKILL.en.md +5 -1
  308. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -1
  309. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  310. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  311. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +144 -109
  312. package/templates/.agents/skills/watch-pr/SKILL.en.md +5 -5
  313. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +5 -5
  314. package/templates/.agents/templates/task.en.md +6 -2
  315. package/templates/.agents/templates/task.zh-CN.md +6 -2
  316. package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
  317. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
  318. package/templates/.agents/workflows/feature-development.en.yaml +9 -8
  319. package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
  320. package/templates/.agents/workflows/refactoring.en.yaml +9 -8
  321. package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
  322. package/templates/.claude/commands/analyze-task.en.md +1 -1
  323. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  324. package/templates/.claude/commands/block-task.en.md +1 -1
  325. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  326. package/templates/.claude/commands/cancel-task.en.md +1 -1
  327. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  328. package/templates/.claude/commands/check-task.en.md +1 -1
  329. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  330. package/templates/.claude/commands/code-task.en.md +1 -1
  331. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  332. package/templates/.claude/commands/commit.en.md +1 -1
  333. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  334. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  335. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  336. package/templates/.claude/commands/complete-task.en.md +1 -1
  337. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  338. package/templates/.claude/commands/create-pr.en.md +1 -1
  339. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  340. package/templates/.claude/commands/plan-task.en.md +1 -1
  341. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  342. package/templates/.claude/commands/review-analysis.en.md +1 -1
  343. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  344. package/templates/.claude/commands/review-code.en.md +1 -1
  345. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  346. package/templates/.claude/commands/review-plan.en.md +1 -1
  347. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  348. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  349. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  350. package/templates/.claude/commands/run-task.en.md +1 -1
  351. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  352. package/templates/.claude/commands/watch-pr.en.md +1 -1
  353. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  354. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
  355. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
  356. package/dist/lib/builtin-tuis.js +0 -40
  357. package/lib/builtin-tuis.ts +0 -56
  358. package/templates/.agents/rules/create-issue.github.en.md +0 -14
  359. package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
  360. package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
  361. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
  362. package/templates/.agents/rules/issue-sync.github.en.md +0 -40
  363. package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
  364. package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
  365. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
  366. package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
  367. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
  368. package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
  369. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
  370. package/templates/.agents/rules/pr-sync.github.en.md +0 -43
  371. package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
  372. package/templates/.agents/rules/release-commands.github.en.md +0 -38
  373. package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -38
  374. package/templates/.agents/rules/security-alerts.github.en.md +0 -43
  375. package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { execFileSync } from 'node:child_process';
3
4
 
4
- import { parseTaskFrontmatter } from '../task/frontmatter.ts';
5
+ import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
5
6
  import { resolveTaskRef } from '../task/resolve-ref.ts';
6
7
  import { extractSection } from '../task/sections.ts';
7
8
  import { captureTaskWriteMetadata, writeTask } from '../task/write.ts';
@@ -20,7 +21,15 @@ import { platformResult } from './types.ts';
20
21
  import type { PlatformOperation, PlatformResult } from './types.ts';
21
22
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
22
23
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
24
+ import { resolveDeliveryTarget } from '../task/delivery-target.ts';
23
25
  import { mergeOperationWarnings, type OperationWarning } from '../task/operation-outcome.ts';
26
+ import {
27
+ buildBoundFact,
28
+ buildSkippedFact,
29
+ factFrontmatterMutation,
30
+ readPrDeliveryFact
31
+ } from '../task/pr-delivery-fact.ts';
32
+ import type { CreationOutcome, PrDeliveryBindingSource, PrDeliveryFact } from '../task/pr-delivery-fact.ts';
24
33
 
25
34
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
26
35
 
@@ -28,6 +37,7 @@ type PullRequestResult = PlatformResult & {
28
37
  task: { id: string | null; issueNumber: number | null; prNumber: number | null };
29
38
  pullRequest: PullRequestSnapshot | null;
30
39
  result: 'pr_created' | 'pr_reused' | 'no_op' | 'pr_created_with_warnings' | 'pr_reused_with_warnings' | 'no_op_with_warnings' | 'failed' | 'blocked' | null;
40
+ creation: CreationOutcome | null;
31
41
  warnings: readonly OperationWarning[];
32
42
  };
33
43
  type InspectionOptions = { cwd?: string; client?: unknown };
@@ -40,11 +50,13 @@ type CreateOptions = SharedOptions & {
40
50
  body: string;
41
51
  draft?: boolean;
42
52
  dryRun?: boolean;
53
+ phase?: { value: CreatePhase };
43
54
  };
44
55
  type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
45
56
  type PullRequestPrimaryResult = 'pr_created' | 'pr_reused' | 'no_op';
46
57
  type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean; primaryResult: PullRequestPrimaryResult };
47
58
  type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
59
+ type SkipFactOptions = { cwd?: string; agent: string; dryRun?: boolean };
48
60
  type ExternalPullRequestSelection =
49
61
  | { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
50
62
  | { status: 'selected'; source: 'unique' | 'explicit'; selected: PullRequestSnapshot; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] }
@@ -57,6 +69,8 @@ type ExternalPullRequestResult = PullRequestResult & {
57
69
  selected: PullRequestSnapshot | null;
58
70
  };
59
71
 
72
+ type CreatePhase = 'before-post' | 'post-dispatched' | 'post-accepted';
73
+
60
74
  type RemotePullRequest = {
61
75
  number?: number;
62
76
  node_id?: string;
@@ -123,6 +137,7 @@ function result(
123
137
  task: { id: taskId, issueNumber, prNumber },
124
138
  pullRequest: null,
125
139
  result: null,
140
+ creation: null,
126
141
  warnings: [],
127
142
  ...overrides
128
143
  };
@@ -144,6 +159,14 @@ function externalResult(
144
159
  };
145
160
  }
146
161
 
162
+ function withCreation(output: PullRequestResult, creation: CreationOutcome): PullRequestResult {
163
+ return { ...output, creation };
164
+ }
165
+
166
+ const PRECONDITION_NOT_CREATED: CreationOutcome = {
167
+ kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
168
+ };
169
+
147
170
  function normalizePullRequest(remote: RemotePullRequest, repository: string): PullRequestSnapshot | null {
148
171
  const number = Number(remote.number);
149
172
  const headRepository = remote.head?.repo?.full_name;
@@ -216,7 +239,7 @@ function hasCompleteExternalIdentity(pullRequest: PullRequestSnapshot): boolean
216
239
  ];
217
240
  return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
218
241
  required.every((value) => Boolean(value?.trim())) &&
219
- Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
242
+ (pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
220
243
  }
221
244
 
222
245
  function selectExternalPullRequest(
@@ -247,6 +270,7 @@ function selectExternalPullRequest(
247
270
  const eligible = unique.filter((candidate) =>
248
271
  repositoryKey(candidate.repository) === wantedRepository &&
249
272
  repositoryKey(candidate.base.repository) === wantedRepository &&
273
+ candidate.state === 'closed' &&
250
274
  Boolean(candidate.mergedAt && candidate.mergeCommitSha)
251
275
  );
252
276
  if (explicitPrNumber !== null) {
@@ -421,15 +445,104 @@ function validateBoundPullRequest(
421
445
  return { ok: true };
422
446
  }
423
447
 
448
+ type BindingIdentityOptions = {
449
+ expectedHead?: string;
450
+ expectedHeadSha?: string;
451
+ errorCode?: string;
452
+ };
453
+
454
+ function resolveTaskDeliveryTarget(repoRoot: string, frontmatter: Record<string, unknown>) {
455
+ const existing = {
456
+ ...(typeof frontmatter.delivery_remote === 'string' && frontmatter.delivery_remote
457
+ ? { remote: frontmatter.delivery_remote } : {}),
458
+ ...(typeof frontmatter.delivery_base_ref === 'string' && frontmatter.delivery_base_ref
459
+ ? { baseRef: frontmatter.delivery_base_ref } : {})
460
+ };
461
+ const target = resolveDeliveryTarget(repoRoot, existing);
462
+ return target.ok
463
+ ? { ok: true as const, value: target.value }
464
+ : { ok: false as const, error: { code: target.code, message: target.message, retryable: false } };
465
+ }
466
+
467
+ function taskDeliveryTarget(base: ReturnType<typeof resolvedContext> & { ok: true }) {
468
+ return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
469
+ }
470
+
471
+ function validateIdentityAgainstTarget(
472
+ repository: string,
473
+ target: { remote: string; baseRef: string },
474
+ pullRequest: PullRequestSnapshot | null,
475
+ options: BindingIdentityOptions = {}
476
+ ): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
477
+ if (!pullRequest || !hasCompleteExternalIdentity(pullRequest)) return {
478
+ ok: false,
479
+ error: { code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity is incomplete', retryable: false }
480
+ };
481
+ const wantedHead = options.expectedHead ? expectedHead(repository, options.expectedHead) : null;
482
+ const matches = repositoryKey(pullRequest.repository) === repositoryKey(repository)
483
+ && repositoryKey(pullRequest.base.repository) === repositoryKey(repository)
484
+ && pullRequest.base.ref === target.baseRef
485
+ && (!wantedHead || (repositoryKey(pullRequest.head.repository) === repositoryKey(wantedHead.repository) && pullRequest.head.ref === wantedHead.ref))
486
+ && (!options.expectedHeadSha || pullRequest.head.sha === options.expectedHeadSha);
487
+ if (!matches) return {
488
+ ok: false,
489
+ error: {
490
+ code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH',
491
+ message: 'Pull request identity does not match the task branch and delivery target',
492
+ retryable: false
493
+ }
494
+ };
495
+ return { ok: true };
496
+ }
497
+
498
+ function validateWriterIdentity(
499
+ base: ReturnType<typeof resolvedContext> & { ok: true },
500
+ pullRequest: PullRequestSnapshot | null,
501
+ options: BindingIdentityOptions = {}
502
+ ): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
503
+ const target = taskDeliveryTarget(base);
504
+ if (!target.ok) return target;
505
+ return validateIdentityAgainstTarget(base.context.platform.repository!, target.value, pullRequest, options);
506
+ }
507
+
508
+ function sameDeliveryIdentity(left: PullRequestSnapshot, right: PullRequestSnapshot): boolean {
509
+ return JSON.stringify(deliveryIdentity(left)) === JSON.stringify(deliveryIdentity(right));
510
+ }
511
+
512
+ function validateExternalMergedEvidence(
513
+ initial: PullRequestSnapshot,
514
+ rechecked: PullRequestSnapshot
515
+ ): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
516
+ if (
517
+ initial.state !== 'closed'
518
+ || !initial.mergedAt
519
+ || !initial.mergeCommitSha
520
+ || rechecked.state !== 'closed'
521
+ || !rechecked.mergedAt
522
+ || !rechecked.mergeCommitSha
523
+ || initial.mergedAt !== rechecked.mergedAt
524
+ || initial.mergeCommitSha !== rechecked.mergeCommitSha
525
+ ) return {
526
+ ok: false,
527
+ error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request merged evidence changed before task binding', retryable: false }
528
+ };
529
+ return { ok: true };
530
+ }
531
+
424
532
  function resolvedContext(taskRef: string, options: InspectionOptions) {
425
533
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
426
534
  if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
427
535
  error: { code: resolved.code, message: resolved.message, retryable: false }
428
536
  }) };
429
537
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
430
- const frontmatter = parseTaskFrontmatter(content);
538
+ const frontmatter = parseTypedTaskFrontmatter(content);
539
+ const factRead = readPrDeliveryFact(frontmatter);
540
+ 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 }
542
+ }) };
543
+ const fact = factRead.status === 'valid' ? factRead.fact : null;
431
544
  const issue = Number(frontmatter.issue_number);
432
- const pr = Number(frontmatter.pr_number);
545
+ const pr = fact?.state === 'bound' ? fact.identity.number : NaN;
433
546
  const issueNumber = Number.isInteger(issue) && issue > 0 ? issue : null;
434
547
  const prNumber = Number.isInteger(pr) && pr > 0 ? pr : null;
435
548
  const client = (options.client as GitHubClient | undefined) || createGitHubClient();
@@ -438,7 +551,7 @@ function resolvedContext(taskRef: string, options: InspectionOptions) {
438
551
  if (!usable) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
439
552
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
440
553
  }) };
441
- return { ok: true as const, resolved, content, frontmatter, issueNumber, prNumber, client, context };
554
+ return { ok: true as const, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
442
555
  }
443
556
 
444
557
  function inspectGitHubPullRequest(client: GitHubClient, repository: string, number: number, cwd: string) {
@@ -582,9 +695,12 @@ function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true
582
695
  function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions = {}): PullRequestResult {
583
696
  const base = resolvedContext(taskRef, options);
584
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 }
700
+ });
585
701
  if (!base.prNumber) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
586
702
  platform: base.context.platform, capabilities: base.context.capabilities,
587
- error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
703
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
588
704
  });
589
705
  const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
590
706
  cwd: base.resolved.repoRoot,
@@ -652,6 +768,38 @@ function appendActivity(content: string, line: string): string {
652
768
  return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
653
769
  }
654
770
 
771
+ function deliveryIdentity(pullRequest: PullRequestSnapshot) {
772
+ return {
773
+ repository: pullRequest.repository,
774
+ number: pullRequest.number,
775
+ nodeId: pullRequest.nodeId,
776
+ url: pullRequest.url,
777
+ head: { ...pullRequest.head },
778
+ base: { ...pullRequest.base }
779
+ };
780
+ }
781
+
782
+ function factTimestamp(timestamp: string): string {
783
+ return new Date(timestamp.replace(' ', 'T')).toISOString();
784
+ }
785
+
786
+ function boundFactFor(
787
+ pullRequest: PullRequestSnapshot,
788
+ source: PrDeliveryBindingSource,
789
+ issueNumber: number | null,
790
+ verifiedAt: string
791
+ ): PrDeliveryFact {
792
+ return buildBoundFact({
793
+ identity: deliveryIdentity(pullRequest),
794
+ source,
795
+ issueNumber,
796
+ verifiedAt: factTimestamp(verifiedAt),
797
+ remoteState: pullRequest.state,
798
+ mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
799
+ mergeCommitSha: pullRequest.state === 'closed' ? pullRequest.mergeCommitSha : null
800
+ });
801
+ }
802
+
655
803
  function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: true }, agent: string) {
656
804
  const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
657
805
  const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
@@ -666,19 +814,40 @@ function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: tru
666
814
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
667
815
  }
668
816
 
669
- function bindIdentity(base: ReturnType<typeof resolvedContext> & { ok: true }, pullRequest: PullRequestSnapshot, agent: string, dryRun = false) {
817
+ function bindIdentity(
818
+ base: ReturnType<typeof resolvedContext> & { ok: true },
819
+ pullRequest: PullRequestSnapshot,
820
+ agent: string,
821
+ source: PrDeliveryBindingSource,
822
+ dryRun = false,
823
+ expectedHead?: string,
824
+ expectedHeadSha?: string
825
+ ) {
826
+ const identity = validateWriterIdentity(base, pullRequest, {
827
+ expectedHead: source === 'explicit-bind' ? String(base.frontmatter.branch || '') : expectedHead,
828
+ expectedHeadSha,
829
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
830
+ });
831
+ if (!identity.ok) return { status: 'failed' as const, error: identity.error };
670
832
  if (base.prNumber && base.prNumber !== pullRequest.number) return { status: 'failed' as const, error: {
671
833
  code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`
672
834
  } };
673
- if (base.prNumber === pullRequest.number) return { status: 'no-op' as const, error: null };
835
+ if (base.prNumber === pullRequest.number && base.fact?.state === 'bound') {
836
+ if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest))) return { status: 'failed' as const, error: {
837
+ code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
838
+ } };
839
+ return { status: 'no-op' as const, error: null };
840
+ }
674
841
  const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
675
842
  const metadata = captureTaskWriteMetadata();
843
+ const fact = boundFactFor(pullRequest, source, base.issueNumber, metadata.timestamp);
844
+ const factMutation = factFrontmatterMutation(fact);
676
845
  return writeTask({
677
846
  taskRef: base.resolved.taskId,
678
847
  expectedState: 'active',
679
848
  dryRun,
680
849
  mutations: [
681
- { kind: 'frontmatter', set: { pr_number: pullRequest.number, pr_status: 'created', assigned_to: agent } },
850
+ { kind: 'frontmatter', set: { ...factMutation.set, assigned_to: agent } },
682
851
  { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
683
852
  current, `- ${metadata.timestamp} — **Create PR** by ${agent} — PR #${pullRequest.number} created → ${pullRequest.url}`
684
853
  ) }
@@ -689,6 +858,9 @@ function bindIdentity(base: ReturnType<typeof resolvedContext> & { ok: true }, p
689
858
  function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullRequestResult {
690
859
  const base = resolvedContext(taskRef, options);
691
860
  if (!base.ok) return base.output;
861
+ if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
862
+ 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
+ });
692
864
  if (!Number.isInteger(options.pr) || options.pr <= 0) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
693
865
  error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
694
866
  });
@@ -697,13 +869,29 @@ function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullReq
697
869
  });
698
870
  const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, options.pr, base.resolved.repoRoot);
699
871
  if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
700
- const written = bindIdentity(base, fetched.value, options.agent, options.dryRun);
872
+ const initialIdentity = validateWriterIdentity(base, fetched.value, {
873
+ expectedHead: String(base.frontmatter.branch || ''),
874
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
875
+ });
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 }
887
+ });
888
+ const written = bindIdentity(base, rechecked.value, options.agent, 'explicit-bind', options.dryRun);
701
889
  if (written.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
702
890
  pullRequest: fetched.value, error: { code: written.error.code, message: written.error.message, retryable: false }
703
891
  });
704
- return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : fetched.value.number, {
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, {
705
893
  changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
706
- resource: { kind: 'pull-request', number: fetched.value.number }, pullRequest: fetched.value,
894
+ resource: { kind: 'pull-request', number: rechecked.value.number }, pullRequest: rechecked.value,
707
895
  operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
708
896
  });
709
897
  }
@@ -737,6 +925,9 @@ function externalIdentityEvidenceNote(
737
925
  function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): ExternalPullRequestResult {
738
926
  const base = resolvedContext(taskRef, options);
739
927
  if (!base.ok) return externalResult(base.output);
928
+ if (!base.fact) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
929
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
930
+ }));
740
931
  const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
741
932
  if (inventory.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
742
933
  platform: base.context.platform,
@@ -791,32 +982,59 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
791
982
  error: { code: selected.code, message: selected.message, retryable: false }
792
983
  }), { candidates: selected.candidates, eligible: selected.eligible });
793
984
 
794
- const note = externalEvidenceNote(base.issueNumber, selected.source, selected.selected);
795
- const identityNote = externalIdentityEvidenceNote(base.issueNumber, selected.selected);
985
+ const initialIdentity = validateWriterIdentity(base, selected.selected, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
986
+ if (!initialIdentity.ok) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
987
+ platform: base.context.platform, capabilities: base.context.capabilities,
988
+ pullRequest: selected.selected, error: initialIdentity.error
989
+ }), { 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);
991
+ if (!rechecked.ok || !rechecked.value) return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
992
+ platform: base.context.platform, capabilities: base.context.capabilities,
993
+ pullRequest: selected.selected,
994
+ error: rechecked.ok ? { code: 'PR_EXTERNAL_RECHECK_FAILED', message: 'Selected pull request recheck returned no identity', retryable: false } : rechecked.error
995
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
996
+ if (!sameDeliveryIdentity(selected.selected, rechecked.value)) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
997
+ platform: base.context.platform, capabilities: base.context.capabilities,
998
+ pullRequest: rechecked.value,
999
+ error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request identity changed before task binding', retryable: false }
1000
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1001
+ const finalIdentity = validateWriterIdentity(base, rechecked.value, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
1002
+ if (!finalIdentity.ok) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1003
+ platform: base.context.platform, capabilities: base.context.capabilities,
1004
+ pullRequest: rechecked.value, error: finalIdentity.error
1005
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1006
+ const finalEvidence = validateExternalMergedEvidence(selected.selected, rechecked.value);
1007
+ if (!finalEvidence.ok) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1008
+ platform: base.context.platform, capabilities: base.context.capabilities,
1009
+ pullRequest: rechecked.value, error: finalEvidence.error
1010
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
1011
+ const selectedPullRequest = rechecked.value;
1012
+ const note = externalEvidenceNote(base.issueNumber, selected.source, selectedPullRequest);
1013
+ const identityNote = externalIdentityEvidenceNote(base.issueNumber, selectedPullRequest);
796
1014
  const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
797
1015
  const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
798
1016
  const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
799
1017
  if (auditLines.length > 0 && !alreadyAudited) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
800
1018
  platform: base.context.platform,
801
1019
  capabilities: base.context.capabilities,
802
- pullRequest: selected.selected,
1020
+ pullRequest: selectedPullRequest,
803
1021
  error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
804
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
805
- if (alreadyAudited && base.prNumber !== selected.selected.number) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1022
+ }), { 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, {
806
1024
  platform: base.context.platform,
807
1025
  capabilities: base.context.capabilities,
808
- pullRequest: selected.selected,
1026
+ pullRequest: selectedPullRequest,
809
1027
  error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
810
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
811
- if (base.prNumber === selected.selected.number && alreadyAudited) {
1028
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1029
+ if (base.prNumber === selectedPullRequest.number && alreadyAudited) {
812
1030
  return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
813
1031
  platform: base.context.platform,
814
1032
  capabilities: base.context.capabilities,
815
- resource: { kind: 'pull-request', number: selected.selected.number },
816
- pullRequest: selected.selected,
1033
+ resource: { kind: 'pull-request', number: selectedPullRequest.number },
1034
+ pullRequest: selectedPullRequest,
817
1035
  operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
818
1036
  error: null
819
- }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
1037
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
820
1038
  }
821
1039
  const metadata = captureTaskWriteMetadata();
822
1040
  const write = writeTask({
@@ -824,7 +1042,7 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
824
1042
  expectedState: 'active',
825
1043
  dryRun: options.dryRun,
826
1044
  mutations: [
827
- { kind: 'frontmatter', set: { pr_number: selected.selected.number, pr_status: 'created', assigned_to: options.agent } },
1045
+ { kind: 'frontmatter', set: { ...factFrontmatterMutation(boundFactFor(selectedPullRequest, selected.source === 'unique' ? 'external-unique' : 'external-explicit', base.issueNumber, metadata.timestamp)).set, assigned_to: options.agent } },
828
1046
  { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
829
1047
  base.content, `- ${metadata.timestamp} — ${activityEntry}`
830
1048
  ) }
@@ -833,181 +1051,205 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
833
1051
  if (write.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
834
1052
  platform: base.context.platform,
835
1053
  capabilities: base.context.capabilities,
836
- pullRequest: selected.selected,
1054
+ pullRequest: selectedPullRequest,
837
1055
  error: { code: write.error.code, message: write.error.message, retryable: false }
838
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
1056
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
839
1057
  const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
840
- return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selected.selected.number, {
1058
+ return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selectedPullRequest.number, {
841
1059
  changed: !options.dryRun && write.changed,
842
1060
  platform: base.context.platform,
843
1061
  capabilities: base.context.capabilities,
844
- resource: { kind: 'pull-request', number: selected.selected.number },
845
- pullRequest: selected.selected,
1062
+ resource: { kind: 'pull-request', number: selectedPullRequest.number },
1063
+ pullRequest: selectedPullRequest,
846
1064
  operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
847
1065
  error: null
848
- }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
1066
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
849
1067
  }
850
1068
 
851
1069
  function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptions): PullRequestResult {
852
1070
  const base = resolvedContext(taskRef, options);
853
- if (!base.ok) return base.output;
854
- if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
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, {
855
1076
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
856
- });
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);
857
1086
  if (base.prNumber) {
858
1087
  const headCheck = verifyCreateHead(base, options.head);
859
- if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1088
+ if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
860
1089
  platform: base.context.platform,
861
1090
  capabilities: base.context.capabilities,
862
1091
  error: headCheck.error
863
- });
1092
+ }), PRECONDITION_NOT_CREATED);
864
1093
  const inspected = inspectPlatformPullRequest(taskRef, options);
865
1094
  const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
866
- if (!identity.ok) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1095
+ if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
867
1096
  platform: base.context.platform,
868
1097
  capabilities: base.context.capabilities,
869
1098
  pullRequest: inspected.pullRequest,
870
1099
  error: identity.error
871
- });
872
- return { ...inspected, result: 'no_op' };
1100
+ }), PRECONDITION_NOT_CREATED);
1101
+ return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
873
1102
  }
874
1103
  const headCheck = verifyCreateHead(base, options.head);
875
- if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
1104
+ if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
876
1105
  platform: base.context.platform,
877
1106
  capabilities: base.context.capabilities,
878
1107
  error: headCheck.error
879
- });
1108
+ }), PRECONDITION_NOT_CREATED);
880
1109
  const expectedHeadSha = headCheck.value;
881
1110
  const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
882
- if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
1111
+ if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
883
1112
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
884
- });
885
- if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return result('failed', base.resolved.taskId, base.issueNumber, null, {
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, {
886
1115
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
887
- });
888
- if (options.dryRun) return result('planned', base.resolved.taskId, base.issueNumber, null, {
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, {
889
1121
  platform: base.context.platform, capabilities: base.context.capabilities,
890
1122
  operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
891
1123
  result: located.ok ? 'pr_reused' : 'pr_created', error: null
892
- });
1124
+ }), { kind: 'planned', action: located.ok ? 'reuse' : 'create', createdByCurrentOperation: false });
893
1125
  const started = writeCreateStarted(base, options.agent);
894
- if (started.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, null, {
1126
+ if (started.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
895
1127
  error: { code: started.error.code, message: started.error.message, retryable: false }
896
- });
1128
+ }), PRECONDITION_NOT_CREATED);
897
1129
  let pullRequest = located.ok ? located.value : null;
898
1130
  let created = false;
899
1131
  if (!pullRequest) {
900
1132
  const repository = base.context.platform.repository!;
1133
+ if (options.phase) options.phase.value = 'post-dispatched';
901
1134
  const response = base.client.json<RemotePullRequest>(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
902
1135
  cwd: base.resolved.repoRoot,
903
1136
  method: 'POST',
904
1137
  input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
905
1138
  });
906
- if (response.ok) pullRequest = normalizePullRequest(response.value, repository);
907
- else if (response.error.retryable) {
908
- const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
909
- if (recovered.ok) pullRequest = recovered.value;
910
- else return result('blocked', base.resolved.taskId, base.issueNumber, null, {
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, {
911
1148
  platform: base.context.platform, capabilities: base.context.capabilities,
912
1149
  operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
913
1150
  error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
914
- });
915
- } else return result('failed', base.resolved.taskId, base.issueNumber, null, {
1151
+ }), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
1152
+ } else return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
916
1153
  platform: base.context.platform, capabilities: base.context.capabilities,
917
1154
  operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
918
- });
919
- if (!pullRequest) return result('failed', base.resolved.taskId, base.issueNumber, null, {
1155
+ }), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
1156
+ if (!pullRequest) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
920
1157
  error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
921
- });
922
- if (pullRequest.head.sha !== expectedHeadSha) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1158
+ }), { kind: 'created', createdByCurrentOperation: true });
1159
+ if (pullRequest.head.sha !== expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
923
1160
  platform: base.context.platform, capabilities: base.context.capabilities,
924
1161
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
925
1162
  error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
926
- });
927
- created = true;
1163
+ }), { kind: 'created', createdByCurrentOperation: true });
928
1164
  }
929
1165
  const bindHeadCheck = verifyCreateHead(base, options.head);
930
1166
  if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
931
1167
  const error = bindHeadCheck.ok
932
1168
  ? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
933
1169
  : bindHeadCheck.error;
934
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1170
+ return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
935
1171
  platform: base.context.platform, capabilities: base.context.capabilities,
936
1172
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
937
- });
1173
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
938
1174
  }
939
1175
  const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository!, pullRequest.number, base.resolved.repoRoot);
940
1176
  if (!beforeBind.ok || !beforeBind.value) {
941
1177
  const error = beforeBind.ok
942
1178
  ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
943
1179
  : beforeBind.error;
944
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1180
+ return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
945
1181
  platform: base.context.platform, capabilities: base.context.capabilities,
946
1182
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
947
- });
1183
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
948
1184
  }
949
1185
  if (
950
1186
  beforeBind.value.head.sha !== expectedHeadSha
951
1187
  || beforeBind.value.head.ref !== expectedHead(base.context.platform.repository!, options.head).ref
952
1188
  || beforeBind.value.base.ref !== options.base
953
1189
  || beforeBind.value.base.repository !== base.context.platform.repository
954
- ) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1190
+ ) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
955
1191
  platform: base.context.platform, capabilities: base.context.capabilities,
956
1192
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
957
1193
  error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
958
- });
1194
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
959
1195
  pullRequest = beforeBind.value;
960
1196
  const refreshed = resolvedContext(taskRef, options);
961
- if (!refreshed.ok) return result('failed', base.resolved.taskId, base.issueNumber, null, {
1197
+ if (!refreshed.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
962
1198
  pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
963
- });
964
- const bound = bindIdentity(refreshed, pullRequest, options.agent);
965
- if (bound.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, null, {
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, {
966
1202
  platform: base.context.platform, capabilities: base.context.capabilities,
967
1203
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
968
1204
  operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
969
1205
  error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
970
- });
971
- return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1206
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
1207
+ return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
972
1208
  changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
973
1209
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
974
1210
  operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
975
1211
  result: created ? 'pr_created' : 'pr_reused', error: null
976
- });
1212
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
977
1213
  }
978
1214
 
979
1215
  function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
980
1216
  const base = resolvedContext(taskRef, options);
981
- if (!base.ok) return base.output;
1217
+ if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
982
1218
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
983
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1219
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
984
1220
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
985
- });
1221
+ }), PRECONDITION_NOT_CREATED);
986
1222
  }
1223
+ const phase = { value: 'before-post' as CreatePhase };
987
1224
  try {
988
1225
  return withTaskExecutionLock(
989
1226
  base.resolved.repoRoot,
990
1227
  base.resolved.taskId,
991
1228
  'platform-pr.create',
992
- () => createPlatformPullRequestUnlocked(taskRef, options)
1229
+ () => createPlatformPullRequestUnlocked(taskRef, { ...options, phase })
993
1230
  );
994
1231
  } catch (error) {
995
1232
  if (error instanceof TaskExecutionLockError) {
996
- return result('blocked', base.resolved.taskId, base.issueNumber, null, {
1233
+ return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
997
1234
  platform: base.context.platform,
998
1235
  capabilities: base.context.capabilities,
999
1236
  error: { code: error.code, message: error.message, retryable: true }
1000
- });
1237
+ }), PRECONDITION_NOT_CREATED);
1001
1238
  }
1002
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
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, {
1003
1245
  platform: base.context.platform,
1004
1246
  capabilities: base.context.capabilities,
1005
1247
  error: {
1006
- code: 'PR_CREATE_FAILED',
1248
+ code: phase.value === 'post-dispatched' ? 'PR_CREATE_OUTCOME_UNKNOWN' : 'PR_CREATE_FAILED',
1007
1249
  message: error instanceof Error ? error.message : String(error),
1008
- retryable: false
1250
+ retryable: phase.value === 'post-dispatched'
1009
1251
  }
1010
- });
1252
+ }), creation);
1011
1253
  }
1012
1254
  }
1013
1255
 
@@ -1031,8 +1273,11 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
1031
1273
  };
1032
1274
  const base = resolvedContext(taskRef, options);
1033
1275
  if (!base.ok) return softenFailure(base.output);
1276
+ if (!base.fact) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1277
+ 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
+ }));
1034
1279
  if (!base.prNumber) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1035
- error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
1280
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
1036
1281
  }));
1037
1282
  const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, base.prNumber, base.resolved.repoRoot);
1038
1283
  if (!fetched.ok) return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
@@ -1049,7 +1294,7 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
1049
1294
  const planned = planPullRequestMetadata({
1050
1295
  pullRequest: fetched.value,
1051
1296
  issue: issue.issue,
1052
- taskType: base.frontmatter.type || 'task',
1297
+ taskType: String(base.frontmatter.type || 'task'),
1053
1298
  issueNumber: base.issueNumber,
1054
1299
  capabilities: base.context.capabilities
1055
1300
  }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
@@ -1088,6 +1333,98 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
1088
1333
  }));
1089
1334
  }
1090
1335
 
1336
+ function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
1337
+ try {
1338
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { prFlow?: unknown };
1339
+ return config.prFlow === 'required' || config.prFlow === 'disabled' ? config.prFlow : undefined;
1340
+ } catch {
1341
+ return undefined;
1342
+ }
1343
+ }
1344
+
1345
+ function skipPlatformPullRequestFactUnlocked(
1346
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
1347
+ options: SkipFactOptions
1348
+ ): PullRequestResult {
1349
+ let content: string;
1350
+ let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
1351
+ try {
1352
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
1353
+ frontmatter = parseTypedTaskFrontmatter(content);
1354
+ } catch (error) {
1355
+ return result('failed', resolved.taskId, null, null, {
1356
+ error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1357
+ });
1358
+ }
1359
+ const issue = Number(frontmatter.issue_number);
1360
+ const issueNumber = Number.isSafeInteger(issue) && issue > 0 ? issue : null;
1361
+ const existing = readPrDeliveryFact(frontmatter);
1362
+ if (existing.status === 'invalid') return result('failed', resolved.taskId, issueNumber, null, {
1363
+ error: { code: 'PR_DELIVERY_FACT_INVALID', message: existing.error.message, retryable: false }
1364
+ });
1365
+ if (existing.status === 'missing') return result('failed', resolved.taskId, issueNumber, null, {
1366
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1367
+ });
1368
+ if (existing.fact.state === 'skipped') return result('no-op', resolved.taskId, issueNumber, null, {
1369
+ operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
1370
+ error: null
1371
+ });
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
+ });
1375
+ if (readProjectPrFlow(resolved.repoRoot) === 'required') return result('failed', resolved.taskId, issueNumber, null, {
1376
+ error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
1377
+ });
1378
+
1379
+ const metadata = captureTaskWriteMetadata();
1380
+ const fact = buildSkippedFact(factTimestamp(metadata.timestamp));
1381
+ const write = writeTask({
1382
+ taskRef: resolved.taskId,
1383
+ expectedState: 'active',
1384
+ dryRun: options.dryRun,
1385
+ mutations: [
1386
+ { kind: 'frontmatter', set: { ...factFrontmatterMutation(fact).set, assigned_to: options.agent } },
1387
+ { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
1388
+ content, `- ${metadata.timestamp} — **Skip PR** by ${options.agent} — PR delivery explicitly skipped`
1389
+ ) }
1390
+ ]
1391
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
1392
+ if (write.status === 'failed') return result('failed', resolved.taskId, issueNumber, null, {
1393
+ error: { code: write.error.code, message: write.error.message, retryable: false }
1394
+ });
1395
+ const status = options.dryRun ? 'planned' : write.status === 'no-op' ? 'no-op' : 'applied';
1396
+ return result(status, resolved.taskId, issueNumber, null, {
1397
+ changed: !options.dryRun && write.changed,
1398
+ operations: [{ name: 'task:skip-pr', status, reasonCode: null }],
1399
+ error: null
1400
+ });
1401
+ }
1402
+
1403
+ function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOptions): PullRequestResult {
1404
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
1405
+ if (!resolved.ok) return result('failed', resolved.taskId, null, null, {
1406
+ error: { code: resolved.code, message: resolved.message, retryable: false }
1407
+ });
1408
+ if (resolved.state !== 'active') return result('failed', resolved.taskId, null, null, {
1409
+ error: { code: 'TASK_STATE_INVALID', message: 'platform-pr skip requires an active task', retryable: false }
1410
+ });
1411
+ try {
1412
+ return withTaskExecutionLock(
1413
+ resolved.repoRoot,
1414
+ resolved.taskId,
1415
+ 'platform-pr.skip',
1416
+ () => skipPlatformPullRequestFactUnlocked(resolved, options)
1417
+ );
1418
+ } catch (error) {
1419
+ if (error instanceof TaskExecutionLockError) return result('blocked', resolved.taskId, null, null, {
1420
+ error: { code: error.code, message: error.message, retryable: true }
1421
+ });
1422
+ return result('failed', resolved.taskId, null, null, {
1423
+ error: { code: 'PR_SKIP_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
1424
+ });
1425
+ }
1426
+ }
1427
+
1091
1428
  export {
1092
1429
  bindPlatformPullRequest,
1093
1430
  createPlatformPullRequest,
@@ -1098,9 +1435,10 @@ export {
1098
1435
  normalizePullRequest,
1099
1436
  resolveGitHubChangeRequestGitEvidence,
1100
1437
  resolveExternalPullRequest,
1438
+ skipPlatformPullRequestFact,
1101
1439
  selectExternalPullRequest,
1102
1440
  selectPullRequest,
1103
1441
  syncPlatformPullRequest
1104
1442
  };
1105
- export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
1443
+ export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
1106
1444
  export { warningResultForPrimary };