@fitlab-ai/agent-infra 0.12.0 → 0.12.2

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 (331) hide show
  1. package/README.md +0 -2
  2. package/README.zh-CN.md +0 -2
  3. package/dist/bin/internal-cli.js +2 -17
  4. package/dist/lib/agent-clients/adapter.js +0 -13
  5. package/dist/lib/agent-clients/adapters/antigravity-cli.js +0 -5
  6. package/dist/lib/agent-clients/adapters/claude-code-credentials.js +0 -17
  7. package/dist/lib/agent-clients/adapters/claude-code.js +0 -9
  8. package/dist/lib/agent-clients/adapters/codex.js +0 -52
  9. package/dist/lib/agent-clients/adapters/opencode.js +0 -5
  10. package/dist/lib/agent-clients/adapters/traecli.js +0 -5
  11. package/dist/lib/agent-clients/config.js +19 -54
  12. package/dist/lib/agent-clients/registry.js +1 -12
  13. package/dist/lib/agent-clients/schema.js +0 -18
  14. package/dist/lib/agent-clients/tokens.js +1 -2
  15. package/dist/lib/agent-clients/types.js +0 -1
  16. package/dist/lib/decide.js +2 -0
  17. package/dist/lib/git/worktree-identity.js +1 -9
  18. package/dist/lib/internal/agent-client.js +2 -62
  19. package/dist/lib/internal/cli-route-inventory.js +1 -5
  20. package/dist/lib/internal/manual-validation.js +29 -27
  21. package/dist/lib/internal/platform-comment.js +56 -7
  22. package/dist/lib/internal/task-create.js +1 -1
  23. package/dist/lib/internal/task-event.js +1 -8
  24. package/dist/lib/internal/task-finalization.js +0 -1
  25. package/dist/lib/internal/task-lifecycle.js +2 -2
  26. package/dist/lib/internal/task-operation-registry.js +4 -20
  27. package/dist/lib/internal/task-review.js +4 -4
  28. package/dist/lib/platform/comment-safety.js +1 -7
  29. package/dist/lib/platform/in-label-sync.js +1 -14
  30. package/dist/lib/platform/issue-comments.js +373 -282
  31. package/dist/lib/platform/issue-summary-content.js +112 -0
  32. package/dist/lib/platform/issues.js +1 -1
  33. package/dist/lib/platform/operation-coordinator.js +4 -2
  34. package/dist/lib/platform/pr-change-report.js +176 -30
  35. package/dist/lib/platform/pr-checks.js +2 -8
  36. package/dist/lib/platform/pr-summary-content.js +54 -0
  37. package/dist/lib/platform/pr-summary.js +318 -284
  38. package/dist/lib/platform/provider-validation.js +0 -20
  39. package/dist/lib/platform/pull-requests.js +73 -84
  40. package/dist/lib/platform/releases.js +1 -43
  41. package/dist/lib/platform/summary-content-pipeline.js +18 -0
  42. package/dist/lib/platform/verification-required.js +0 -2
  43. package/dist/lib/platform/verification-sync.js +1 -79
  44. package/dist/lib/process-data/normalize.js +1 -185
  45. package/dist/lib/process-data/privacy.js +1 -16
  46. package/dist/lib/process-data/sources.js +2 -279
  47. package/dist/lib/render.js +0 -12
  48. package/dist/lib/run/index.js +15 -10
  49. package/dist/lib/run/skills.js +0 -3
  50. package/dist/lib/sandbox/commands/create.js +40 -53
  51. package/dist/lib/sandbox/commands/list-running.js +0 -18
  52. package/dist/lib/sandbox/commands/start.js +7 -8
  53. package/dist/lib/sandbox/constants.js +0 -6
  54. package/dist/lib/sandbox/control/client.js +0 -1
  55. package/dist/lib/sandbox/control/container-identity.js +1 -16
  56. package/dist/lib/sandbox/control/executor.js +0 -4
  57. package/dist/lib/sandbox/control/identity-sentinel.js +0 -7
  58. package/dist/lib/sandbox/control/lifecycle.js +21 -37
  59. package/dist/lib/sandbox/control/native-file-lock.js +0 -3
  60. package/dist/lib/sandbox/control/server.js +18 -38
  61. package/dist/lib/sandbox/control/state.js +1 -9
  62. package/dist/lib/sandbox/dockerfile.js +0 -6
  63. package/dist/lib/sandbox/engine.js +0 -14
  64. package/dist/lib/sandbox/engines/docker-exact-discovery.js +1 -4
  65. package/dist/lib/sandbox/engines/wsl2-paths.js +0 -3
  66. package/dist/lib/sandbox/recovery.js +24 -26
  67. package/dist/lib/sandbox/removal.js +67 -518
  68. package/dist/lib/sandbox/task-resolver.js +0 -41
  69. package/dist/lib/sandbox/tools.js +1 -4
  70. package/dist/lib/sandbox/workspace-view.js +1 -2
  71. package/dist/lib/server/display.js +0 -6
  72. package/dist/lib/task/activity-intent.js +45 -40
  73. package/dist/lib/task/activity-log.js +9 -16
  74. package/dist/lib/task/artifact-command.js +1 -1
  75. package/dist/lib/task/artifact-lifecycle.js +61 -63
  76. package/dist/lib/task/artifact-name.js +0 -6
  77. package/dist/lib/task/artifact-operations.js +3 -3
  78. package/dist/lib/task/capabilities.js +4 -5
  79. package/dist/lib/task/commands/log.js +0 -6
  80. package/dist/lib/task/commands/status.js +17 -60
  81. package/dist/lib/task/commit-operation.js +28 -39
  82. package/dist/lib/task/completion-facts.js +56 -43
  83. package/dist/lib/task/control-authority.js +23 -256
  84. package/dist/lib/task/control-recovery.js +3 -5
  85. package/dist/lib/task/create.js +2 -4
  86. package/dist/lib/task/decision-intents.js +8 -9
  87. package/dist/lib/task/delivery-target.js +1 -40
  88. package/dist/lib/task/events.js +84 -107
  89. package/dist/lib/task/finalization.js +60 -15
  90. package/dist/lib/task/ledger-intents.js +1 -1
  91. package/dist/lib/task/lifecycle-execution.js +38 -33
  92. package/dist/lib/task/lifecycle.js +49 -41
  93. package/dist/lib/task/local-artifact-finalization.js +1 -4
  94. package/dist/lib/task/manual-validation-evidence.js +1 -37
  95. package/dist/lib/task/platform-operation-recovery.js +151 -108
  96. package/dist/lib/task/pr-delivery-fact.js +2 -5
  97. package/dist/lib/task/qualification-confirmation.js +5 -7
  98. package/dist/lib/task/resolve-ref.js +1 -25
  99. package/dist/lib/task/review-command.js +2 -2
  100. package/dist/lib/task/review-finalization.js +1 -21
  101. package/dist/lib/task/review-fingerprint.js +0 -4
  102. package/dist/lib/task/secure-fs.js +0 -56
  103. package/dist/lib/task/short-id.js +1 -39
  104. package/dist/lib/task/skill-scope.js +0 -1
  105. package/dist/lib/task/verification-engine.js +58 -282
  106. package/dist/lib/task/verification.js +6 -3
  107. package/dist/lib/task/write.js +11 -1
  108. package/lib/agent-clients/adapter.ts +0 -76
  109. package/lib/agent-clients/adapters/antigravity-cli.ts +0 -5
  110. package/lib/agent-clients/adapters/claude-code-credentials.ts +0 -19
  111. package/lib/agent-clients/adapters/claude-code.ts +0 -9
  112. package/lib/agent-clients/adapters/codex.ts +0 -56
  113. package/lib/agent-clients/adapters/opencode.ts +0 -5
  114. package/lib/agent-clients/adapters/traecli.ts +0 -5
  115. package/lib/agent-clients/config.ts +20 -61
  116. package/lib/agent-clients/registry.ts +1 -34
  117. package/lib/agent-clients/schema.ts +0 -18
  118. package/lib/agent-clients/tokens.ts +1 -2
  119. package/lib/agent-clients/types.ts +0 -15
  120. package/lib/decide.ts +1 -0
  121. package/lib/git/worktree-identity.ts +0 -10
  122. package/lib/internal/agent-client.ts +2 -70
  123. package/lib/internal/cli-route-inventory.ts +1 -4
  124. package/lib/internal/manual-validation.ts +28 -27
  125. package/lib/internal/platform-comment.ts +46 -4
  126. package/lib/internal/task-create.ts +0 -1
  127. package/lib/internal/task-event.ts +1 -5
  128. package/lib/internal/task-finalization.ts +0 -1
  129. package/lib/internal/task-lifecycle.ts +3 -5
  130. package/lib/internal/task-operation-registry.ts +3 -19
  131. package/lib/internal/task-review.ts +4 -4
  132. package/lib/platform/comment-safety.ts +0 -10
  133. package/lib/platform/github-provider.ts +0 -2
  134. package/lib/platform/in-label-sync.ts +0 -16
  135. package/lib/platform/issue-comments.ts +478 -301
  136. package/lib/platform/issue-summary-content.ts +132 -0
  137. package/lib/platform/issues.ts +0 -2
  138. package/lib/platform/operation-coordinator.ts +4 -2
  139. package/lib/platform/pr-change-report.ts +165 -25
  140. package/lib/platform/pr-checks.ts +1 -10
  141. package/lib/platform/pr-review.ts +1 -1
  142. package/lib/platform/pr-summary-content.ts +60 -0
  143. package/lib/platform/pr-summary.ts +364 -257
  144. package/lib/platform/provider-bridge.ts +0 -1
  145. package/lib/platform/provider-loader.ts +1 -5
  146. package/lib/platform/provider-validation.ts +1 -49
  147. package/lib/platform/pull-requests.ts +64 -99
  148. package/lib/platform/releases.ts +1 -40
  149. package/lib/platform/summary-content-pipeline.ts +23 -0
  150. package/lib/platform/verification-required.ts +0 -2
  151. package/lib/platform/verification-sync.ts +0 -94
  152. package/lib/process-data/normalize.ts +1 -186
  153. package/lib/process-data/privacy.ts +2 -19
  154. package/lib/process-data/sources.ts +3 -283
  155. package/lib/render.ts +0 -15
  156. package/lib/run/index.ts +13 -7
  157. package/lib/run/skills.ts +0 -4
  158. package/lib/sandbox/commands/create.ts +37 -74
  159. package/lib/sandbox/commands/list-running.ts +0 -19
  160. package/lib/sandbox/commands/start.ts +7 -10
  161. package/lib/sandbox/constants.ts +0 -8
  162. package/lib/sandbox/control/client.ts +0 -1
  163. package/lib/sandbox/control/container-identity.ts +1 -16
  164. package/lib/sandbox/control/executor.ts +1 -5
  165. package/lib/sandbox/control/identity-sentinel.ts +0 -8
  166. package/lib/sandbox/control/lifecycle.ts +18 -51
  167. package/lib/sandbox/control/native-file-lock.ts +0 -8
  168. package/lib/sandbox/control/protocol.ts +0 -2
  169. package/lib/sandbox/control/server.ts +13 -39
  170. package/lib/sandbox/control/state.ts +1 -10
  171. package/lib/sandbox/dockerfile.ts +0 -11
  172. package/lib/sandbox/engine.ts +0 -17
  173. package/lib/sandbox/engines/docker-exact-discovery.ts +1 -5
  174. package/lib/sandbox/engines/wsl2-paths.ts +0 -4
  175. package/lib/sandbox/recovery.ts +24 -26
  176. package/lib/sandbox/removal.ts +58 -687
  177. package/lib/sandbox/shell.ts +1 -1
  178. package/lib/sandbox/task-resolver.ts +0 -48
  179. package/lib/sandbox/tools.ts +1 -5
  180. package/lib/sandbox/workspace-view.ts +2 -4
  181. package/lib/server/display.ts +0 -14
  182. package/lib/server/process-control.ts +1 -1
  183. package/lib/task/activity-intent.ts +36 -44
  184. package/lib/task/activity-log.ts +9 -22
  185. package/lib/task/artifact-command.ts +1 -1
  186. package/lib/task/artifact-lifecycle.ts +58 -67
  187. package/lib/task/artifact-name.ts +0 -7
  188. package/lib/task/artifact-operations.ts +3 -3
  189. package/lib/task/capabilities.ts +5 -6
  190. package/lib/task/commands/log.ts +0 -7
  191. package/lib/task/commands/status.ts +14 -79
  192. package/lib/task/commit-operation.ts +29 -51
  193. package/lib/task/completion-facts.ts +45 -42
  194. package/lib/task/control-authority.ts +27 -317
  195. package/lib/task/control-recovery.ts +2 -5
  196. package/lib/task/create.ts +2 -5
  197. package/lib/task/decision-intents.ts +5 -5
  198. package/lib/task/delivery-target.ts +0 -40
  199. package/lib/task/events.ts +89 -123
  200. package/lib/task/finalization.ts +52 -15
  201. package/lib/task/ledger-intents.ts +1 -1
  202. package/lib/task/lifecycle-execution.ts +37 -89
  203. package/lib/task/lifecycle.ts +48 -44
  204. package/lib/task/local-artifact-finalization.ts +0 -7
  205. package/lib/task/manual-validation-evidence.ts +1 -47
  206. package/lib/task/platform-operation-recovery.ts +117 -104
  207. package/lib/task/pr-delivery-fact.ts +2 -11
  208. package/lib/task/qualification-confirmation.ts +6 -6
  209. package/lib/task/resolve-ref.ts +0 -23
  210. package/lib/task/review-command.ts +2 -2
  211. package/lib/task/review-finalization.ts +1 -27
  212. package/lib/task/review-fingerprint.ts +0 -8
  213. package/lib/task/secure-fs.ts +0 -42
  214. package/lib/task/short-id.ts +0 -43
  215. package/lib/task/skill-scope.ts +0 -1
  216. package/lib/task/verification-engine.ts +56 -287
  217. package/lib/task/verification.ts +5 -5
  218. package/lib/task/write.ts +12 -1
  219. package/package.json +2 -2
  220. package/templates/.agents/README.en.md +1 -3
  221. package/templates/.agents/README.zh-CN.md +0 -2
  222. package/templates/.agents/rules/README.en.md +0 -1
  223. package/templates/.agents/rules/README.zh-CN.md +0 -1
  224. package/templates/.agents/rules/commit-and-pr.en.md +1 -1
  225. package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -1
  226. package/templates/.agents/rules/no-mid-flow-questions.en.md +0 -10
  227. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +0 -10
  228. package/templates/.agents/rules/pr-sync.en.md +4 -2
  229. package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
  230. package/templates/.agents/rules/task-management.en.md +3 -3
  231. package/templates/.agents/rules/task-management.zh-CN.md +3 -3
  232. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  233. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  234. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  235. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  236. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  237. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  238. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  239. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  240. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +6 -6
  241. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +6 -6
  242. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  243. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  244. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
  245. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
  246. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
  247. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
  248. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  249. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  250. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
  251. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
  252. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  253. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  254. package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
  255. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
  256. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  257. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  258. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  259. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  260. package/templates/.agents/skills/review-code/SKILL.en.md +5 -5
  261. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -5
  262. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  263. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  264. package/templates/.agents/skills/review-pr/SKILL.en.md +4 -4
  265. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +4 -4
  266. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  267. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  268. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +3 -75
  269. package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
  270. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  271. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
  272. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
  273. package/templates/.agents/templates/task.en.md +1 -0
  274. package/templates/.agents/templates/task.zh-CN.md +1 -0
  275. package/templates/.claude/settings.json +0 -24
  276. package/templates/.codex/hooks.json +0 -50
  277. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +0 -582
  278. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +0 -114
  279. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +0 -237
  280. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +0 -443
  281. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +0 -236
  282. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +0 -292
  283. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +0 -267
  284. package/dist/lib/agent-clients/adapters/codex-orchestration.js +0 -466
  285. package/dist/lib/internal/task-orchestration.js +0 -71
  286. package/dist/lib/platform/verification.js +0 -6
  287. package/dist/lib/run/prompt.js +0 -2
  288. package/dist/lib/sandbox/engines/authority.js +0 -289
  289. package/dist/lib/task/delegation-receipts.js +0 -410
  290. package/dist/lib/task/orchestration.js +0 -1233
  291. package/dist/lib/task/verification-checks.js +0 -9
  292. package/dist/lib/task/workspace-snapshot.js +0 -121
  293. package/dist/lifecycle-build-manifest.json +0 -6
  294. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +0 -688
  295. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +0 -147
  296. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +0 -349
  297. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +0 -506
  298. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +0 -385
  299. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -41
  300. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +0 -418
  301. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +0 -354
  302. package/lib/agent-clients/adapters/codex-orchestration.ts +0 -630
  303. package/lib/internal/task-orchestration.ts +0 -74
  304. package/lib/platform/verification.ts +0 -11
  305. package/lib/run/prompt.ts +0 -1
  306. package/lib/sandbox/engines/authority.ts +0 -365
  307. package/lib/task/delegation-receipts.ts +0 -547
  308. package/lib/task/orchestration.ts +0 -1651
  309. package/lib/task/verification-checks.ts +0 -10
  310. package/lib/task/workspace-snapshot.ts +0 -144
  311. package/templates/.agents/hooks/lifecycle-delegation.js +0 -140
  312. package/templates/.agents/rules/lifecycle-orchestration.en.md +0 -44
  313. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +0 -44
  314. package/templates/.agents/skills/run-task/SKILL.en.md +0 -29
  315. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +0 -29
  316. package/templates/.agents/skills/run-task/config/verify.en.json +0 -10
  317. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +0 -10
  318. package/templates/.agents/skills/run-task/reference/host-validation.en.md +0 -24
  319. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +0 -24
  320. package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +0 -6
  321. package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +0 -6
  322. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +0 -6
  323. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +0 -6
  324. package/templates/.claude/commands/run-task.en.md +0 -8
  325. package/templates/.claude/commands/run-task.zh-CN.md +0 -8
  326. package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +0 -3
  327. package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +0 -3
  328. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +0 -3
  329. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +0 -3
  330. package/templates/.opencode/commands/run-task.en.md +0 -11
  331. package/templates/.opencode/commands/run-task.zh-CN.md +0 -11
@@ -16,6 +16,16 @@ import { canonicalizeCommentBody, escapeHtmlText, fenceRanges, renderSafeCodeFen
16
16
  function isRetryable(error) {
17
17
  return error.retryable ?? providerErrorRetryable(error.code);
18
18
  }
19
+ async function syncCommentPlan(strategy, operation, queuedOperation) {
20
+ const execute = () => executeCommentPlan(strategy, strategy.client);
21
+ if (strategy.dryRun)
22
+ return execute();
23
+ if (queuedOperation)
24
+ return execute();
25
+ return coordinatePlatformWrite({ operation, agent: strategy.agent, client: strategy.client, execute,
26
+ block: (error) => platformResult('blocked', { error }),
27
+ persistenceFailure: (error) => platformResult('failed', { error }) });
28
+ }
19
29
  function providerCommentId(id, provider) {
20
30
  return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
21
31
  }
@@ -91,9 +101,6 @@ function renderTaskCommentResult(content, taskId, language = '') {
91
101
  function renderTaskComment(content, taskId, _agent, language = '') {
92
102
  return renderTaskCommentResult(content, taskId, language).body;
93
103
  }
94
- function isTaskCommentTooLarge(content, taskId = 'TASK-UNKNOWN', _agent = 'codex') {
95
- return renderTaskCommentResult(content, taskId).byteLength > COMMENT_BYTE_LIMIT;
96
- }
97
104
  function taskCommentLanguage(repoRoot) {
98
105
  try {
99
106
  const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
@@ -291,18 +298,6 @@ function chunkArtifactComment(input) {
291
298
  function findMarkerComments(comments, marker) {
292
299
  return comments.filter((comment) => normalizeCommentContent(String(comment.body || '')).split('\n', 1)[0] === marker);
293
300
  }
294
- function flattenComments(value) {
295
- if (!Array.isArray(value))
296
- return [];
297
- const flattened = value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
298
- return flattened.filter((entry) => Boolean(entry && typeof entry === 'object' && (typeof entry.id === 'number' || typeof entry.id === 'string') && typeof entry.body === 'string'));
299
- }
300
- function listRemoteComments(client, repo, issue, cwd) {
301
- const result = client.json([
302
- 'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
303
- ], { cwd });
304
- return result.ok ? { ok: true, value: flattenComments(result.value) } : result;
305
- }
306
301
  function contextFields(context) {
307
302
  return {
308
303
  platform: context.platform,
@@ -378,38 +373,285 @@ function relatedComments(comments, prefix) {
378
373
  return first === base || (first.startsWith(chunkNamespace) && first.endsWith(' -->'));
379
374
  });
380
375
  }
381
- function taskManagedComment(comment, taskId) {
382
- const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
383
- return marker === MARKERS.task(taskId)
384
- || marker === MARKERS.summary(taskId)
385
- || marker.startsWith(`<!-- sync-issue:${taskId}:`);
386
- }
387
- function summaryPosition(comments, taskId) {
388
- const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
389
- const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
390
- if (summary.length !== 1)
391
- return { ok: false, known: false, summary: null };
392
- if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
393
- return { ok: false, known: false, summary: summary[0] };
394
- }
395
- const sequence = summary[0].createdSequence;
396
- return { ok: managed.every((comment) => sequence >= comment.createdSequence), known: true, summary: summary[0] };
397
- }
398
376
  async function listedComments(provider, loaded, parent) {
399
377
  return provider.comments?.list
400
378
  ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response) => response.ok
401
- ? {
402
- ok: true,
403
- value: response.value.map((comment) => ({
404
- id: providerCommentId(comment.id, provider),
405
- body: comment.body,
406
- user: comment.author?.name ? { login: comment.author.name } : undefined,
407
- createdSequence: comment.createdSequence
408
- }))
409
- }
410
- : response)
379
+ ? { ok: true, value: response.value.map((comment) => parentComment(comment, provider)) }
380
+ : { ...response, error: { ...response.error, retryable: isRetryable(response.error) } })
411
381
  : unsupportedProviderOperation(provider, 'comments.list');
412
382
  }
383
+ function markerSetError(strategy, context, resource, existing) {
384
+ if (validateRelatedMarkerSet(existing, strategy.markerPrefix).ok)
385
+ return null;
386
+ return platformResult('failed', { ...contextFields(context), resource, error: {
387
+ code: strategy.parentKind === 'pull-request' ? 'PR_SUMMARY_MARKER_AMBIGUOUS' : 'COMMENT_MARKER_CONFLICT',
388
+ message: strategy.parentKind === 'pull-request' ? 'Multiple PR comments contain the summary marker' : 'Multiple comments use the same registered marker', retryable: false
389
+ } });
390
+ }
391
+ function taskCommentOwnerError(strategy, context, resource, existing) {
392
+ if (strategy.kind !== 'task' || existing.length !== 1)
393
+ return null;
394
+ const owner = existing[0].user?.login;
395
+ return owner && context.platform.currentUser && owner === context.platform.currentUser ? null : platformResult('blocked', {
396
+ ...contextFields(context), resource, error: {
397
+ code: 'COMMENT_OWNER_CONFLICT', message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable', retryable: false
398
+ }
399
+ });
400
+ }
401
+ async function strategyValidationError(strategy, context, resource) {
402
+ const error = strategy.validate ? await strategy.validate() : null;
403
+ if (!error)
404
+ return null;
405
+ return platformResult(error.retryable ? 'blocked' : 'failed', { ...contextFields(context), resource, error });
406
+ }
407
+ function artifactBackfillResult(strategy, context, resource, existing, desired) {
408
+ if (!strategy.backfill || strategy.kind !== 'artifact' || existing.length === 0)
409
+ return null;
410
+ const operations = existing.map((comment) => ({
411
+ name: `comment:${normalizeCommentContent(comment.body).split('\n', 1)[0]}`,
412
+ status: 'no-op',
413
+ reasonCode: 'BACKFILL_ALREADY_PRESENT'
414
+ }));
415
+ return platformResult('no-op', {
416
+ ...contextFields(context),
417
+ resource,
418
+ operations,
419
+ comment: {
420
+ kind: strategy.kind,
421
+ marker: desired[0]?.marker ?? strategy.markerPrefix,
422
+ ids: existing.map((item) => item.id),
423
+ parts: existing.length
424
+ },
425
+ error: null
426
+ });
427
+ }
428
+ function repositionAuthorizationError(strategy, context, resource, comments, byMarker, desired) {
429
+ const marker = desired[0]?.marker;
430
+ const current = marker ? byMarker.get(marker) : undefined;
431
+ const position = strategy.summaryPosition && current
432
+ ? issueSummaryPosition(strategy.taskId, comments, marker)
433
+ : true;
434
+ if (position === null)
435
+ return {
436
+ error: platformResult('blocked', { ...contextFields(context), resource, error: {
437
+ code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true
438
+ } }),
439
+ reposition: false
440
+ };
441
+ const reposition = Boolean(strategy.summaryPosition && current && !position);
442
+ if (!reposition || !current)
443
+ return { error: null, reposition: false };
444
+ const error = strategy.authorizeReposition
445
+ ? strategy.authorizeReposition(current, context.platform.currentUser ?? null)
446
+ : { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current authorization and an owned summary comment', retryable: false };
447
+ return {
448
+ error: error ? platformResult(error.retryable ? 'blocked' : 'failed', { ...contextFields(context), resource, error }) : null,
449
+ reposition: true
450
+ };
451
+ }
452
+ async function inspectCommentPlan(strategy, context, resource, comments, desired) {
453
+ const existing = relatedComments(comments, strategy.markerPrefix);
454
+ const markerError = markerSetError(strategy, context, resource, existing);
455
+ if (markerError)
456
+ return { ok: false, result: markerError };
457
+ const ownerError = taskCommentOwnerError(strategy, context, resource, existing);
458
+ if (ownerError)
459
+ return { ok: false, result: ownerError };
460
+ const validationError = await strategyValidationError(strategy, context, resource);
461
+ if (validationError)
462
+ return { ok: false, result: validationError };
463
+ const backfill = artifactBackfillResult(strategy, context, resource, existing, desired);
464
+ if (backfill)
465
+ return { ok: true, value: { existing, byMarker: new Map(), reposition: false }, result: backfill };
466
+ const byMarker = new Map(existing.map((comment) => [normalizeCommentContent(comment.body).split('\n', 1)[0], comment]));
467
+ const reposition = repositionAuthorizationError(strategy, context, resource, comments, byMarker, desired);
468
+ if (reposition.error)
469
+ return { ok: false, result: reposition.error };
470
+ return { ok: true, value: { existing, byMarker, reposition: reposition.reposition } };
471
+ }
472
+ async function repositionSummaryComment(strategy, loaded, context, parent, resource, current, operations) {
473
+ if (!current)
474
+ return null;
475
+ const deleted = loaded.provider.comments?.delete
476
+ ? await loaded.provider.comments.delete({ context: providerOperationContext(loaded), parent,
477
+ comment: providerResourceToken(loaded.provider, 'comment', String(current.id)),
478
+ mutation: { idempotencyKey: `comment-reposition-delete:${strategy.taskId}:${current.id}` } })
479
+ : unsupportedProviderOperation(loaded.provider, 'comments.delete');
480
+ if (!deleted.ok)
481
+ return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
482
+ ...contextFields(context), resource, error: deleted.error
483
+ });
484
+ operations.push({
485
+ name: `comment:${current.id}`,
486
+ status: 'applied',
487
+ reasonCode: 'SUMMARY_REPOSITIONED'
488
+ });
489
+ return null;
490
+ }
491
+ async function observeCommentWrite(strategy, loaded, parent, item) {
492
+ const after = await listedComments(loaded.provider, loaded, parent);
493
+ const comments = after.ok ? after.value : [];
494
+ const found = comments.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
495
+ const positionOk = !strategy.summaryPosition || issueSummaryPosition(strategy.taskId, comments, item.marker) === true;
496
+ const validation = strategy.validate ? await strategy.validate() : null;
497
+ if (after.ok
498
+ && found.length === 1
499
+ && normalizeCommentContent(found[0].body) === normalizeCommentContent(item.body)
500
+ && positionOk
501
+ && !validation)
502
+ return { valid: true };
503
+ const error = validation ?? {
504
+ code: strategy.kind === 'summary' || strategy.kind === 'cancel' ? 'SUMMARY_BODY_READBACK_FAILED' : 'COMMENT_READBACK_FAILED',
505
+ message: after.ok ? 'Remote comment did not match the requested body' : after.error.message,
506
+ retryable: after.ok || isRetryable(after.error)
507
+ };
508
+ return { valid: false, error };
509
+ }
510
+ async function verifyIssueSummaryReadback(strategy, loaded, context, parent, resource, desired, operations) {
511
+ if (strategy.parentKind !== 'issue' || (strategy.kind !== 'summary' && strategy.kind !== 'cancel'))
512
+ return null;
513
+ const after = await listedComments(loaded.provider, loaded, parent);
514
+ if (!after.ok)
515
+ return platformResult(isRetryable(after.error) ? 'blocked' : 'failed', {
516
+ ...contextFields(context), resource, operations, error: after.error
517
+ });
518
+ const item = desired[0];
519
+ const matching = item
520
+ ? after.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker)
521
+ : [];
522
+ if (!item || matching.length !== 1 || normalizeCommentContent(matching[0].body) !== normalizeCommentContent(item.body)) {
523
+ return platformResult('blocked', { ...contextFields(context), resource, operations, error: {
524
+ code: 'SUMMARY_BODY_READBACK_FAILED',
525
+ message: strategy.kind === 'cancel' ? 'Remote cancellation body did not match the rendered cancellation summary' : 'Remote summary body did not match the rendered delivery summary',
526
+ retryable: true
527
+ } });
528
+ }
529
+ if (strategy.summaryPosition && issueSummaryPosition(strategy.taskId, after.value, item.marker) !== true) {
530
+ return platformResult('blocked', { ...contextFields(context), resource, operations, error: {
531
+ code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true
532
+ } });
533
+ }
534
+ return null;
535
+ }
536
+ async function compensatePullRequestComment(strategy, loaded, parent, current, id, cause) {
537
+ const compensation = current
538
+ ? await loaded.provider.comments?.write?.({ context: providerOperationContext(loaded), parent, body: current.body,
539
+ existingComment: providerResourceToken(loaded.provider, 'comment', String(current.id)),
540
+ mutation: { idempotencyKey: `pr-summary-restore:${strategy.taskId}` } })
541
+ : await loaded.provider.comments?.delete?.({ context: providerOperationContext(loaded), parent,
542
+ comment: providerResourceToken(loaded.provider, 'comment', String(id)),
543
+ mutation: { idempotencyKey: `pr-summary-delete:${strategy.taskId}:${id}` } });
544
+ if (!compensation?.ok)
545
+ return {
546
+ code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
547
+ message: compensation?.error?.message ?? 'Provider does not support comment compensation',
548
+ retryable: true
549
+ };
550
+ if (cause?.code === 'PR_SUMMARY_HEAD_RACE')
551
+ return cause;
552
+ return {
553
+ code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
554
+ message: cause?.message ?? 'Post-write summary verification failed',
555
+ retryable: true
556
+ };
557
+ }
558
+ async function verifyCommentWrite(strategy, loaded, parent, item, current, id) {
559
+ if (!strategy.verifyReadback || strategy.parentKind === 'issue')
560
+ return null;
561
+ const observed = await observeCommentWrite(strategy, loaded, parent, item);
562
+ if (observed.valid)
563
+ return null;
564
+ if (strategy.parentKind === 'pull-request') {
565
+ return compensatePullRequestComment(strategy, loaded, parent, current, id, observed.error);
566
+ }
567
+ return observed.error;
568
+ }
569
+ async function writeDesiredComments(strategy, loaded, context, parent, resource, desired, selection, operations) {
570
+ const ids = [];
571
+ for (const item of desired) {
572
+ const current = selection.reposition && item === desired[0] ? undefined : selection.byMarker.get(item.marker);
573
+ if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
574
+ ids.push(current.id);
575
+ operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
576
+ continue;
577
+ }
578
+ const written = loaded.provider.comments?.write
579
+ ? await loaded.provider.comments.write({ context: providerOperationContext(loaded), parent, body: item.body,
580
+ ...(current ? { existingComment: providerResourceToken(loaded.provider, 'comment', String(current.id)) } : {}),
581
+ mutation: { idempotencyKey: `comment:${strategy.taskId}:${item.marker}` } })
582
+ : unsupportedProviderOperation(loaded.provider, 'comments.write');
583
+ if (!written.ok)
584
+ return { ok: false, result: platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
585
+ ...contextFields(context), resource, operations, error: written.error
586
+ }) };
587
+ const id = providerCommentId(written.value.remoteId, loaded.provider);
588
+ ids.push(id);
589
+ operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
590
+ const error = await verifyCommentWrite(strategy, loaded, parent, item, current, id);
591
+ if (error)
592
+ return { ok: false, result: platformResult(error.retryable ? 'blocked' : 'failed', {
593
+ ...contextFields(context), resource, operations, error
594
+ }) };
595
+ }
596
+ return { ok: true, ids };
597
+ }
598
+ async function deleteStaleComments(strategy, loaded, context, parent, resource, existing, desired, operations) {
599
+ const markers = new Set(desired.map((item) => item.marker));
600
+ for (const stale of existing.filter((comment) => !markers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]))) {
601
+ const deleted = loaded.provider.comments?.delete
602
+ ? await loaded.provider.comments.delete({ context: providerOperationContext(loaded), parent,
603
+ comment: providerResourceToken(loaded.provider, 'comment', String(stale.id)),
604
+ mutation: { idempotencyKey: `comment-delete:${strategy.taskId}:${stale.id}` } })
605
+ : unsupportedProviderOperation(loaded.provider, 'comments.delete');
606
+ if (!deleted.ok)
607
+ return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
608
+ ...contextFields(context), resource, operations, error: deleted.error
609
+ });
610
+ operations.push({ name: `comment:${stale.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
611
+ }
612
+ return null;
613
+ }
614
+ async function prepareCommentExecution(strategy, client) {
615
+ const resolved = strategy.resolved
616
+ ? { ok: true, value: strategy.resolved.loaded }
617
+ : await resolvePlatformProviderContext({ cwd: strategy.cwd, client });
618
+ const loaded = resolved;
619
+ const context = strategy.resolved?.context ?? (loaded.ok ? loaded.value.context : loaded.context);
620
+ if (!loaded.ok || !hasResolvedPlatformContext(context))
621
+ return { ok: false, result: context };
622
+ let parent;
623
+ let desired;
624
+ try {
625
+ ({ parent, items: desired } = await strategy.prepare());
626
+ }
627
+ catch (error) {
628
+ const value = error;
629
+ return { ok: false, result: platformResult('failed', { ...contextFields(context), error: {
630
+ code: value.code || 'COMMENT_PAYLOAD_INVALID', message: value.message || String(error), retryable: false
631
+ } }) };
632
+ }
633
+ const resource = { kind: strategy.parentKind, number: resourceIdentityNumber(parent), identity: parent };
634
+ const listed = await listedComments(loaded.value.provider, loaded.value, parent);
635
+ if (!listed.ok)
636
+ return { ok: false, result: platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
637
+ ...contextFields(context), resource, error: listed.error
638
+ }) };
639
+ const selection = await inspectCommentPlan(strategy, context, resource, listed.value, desired);
640
+ if (!selection.ok)
641
+ return selection;
642
+ return {
643
+ ok: true,
644
+ value: {
645
+ loaded: loaded.value,
646
+ context,
647
+ parent,
648
+ desired,
649
+ resource,
650
+ selection: selection.value,
651
+ result: selection.result
652
+ }
653
+ };
654
+ }
413
655
  function validateRelatedMarkerSet(comments, prefix) {
414
656
  const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
415
657
  if (new Set(markers).size !== markers.length)
@@ -433,259 +675,108 @@ function validateRelatedMarkerSet(comments, prefix) {
433
675
  const complete = total > 0 && parts.length === total && parts.every((part, index) => part === index + 1);
434
676
  return complete ? { ok: true, code: null } : { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
435
677
  }
436
- function writeComment(client, repo, issue, cwd, body, id) {
437
- const endpoint = id ? `repos/${repo}/issues/comments/${id}` : `repos/${repo}/issues/${issue}/comments`;
438
- return client.json(['api', endpoint, '-X', id ? 'PATCH' : 'POST', '--input', '-'], { cwd, method: id ? 'PATCH' : 'POST', input: JSON.stringify({ body }) });
678
+ function parentComment(comment, provider) {
679
+ return {
680
+ id: providerCommentId(comment.id, provider), body: comment.body,
681
+ user: comment.author?.name ? { login: comment.author.name } : undefined,
682
+ createdSequence: comment.createdSequence
683
+ };
439
684
  }
440
- async function syncPlatformCommentImpl(taskRef, options) {
441
- const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
442
- if (!resolved.ok) {
443
- return platformResult('failed', {
444
- error: { code: resolved.code, message: resolved.message, retryable: false }
445
- });
446
- }
447
- const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
448
- let issueIdentityFromTask;
449
- try {
450
- issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent));
451
- }
452
- catch (error) {
453
- return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } });
454
- }
455
- if (!issueIdentityFromTask) {
456
- return platformResult('no-op', {
457
- error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
458
- });
459
- }
460
- let desired;
461
- try {
462
- const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
463
- if (options.kind === 'task' && options.taskProjection
464
- && createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
465
- throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
466
- }
467
- desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
468
- }
469
- catch (error) {
470
- return platformResult('failed', {
471
- resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
472
- error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
473
- });
474
- }
475
- if (options.kind === 'task' && Buffer.byteLength(desired[0].body, 'utf8') > COMMENT_BYTE_LIMIT) {
476
- return platformResult('failed', {
477
- resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
478
- operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
479
- error: { code: 'COMMENT_PAYLOAD_TOO_LARGE', message: 'Projected task comment exceeds the platform byte limit', retryable: false }
480
- });
481
- }
482
- const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
483
- const context = loaded.ok ? loaded.value.context : loaded.context;
484
- if (!hasResolvedPlatformContext(context) || !loaded.ok)
485
- return context;
486
- const issue = resourceIdentityNumber(issueIdentityFromTask);
487
- const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
488
- if (!listed.ok) {
489
- return platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
490
- ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
491
- });
492
- }
493
- const existing = relatedComments(listed.value, markerPrefix(resolved.taskId, options));
494
- if (!validateRelatedMarkerSet(existing, markerPrefix(resolved.taskId, options)).ok) {
495
- return platformResult('failed', {
496
- ...contextFields(context),
497
- resource: { kind: 'issue', number: issue },
498
- error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple comments use the same registered marker', retryable: false }
499
- });
500
- }
501
- if (options.kind === 'task' && existing.length === 1) {
502
- const owner = existing[0].user?.login;
503
- if (!owner || !context.platform.currentUser || owner !== context.platform.currentUser) {
504
- return platformResult('blocked', {
505
- ...contextFields(context),
506
- resource: { kind: 'issue', number: issue, identity: issueIdentityFromTask },
507
- error: {
508
- code: 'COMMENT_OWNER_CONFLICT',
509
- message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable',
510
- retryable: false
511
- }
512
- });
513
- }
514
- }
515
- // Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
516
- if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
517
- const operations = existing.map((comment) => ({
518
- name: `comment:${normalizeCommentContent(comment.body).split('\n', 1)[0]}`,
519
- status: 'no-op',
520
- reasonCode: 'BACKFILL_ALREADY_PRESENT'
521
- }));
522
- return platformResult('no-op', {
523
- ...contextFields(context),
524
- changed: false,
525
- resource: { kind: 'issue', number: issue },
526
- operations,
527
- comment: {
528
- kind: options.kind,
529
- marker: desired[0].marker,
530
- ids: existing.map((comment) => comment.id),
531
- parts: existing.length
532
- },
533
- error: null
685
+ function issueSummaryPosition(taskId, comments, marker) {
686
+ const managed = comments.filter((comment) => comment.body.split('\n', 1)[0]?.startsWith(`<!-- sync-issue:${taskId}:`));
687
+ const summary = managed.filter((comment) => comment.body.split('\n', 1)[0] === marker);
688
+ if (summary.length !== 1 || managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || comment.createdSequence === null))
689
+ return null;
690
+ return managed.every((comment) => summary[0].createdSequence >= comment.createdSequence);
691
+ }
692
+ async function executeCommentPlan(strategy, client) {
693
+ const prepared = await prepareCommentExecution(strategy, client);
694
+ if (!prepared.ok)
695
+ return prepared.result;
696
+ const { loaded, context, parent, desired, resource, selection, result } = prepared.value;
697
+ if (result)
698
+ return result;
699
+ if (strategy.dryRun)
700
+ return platformResult('planned', {
701
+ ...contextFields(context), resource,
702
+ operations: desired.map((item) => ({ name: `comment:${item.marker}`, status: 'planned', reasonCode: null })), error: null
534
703
  });
535
- }
536
704
  const operations = [];
537
- const ids = [];
538
- for (const item of desired) {
539
- const current = existing.find((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
540
- if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
541
- ids.push(current.id);
542
- operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
543
- continue;
544
- }
545
- const written = loaded.value.provider.comments?.write
546
- ? await loaded.value.provider.comments.write({
547
- context: providerOperationContext(loaded.value),
548
- parent: issueIdentityFromTask,
549
- body: item.body,
550
- ...(current ? { existingComment: { kind: 'id', value: String(current.id) } } : {}),
551
- mutation: { idempotencyKey: `comment:${resolved.taskId}:${item.marker}` }
552
- })
553
- : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
554
- if (!written.ok) {
555
- if (!current && isRetryable(written.error)) {
556
- // A provider owns reconciliation because only it knows how to address the resource.
557
- }
558
- return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
559
- ...contextFields(context),
560
- resource: { kind: 'issue', number: issue },
561
- operations,
562
- error: written.error
563
- });
564
- }
565
- ids.push(providerCommentId(written.value.remoteId, loaded.value.provider));
566
- operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
567
- }
568
- const desiredMarkers = new Set(desired.map((item) => item.marker));
569
- const stale = existing.filter((comment) => !desiredMarkers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]));
570
- for (const comment of stale) {
571
- const deleted = loaded.value.provider.comments?.delete
572
- ? await loaded.value.provider.comments.delete({
573
- context: providerOperationContext(loaded.value),
574
- parent: issueIdentityFromTask,
575
- comment: { kind: 'id', value: String(comment.id) },
576
- mutation: { idempotencyKey: `comment-delete:${resolved.taskId}:${comment.id}` }
577
- })
578
- : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
579
- if (!deleted.ok) {
580
- return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
581
- ...contextFields(context),
582
- resource: { kind: 'issue', number: issue },
583
- operations,
584
- error: deleted.error
585
- });
586
- }
587
- operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
588
- }
589
- if (options.kind === 'summary') {
590
- const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
591
- if (!refreshed.ok) {
592
- return platformResult(isRetryable(refreshed.error) ? 'blocked' : 'failed', {
593
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
594
- });
595
- }
596
- const position = summaryPosition(refreshed.value, resolved.taskId);
597
- if (!position.ok && !position.known) {
598
- return platformResult('blocked', {
599
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
600
- error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
601
- });
602
- }
603
- if (!position.ok && position.summary) {
604
- const owner = position.summary.user?.login;
605
- if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
606
- || !owner || owner !== context.platform.currentUser) {
607
- return platformResult('failed', {
608
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
609
- error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
610
- });
611
- }
612
- const deleted = loaded.value.provider.comments?.delete
613
- ? await loaded.value.provider.comments.delete({
614
- context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
615
- comment: { kind: 'id', value: String(position.summary.id) },
616
- mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
617
- })
618
- : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
619
- if (!deleted.ok) {
620
- return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
621
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
622
- });
623
- }
624
- const written = loaded.value.provider.comments?.write
625
- ? await loaded.value.provider.comments.write({
626
- context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0].body,
627
- mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
628
- })
629
- : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
630
- if (!written.ok) {
631
- return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
632
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
633
- });
634
- }
635
- ids.splice(0, ids.length, providerCommentId(written.value.remoteId, loaded.value.provider));
636
- operations.push({ name: `comment:${desired[0].marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
637
- const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
638
- if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
639
- const error = reconciled.ok
640
- ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
641
- : reconciled.error;
642
- return platformResult(isRetryable(error) ? 'blocked' : 'failed', {
643
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
644
- });
645
- }
646
- }
647
- }
705
+ const currentSummary = desired[0] ? selection.byMarker.get(desired[0].marker) : undefined;
706
+ if (selection.reposition) {
707
+ const repositioned = await repositionSummaryComment(strategy, loaded, context, parent, resource, currentSummary, operations);
708
+ if (repositioned)
709
+ return repositioned;
710
+ }
711
+ const written = await writeDesiredComments(strategy, loaded, context, parent, resource, desired, selection, operations);
712
+ if (!written.ok)
713
+ return written.result;
714
+ const deleted = await deleteStaleComments(strategy, loaded, context, parent, resource, selection.existing, desired, operations);
715
+ if (deleted)
716
+ return deleted;
717
+ const readback = await verifyIssueSummaryReadback(strategy, loaded, context, parent, resource, desired, operations);
718
+ if (readback)
719
+ return readback;
648
720
  const changed = operations.some((operation) => operation.status === 'applied');
649
- const result = platformResult(changed ? 'applied' : 'no-op', {
650
- ...contextFields(context),
651
- changed,
652
- resource: { kind: 'issue', number: issue },
653
- operations,
654
- comment: { kind: options.kind, marker: desired[0]?.marker ?? markerPrefix(resolved.taskId, options), ids, parts: desired.length },
721
+ return platformResult(changed ? 'applied' : 'no-op', {
722
+ ...contextFields(context), changed, resource, operations,
723
+ comment: {
724
+ kind: strategy.kind,
725
+ marker: desired[0]?.marker ?? strategy.markerPrefix,
726
+ ids: written.ids,
727
+ parts: desired.length
728
+ },
655
729
  error: null
656
730
  });
657
- return result;
658
731
  }
659
- async function syncPlatformComment(taskRef, options) {
732
+ function syncPlatformComment(taskRef, options) {
660
733
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
661
734
  if (!resolved.ok)
662
- return syncPlatformCommentImpl(taskRef, options);
663
- const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
664
- if (!taskIssueIdentity(parseTaskFrontmatter(taskContent)))
665
- return syncPlatformCommentImpl(taskRef, options);
735
+ return Promise.resolve(platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }));
736
+ const readStrategy = () => {
737
+ const prefix = markerPrefix(resolved.taskId, options);
738
+ return {
739
+ taskId: resolved.taskId, cwd: resolved.repoRoot, agent: options.agent, client: options.client,
740
+ parentKind: 'issue', kind: options.kind, markerPrefix: prefix,
741
+ backfill: options.backfill,
742
+ prepare: async () => {
743
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
744
+ const parent = taskIssueIdentity(parseTaskFrontmatter(taskContent));
745
+ if (!parent)
746
+ throw Object.assign(new Error('Task has no valid platform issue identity'), { code: 'ISSUE_NOT_LINKED' });
747
+ const source = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
748
+ if (options.kind === 'task' && options.taskProjection && createHash('sha256').update(source).digest('hex') !== options.taskProjection.sha256) {
749
+ throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
750
+ }
751
+ const items = expectedComments(resolved.taskId, source, resolved.taskDir, resolved.repoRoot, options);
752
+ if (options.kind === 'task' && Buffer.byteLength(items[0].body, 'utf8') > COMMENT_BYTE_LIMIT)
753
+ throw Object.assign(new Error('Projected task comment exceeds the platform byte limit'), { code: 'COMMENT_PAYLOAD_TOO_LARGE' });
754
+ return { parent, items };
755
+ },
756
+ summaryPosition: options.kind === 'summary',
757
+ verifyReadback: options.kind === 'summary' || options.kind === 'cancel',
758
+ authorizeReposition: (comment, currentUser) => options.summaryAuthorization
759
+ && options.summaryAuthorization.sha256 === createHash('sha256').update(options.body ?? '').digest('hex')
760
+ && comment.user?.login && currentUser && comment.user.login === currentUser
761
+ ? null : { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
762
+ };
763
+ };
764
+ let strategy;
666
765
  let operation;
667
766
  try {
668
- const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
669
- const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
670
- const content = desired.map((chunk) => chunk.content).join('\0');
671
- const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
672
- : options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
767
+ strategy = readStrategy();
768
+ const source = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : fs.readFileSync(resolved.taskMdPath, 'utf8');
769
+ const expected = expectedComments(resolved.taskId, source, resolved.taskDir, resolved.repoRoot, options);
770
+ const digest = createHash('sha256').update(expected.map((item) => item.content).join('\0')).digest('hex');
771
+ const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'artifact' ? 'artifact-comment' : options.kind === 'summary' ? 'summary-comment' : 'cancel-comment';
673
772
  const target = options.kind === 'artifact' ? options.artifact : options.kind;
674
- operation = { kind, target, expectedDigest: createHash('sha256').update(content).digest('hex') };
773
+ operation = { taskRef: resolved.taskId, cwd: resolved.repoRoot, kind, target, expectedDigest: digest, dependency: options.dependency ?? 'deferred' };
675
774
  }
676
- catch {
677
- return syncPlatformCommentImpl(taskRef, options);
775
+ catch (error) {
776
+ const value = error;
777
+ return Promise.resolve(platformResult('failed', { error: { code: value.code || 'COMMENT_PAYLOAD_INVALID', message: value.message || String(error), retryable: false } }));
678
778
  }
679
- const execute = () => syncPlatformCommentImpl(taskRef, options);
680
- if (options.skipQueue)
681
- return execute();
682
- return coordinatePlatformWrite({
683
- operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
684
- agent: options.agent,
685
- execute,
686
- block: (error) => platformResult('blocked', { error }),
687
- persistenceFailure: (error) => platformResult('failed', { error })
688
- });
779
+ return syncCommentPlan(strategy, operation, options.queuedOperation);
689
780
  }
690
781
  function inspectPlatformCommentOperation(taskRef, options) {
691
782
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
@@ -766,5 +857,5 @@ async function checkPlatformCommentOwner(taskRef, options = {}) {
766
857
  }
767
858
  return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
768
859
  }
769
- export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, inspectPlatformCommentOperation, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
860
+ export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, inspectPlatformCommentOperation, findMarkerComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, syncCommentPlan, syncPlatformComment, validateRelatedMarkerSet };
770
861
  //# sourceMappingURL=issue-comments.js.map