@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
@@ -5,11 +5,13 @@ import { createHash } from 'node:crypto';
5
5
  import { parseTaskFrontmatter } from '../task/frontmatter.ts';
6
6
  import { resolveTaskRef } from '../task/resolve-ref.ts';
7
7
  import { coordinatePlatformWrite } from './operation-coordinator.ts';
8
+ import type { PlatformOperation as JournalOperation, RecordOperationInput } from '../task/platform-operation-journal.ts';
8
9
  import { resolvePlatformProviderContext } from './context.ts';
9
- import type { PlatformClient } from './context.ts';
10
+ import type { LoadedContext, PlatformClient } from './context.ts';
10
11
  import { providerErrorRetryable } from './provider-validation.ts';
11
12
  import { platformResult } from './types.ts';
12
13
  import type { PlatformOperation, PlatformResult } from './types.ts';
14
+ import type { ResourceIdentity } from './resource-identity.ts';
13
15
  import {
14
16
  providerError,
15
17
  providerOperationContext,
@@ -58,9 +60,39 @@ type SyncOptions = {
58
60
  runtimeVersion?: string;
59
61
  summaryAuthorization?: { sha256: string };
60
62
  dependency?: 'deferred' | 'required';
61
- skipQueue?: boolean;
63
+ queuedOperation?: JournalOperation;
62
64
  taskProjection?: { content: string; sha256: string };
63
65
  };
66
+ type CommentPlanStrategy = Readonly<{
67
+ taskId: string;
68
+ cwd: string;
69
+ agent: string;
70
+ parentKind: 'issue' | 'pull-request';
71
+ kind: CommentKind;
72
+ client?: PlatformClient;
73
+ resolved?: { loaded: LoadedContext; context: PlatformResult };
74
+ backfill?: boolean;
75
+ markerPrefix: string;
76
+ prepare: () => Promise<{ parent: ResourceIdentity; items: RenderedChunk[] }>;
77
+ validate?: () => Promise<{ code: string; message: string; retryable: boolean } | null>;
78
+ summaryPosition?: boolean;
79
+ authorizeReposition?: (comment: RemoteComment, currentUser: string | null) => { code: string; message: string; retryable: boolean } | null;
80
+ verifyReadback?: boolean;
81
+ dryRun?: boolean;
82
+ }>;
83
+
84
+ async function syncCommentPlan(
85
+ strategy: CommentPlanStrategy,
86
+ operation: Omit<RecordOperationInput, 'state' | 'lastCode'>,
87
+ queuedOperation?: JournalOperation
88
+ ): Promise<PlatformResult> {
89
+ const execute = () => executeCommentPlan(strategy, strategy.client);
90
+ if (strategy.dryRun) return execute();
91
+ if (queuedOperation) return execute();
92
+ return coordinatePlatformWrite({ operation, agent: strategy.agent, client: strategy.client, execute,
93
+ block: (error) => platformResult('blocked', { error }),
94
+ persistenceFailure: (error) => platformResult('failed', { error }) });
95
+ }
64
96
 
65
97
  function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
66
98
  return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
@@ -146,10 +178,6 @@ function renderTaskComment(content: string, taskId: string, _agent: string, lang
146
178
  return renderTaskCommentResult(content, taskId, language).body;
147
179
  }
148
180
 
149
- function isTaskCommentTooLarge(content: string, taskId = 'TASK-UNKNOWN', _agent = 'codex'): boolean {
150
- return renderTaskCommentResult(content, taskId).byteLength > COMMENT_BYTE_LIMIT;
151
- }
152
-
153
181
  function taskCommentLanguage(repoRoot: string): string {
154
182
  try {
155
183
  const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { language?: unknown };
@@ -377,21 +405,6 @@ function findMarkerComments(comments: RemoteComment[], marker: string): RemoteCo
377
405
  return comments.filter((comment) => normalizeCommentContent(String(comment.body || '')).split('\n', 1)[0] === marker);
378
406
  }
379
407
 
380
- function flattenComments(value: unknown): RemoteComment[] {
381
- if (!Array.isArray(value)) return [];
382
- const flattened = value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
383
- return flattened.filter((entry): entry is RemoteComment =>
384
- Boolean(entry && typeof entry === 'object' && (typeof entry.id === 'number' || typeof entry.id === 'string') && typeof entry.body === 'string')
385
- );
386
- }
387
-
388
- function listRemoteComments(client: PlatformClient, repo: string, issue: number, cwd: string) {
389
- const result = client.json<unknown>([
390
- 'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
391
- ], { cwd });
392
- return result.ok ? { ok: true as const, value: flattenComments(result.value) } : result;
393
- }
394
-
395
408
  function contextFields(context: PlatformResult): Partial<PlatformResult> {
396
409
  return {
397
410
  platform: context.platform,
@@ -472,38 +485,370 @@ function relatedComments(comments: RemoteComment[], prefix: string): RemoteComme
472
485
  });
473
486
  }
474
487
 
475
- function taskManagedComment(comment: RemoteComment, taskId: string): boolean {
476
- const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
477
- return marker === MARKERS.task(taskId)
478
- || marker === MARKERS.summary(taskId)
479
- || marker.startsWith(`<!-- sync-issue:${taskId}:`);
488
+ async function listedComments(provider: any, loaded: any, parent: ResourceIdentity): Promise<any> {
489
+ return provider.comments?.list
490
+ ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
491
+ ? { ok: true as const, value: response.value.map((comment: any) => parentComment(comment, provider)) }
492
+ : { ...response, error: { ...response.error, retryable: isRetryable(response.error) } })
493
+ : unsupportedProviderOperation(provider, 'comments.list');
480
494
  }
481
495
 
482
- function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boolean; known: boolean; summary: RemoteComment | null } {
483
- const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
484
- const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
485
- if (summary.length !== 1) return { ok: false, known: false, summary: null };
486
- if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
487
- return { ok: false, known: false, summary: summary[0]! };
496
+ type CommentPlanSelection = Readonly<{
497
+ existing: RemoteComment[];
498
+ byMarker: Map<string, RemoteComment>;
499
+ reposition: boolean;
500
+ }>;
501
+
502
+ function markerSetError(strategy: CommentPlanStrategy, context: PlatformResult, resource: PlatformResult['resource'], existing: RemoteComment[]): PlatformResult | null {
503
+ if (validateRelatedMarkerSet(existing, strategy.markerPrefix).ok) return null;
504
+ return platformResult('failed', { ...contextFields(context), resource, error: {
505
+ code: strategy.parentKind === 'pull-request' ? 'PR_SUMMARY_MARKER_AMBIGUOUS' : 'COMMENT_MARKER_CONFLICT',
506
+ message: strategy.parentKind === 'pull-request' ? 'Multiple PR comments contain the summary marker' : 'Multiple comments use the same registered marker', retryable: false
507
+ } });
508
+ }
509
+
510
+ function taskCommentOwnerError(
511
+ strategy: CommentPlanStrategy,
512
+ context: PlatformResult,
513
+ resource: PlatformResult['resource'],
514
+ existing: RemoteComment[]
515
+ ): PlatformResult | null {
516
+ if (strategy.kind !== 'task' || existing.length !== 1) return null;
517
+ const owner = existing[0]!.user?.login;
518
+ return owner && context.platform.currentUser && owner === context.platform.currentUser ? null : platformResult('blocked', {
519
+ ...contextFields(context), resource, error: {
520
+ code: 'COMMENT_OWNER_CONFLICT', message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable', retryable: false
521
+ }
522
+ });
523
+ }
524
+
525
+ async function strategyValidationError(
526
+ strategy: CommentPlanStrategy,
527
+ context: PlatformResult,
528
+ resource: PlatformResult['resource']
529
+ ): Promise<PlatformResult | null> {
530
+ const error = strategy.validate ? await strategy.validate() : null;
531
+ if (!error) return null;
532
+ return platformResult(error.retryable ? 'blocked' : 'failed', { ...contextFields(context), resource, error });
533
+ }
534
+
535
+ function artifactBackfillResult(
536
+ strategy: CommentPlanStrategy,
537
+ context: PlatformResult,
538
+ resource: PlatformResult['resource'],
539
+ existing: RemoteComment[],
540
+ desired: RenderedChunk[]
541
+ ): PlatformResult | null {
542
+ if (!strategy.backfill || strategy.kind !== 'artifact' || existing.length === 0) return null;
543
+ const operations = existing.map((comment): PlatformOperation => ({
544
+ name: `comment:${normalizeCommentContent(comment.body).split('\n', 1)[0]}`,
545
+ status: 'no-op',
546
+ reasonCode: 'BACKFILL_ALREADY_PRESENT'
547
+ }));
548
+ return platformResult('no-op', {
549
+ ...contextFields(context),
550
+ resource,
551
+ operations,
552
+ comment: {
553
+ kind: strategy.kind,
554
+ marker: desired[0]?.marker ?? strategy.markerPrefix,
555
+ ids: existing.map((item) => item.id),
556
+ parts: existing.length
557
+ },
558
+ error: null
559
+ });
560
+ }
561
+
562
+ function repositionAuthorizationError(
563
+ strategy: CommentPlanStrategy,
564
+ context: PlatformResult,
565
+ resource: PlatformResult['resource'],
566
+ comments: RemoteComment[],
567
+ byMarker: Map<string, RemoteComment>,
568
+ desired: RenderedChunk[]
569
+ ): { error: PlatformResult | null; reposition: boolean } {
570
+ const marker = desired[0]?.marker;
571
+ const current = marker ? byMarker.get(marker) : undefined;
572
+ const position = strategy.summaryPosition && current
573
+ ? issueSummaryPosition(strategy.taskId, comments, marker!)
574
+ : true;
575
+ if (position === null) return {
576
+ error: platformResult('blocked', { ...contextFields(context), resource, error: {
577
+ code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true
578
+ } }),
579
+ reposition: false
580
+ };
581
+ const reposition = Boolean(strategy.summaryPosition && current && !position);
582
+ if (!reposition || !current) return { error: null, reposition: false };
583
+ const error = strategy.authorizeReposition
584
+ ? strategy.authorizeReposition(current, context.platform.currentUser ?? null)
585
+ : { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current authorization and an owned summary comment', retryable: false };
586
+ return {
587
+ error: error ? platformResult(error.retryable ? 'blocked' : 'failed', { ...contextFields(context), resource, error }) : null,
588
+ reposition: true
589
+ };
590
+ }
591
+
592
+ async function inspectCommentPlan(
593
+ strategy: CommentPlanStrategy,
594
+ context: PlatformResult,
595
+ resource: PlatformResult['resource'],
596
+ comments: RemoteComment[],
597
+ desired: RenderedChunk[]
598
+ ): Promise<{ ok: true; value: CommentPlanSelection; result?: PlatformResult } | { ok: false; result: PlatformResult }> {
599
+ const existing = relatedComments(comments, strategy.markerPrefix);
600
+ const markerError = markerSetError(strategy, context, resource, existing);
601
+ if (markerError) return { ok: false, result: markerError };
602
+ const ownerError = taskCommentOwnerError(strategy, context, resource, existing);
603
+ if (ownerError) return { ok: false, result: ownerError };
604
+ const validationError = await strategyValidationError(strategy, context, resource);
605
+ if (validationError) return { ok: false, result: validationError };
606
+ const backfill = artifactBackfillResult(strategy, context, resource, existing, desired);
607
+ if (backfill) return { ok: true, value: { existing, byMarker: new Map(), reposition: false }, result: backfill };
608
+ const byMarker = new Map(existing.map((comment) => [normalizeCommentContent(comment.body).split('\n', 1)[0]!, comment]));
609
+ const reposition = repositionAuthorizationError(strategy, context, resource, comments, byMarker, desired);
610
+ if (reposition.error) return { ok: false, result: reposition.error };
611
+ return { ok: true, value: { existing, byMarker, reposition: reposition.reposition } };
612
+ }
613
+
614
+ async function repositionSummaryComment(
615
+ strategy: CommentPlanStrategy,
616
+ loaded: LoadedContext,
617
+ context: PlatformResult,
618
+ parent: ResourceIdentity,
619
+ resource: PlatformResult['resource'],
620
+ current: RemoteComment | undefined,
621
+ operations: PlatformOperation[]
622
+ ): Promise<PlatformResult | null> {
623
+ if (!current) return null;
624
+ const deleted = loaded.provider.comments?.delete
625
+ ? await loaded.provider.comments.delete({ context: providerOperationContext(loaded), parent,
626
+ comment: providerResourceToken(loaded.provider, 'comment', String(current.id)),
627
+ mutation: { idempotencyKey: `comment-reposition-delete:${strategy.taskId}:${current.id}` } })
628
+ : unsupportedProviderOperation(loaded.provider, 'comments.delete');
629
+ if (!deleted.ok) return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
630
+ ...contextFields(context), resource, error: deleted.error
631
+ });
632
+ operations.push({
633
+ name: `comment:${current.id}`,
634
+ status: 'applied',
635
+ reasonCode: 'SUMMARY_REPOSITIONED'
636
+ });
637
+ return null;
638
+ }
639
+
640
+ async function observeCommentWrite(
641
+ strategy: CommentPlanStrategy,
642
+ loaded: LoadedContext,
643
+ parent: ResourceIdentity,
644
+ item: RenderedChunk
645
+ ): Promise<{ valid: true } | { valid: false; error: PlatformResult['error'] }> {
646
+ const after = await listedComments(loaded.provider, loaded, parent);
647
+ const comments = after.ok ? after.value as RemoteComment[] : [];
648
+ const found = comments.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
649
+ const positionOk = !strategy.summaryPosition || issueSummaryPosition(strategy.taskId, comments, item.marker) === true;
650
+ const validation = strategy.validate ? await strategy.validate() : null;
651
+ if (after.ok
652
+ && found.length === 1
653
+ && normalizeCommentContent(found[0]!.body) === normalizeCommentContent(item.body)
654
+ && positionOk
655
+ && !validation) return { valid: true };
656
+ const error = validation ?? {
657
+ code: strategy.kind === 'summary' || strategy.kind === 'cancel' ? 'SUMMARY_BODY_READBACK_FAILED' : 'COMMENT_READBACK_FAILED',
658
+ message: after.ok ? 'Remote comment did not match the requested body' : after.error.message,
659
+ retryable: after.ok || isRetryable(after.error)
660
+ };
661
+ return { valid: false, error };
662
+ }
663
+
664
+ async function verifyIssueSummaryReadback(
665
+ strategy: CommentPlanStrategy,
666
+ loaded: LoadedContext,
667
+ context: PlatformResult,
668
+ parent: ResourceIdentity,
669
+ resource: PlatformResult['resource'],
670
+ desired: RenderedChunk[],
671
+ operations: PlatformOperation[]
672
+ ): Promise<PlatformResult | null> {
673
+ if (strategy.parentKind !== 'issue' || (strategy.kind !== 'summary' && strategy.kind !== 'cancel')) return null;
674
+ const after = await listedComments(loaded.provider, loaded, parent);
675
+ if (!after.ok) return platformResult(isRetryable(after.error) ? 'blocked' : 'failed', {
676
+ ...contextFields(context), resource, operations, error: after.error
677
+ });
678
+ const item = desired[0];
679
+ const matching = item
680
+ ? after.value.filter((comment: RemoteComment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker)
681
+ : [];
682
+ if (!item || matching.length !== 1 || normalizeCommentContent(matching[0]!.body) !== normalizeCommentContent(item.body)) {
683
+ return platformResult('blocked', { ...contextFields(context), resource, operations, error: {
684
+ code: 'SUMMARY_BODY_READBACK_FAILED',
685
+ message: strategy.kind === 'cancel' ? 'Remote cancellation body did not match the rendered cancellation summary' : 'Remote summary body did not match the rendered delivery summary',
686
+ retryable: true
687
+ } });
488
688
  }
489
- const sequence = summary[0]!.createdSequence!;
490
- return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
689
+ if (strategy.summaryPosition && issueSummaryPosition(strategy.taskId, after.value, item.marker) !== true) {
690
+ return platformResult('blocked', { ...contextFields(context), resource, operations, error: {
691
+ code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true
692
+ } });
693
+ }
694
+ return null;
491
695
  }
492
696
 
493
- async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
494
- return provider.comments?.list
495
- ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
496
- ? {
497
- ok: true as const,
498
- value: response.value.map((comment: any) => ({
499
- id: providerCommentId(comment.id, provider),
500
- body: comment.body,
501
- user: comment.author?.name ? { login: comment.author.name } : undefined,
502
- createdSequence: comment.createdSequence
503
- }))
504
- }
505
- : response)
506
- : unsupportedProviderOperation(provider, 'comments.list');
697
+ async function compensatePullRequestComment(
698
+ strategy: CommentPlanStrategy,
699
+ loaded: LoadedContext,
700
+ parent: ResourceIdentity,
701
+ current: RemoteComment | undefined,
702
+ id: number | string,
703
+ cause: PlatformResult['error']
704
+ ): Promise<PlatformResult['error']> {
705
+ const compensation = current
706
+ ? await loaded.provider.comments?.write?.({ context: providerOperationContext(loaded), parent, body: current.body,
707
+ existingComment: providerResourceToken(loaded.provider, 'comment', String(current.id)),
708
+ mutation: { idempotencyKey: `pr-summary-restore:${strategy.taskId}` } })
709
+ : await loaded.provider.comments?.delete?.({ context: providerOperationContext(loaded), parent,
710
+ comment: providerResourceToken(loaded.provider, 'comment', String(id)),
711
+ mutation: { idempotencyKey: `pr-summary-delete:${strategy.taskId}:${id}` } });
712
+ if (!compensation?.ok) return {
713
+ code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
714
+ message: compensation?.error?.message ?? 'Provider does not support comment compensation',
715
+ retryable: true
716
+ };
717
+ if (cause?.code === 'PR_SUMMARY_HEAD_RACE') return cause;
718
+ return {
719
+ code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
720
+ message: cause?.message ?? 'Post-write summary verification failed',
721
+ retryable: true
722
+ };
723
+ }
724
+
725
+ async function verifyCommentWrite(
726
+ strategy: CommentPlanStrategy,
727
+ loaded: LoadedContext,
728
+ parent: ResourceIdentity,
729
+ item: RenderedChunk,
730
+ current: RemoteComment | undefined,
731
+ id: number | string
732
+ ): Promise<PlatformResult['error']> {
733
+ if (!strategy.verifyReadback || strategy.parentKind === 'issue') return null;
734
+ const observed = await observeCommentWrite(strategy, loaded, parent, item);
735
+ if (observed.valid) return null;
736
+ if (strategy.parentKind === 'pull-request') {
737
+ return compensatePullRequestComment(strategy, loaded, parent, current, id, observed.error);
738
+ }
739
+ return observed.error;
740
+ }
741
+
742
+ async function writeDesiredComments(
743
+ strategy: CommentPlanStrategy,
744
+ loaded: LoadedContext,
745
+ context: PlatformResult,
746
+ parent: ResourceIdentity,
747
+ resource: PlatformResult['resource'],
748
+ desired: RenderedChunk[],
749
+ selection: CommentPlanSelection,
750
+ operations: PlatformOperation[]
751
+ ): Promise<{ ok: true; ids: Array<number | string> } | { ok: false; result: PlatformResult }> {
752
+ const ids: Array<number | string> = [];
753
+ for (const item of desired) {
754
+ const current = selection.reposition && item === desired[0] ? undefined : selection.byMarker.get(item.marker);
755
+ if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
756
+ ids.push(current.id);
757
+ operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
758
+ continue;
759
+ }
760
+ const written = loaded.provider.comments?.write
761
+ ? await loaded.provider.comments.write({ context: providerOperationContext(loaded), parent, body: item.body,
762
+ ...(current ? { existingComment: providerResourceToken(loaded.provider, 'comment', String(current.id)) } : {}),
763
+ mutation: { idempotencyKey: `comment:${strategy.taskId}:${item.marker}` } })
764
+ : unsupportedProviderOperation(loaded.provider, 'comments.write');
765
+ if (!written.ok) return { ok: false, result: platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
766
+ ...contextFields(context), resource, operations, error: written.error
767
+ }) };
768
+ const id = providerCommentId(written.value.remoteId, loaded.provider);
769
+ ids.push(id);
770
+ operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
771
+ const error = await verifyCommentWrite(strategy, loaded, parent, item, current, id);
772
+ if (error) return { ok: false, result: platformResult(error.retryable ? 'blocked' : 'failed', {
773
+ ...contextFields(context), resource, operations, error
774
+ }) };
775
+ }
776
+ return { ok: true, ids };
777
+ }
778
+
779
+ async function deleteStaleComments(
780
+ strategy: CommentPlanStrategy,
781
+ loaded: LoadedContext,
782
+ context: PlatformResult,
783
+ parent: ResourceIdentity,
784
+ resource: PlatformResult['resource'],
785
+ existing: RemoteComment[],
786
+ desired: RenderedChunk[],
787
+ operations: PlatformOperation[]
788
+ ): Promise<PlatformResult | null> {
789
+ const markers = new Set(desired.map((item) => item.marker));
790
+ for (const stale of existing.filter((comment) => !markers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]!))) {
791
+ const deleted = loaded.provider.comments?.delete
792
+ ? await loaded.provider.comments.delete({ context: providerOperationContext(loaded), parent,
793
+ comment: providerResourceToken(loaded.provider, 'comment', String(stale.id)),
794
+ mutation: { idempotencyKey: `comment-delete:${strategy.taskId}:${stale.id}` } })
795
+ : unsupportedProviderOperation(loaded.provider, 'comments.delete');
796
+ if (!deleted.ok) return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
797
+ ...contextFields(context), resource, operations, error: deleted.error
798
+ });
799
+ operations.push({ name: `comment:${stale.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
800
+ }
801
+ return null;
802
+ }
803
+
804
+ type PreparedCommentExecution = Readonly<{
805
+ loaded: LoadedContext;
806
+ context: PlatformResult;
807
+ parent: ResourceIdentity;
808
+ desired: RenderedChunk[];
809
+ resource: PlatformResult['resource'];
810
+ selection: CommentPlanSelection;
811
+ result?: PlatformResult;
812
+ }>;
813
+
814
+ async function prepareCommentExecution(
815
+ strategy: CommentPlanStrategy,
816
+ client?: PlatformClient
817
+ ): Promise<{ ok: true; value: PreparedCommentExecution } | { ok: false; result: PlatformResult }> {
818
+ const resolved = strategy.resolved
819
+ ? { ok: true as const, value: strategy.resolved.loaded }
820
+ : await resolvePlatformProviderContext({ cwd: strategy.cwd, client });
821
+ const loaded = resolved;
822
+ const context = strategy.resolved?.context ?? (loaded.ok ? loaded.value.context : loaded.context);
823
+ if (!loaded.ok || !hasResolvedPlatformContext(context)) return { ok: false, result: context };
824
+ let parent: ResourceIdentity;
825
+ let desired: RenderedChunk[];
826
+ try { ({ parent, items: desired } = await strategy.prepare()); }
827
+ catch (error) {
828
+ const value = error as { code?: string; message?: string };
829
+ return { ok: false, result: platformResult('failed', { ...contextFields(context), error: {
830
+ code: value.code || 'COMMENT_PAYLOAD_INVALID', message: value.message || String(error), retryable: false
831
+ } }) };
832
+ }
833
+ const resource = { kind: strategy.parentKind, number: resourceIdentityNumber(parent), identity: parent } as const;
834
+ const listed = await listedComments(loaded.value.provider, loaded.value, parent);
835
+ if (!listed.ok) return { ok: false, result: platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
836
+ ...contextFields(context), resource, error: listed.error
837
+ }) };
838
+ const selection = await inspectCommentPlan(strategy, context, resource, listed.value, desired);
839
+ if (!selection.ok) return selection;
840
+ return {
841
+ ok: true,
842
+ value: {
843
+ loaded: loaded.value,
844
+ context,
845
+ parent,
846
+ desired,
847
+ resource,
848
+ selection: selection.value,
849
+ result: selection.result
850
+ }
851
+ };
507
852
  }
508
853
 
509
854
  function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): { ok: boolean; code: string | null } {
@@ -525,268 +870,102 @@ function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): {
525
870
  return complete ? { ok: true, code: null } : { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
526
871
  }
527
872
 
528
- function writeComment(
529
- client: PlatformClient,
530
- repo: string,
531
- issue: number,
532
- cwd: string,
533
- body: string,
534
- id?: number
535
- ) {
536
- const endpoint = id ? `repos/${repo}/issues/comments/${id}` : `repos/${repo}/issues/${issue}/comments`;
537
- return client.json<{ id?: number }>(
538
- ['api', endpoint, '-X', id ? 'PATCH' : 'POST', '--input', '-'],
539
- { cwd, method: id ? 'PATCH' : 'POST', input: JSON.stringify({ body }) }
540
- );
873
+ function parentComment(comment: any, provider: any): RemoteComment {
874
+ return {
875
+ id: providerCommentId(comment.id, provider), body: comment.body,
876
+ user: comment.author?.name ? { login: comment.author.name } : undefined,
877
+ createdSequence: comment.createdSequence
878
+ };
541
879
  }
542
880
 
543
- async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
544
- const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
545
- if (!resolved.ok) {
546
- return platformResult('failed', {
547
- error: { code: resolved.code, message: resolved.message, retryable: false }
548
- });
549
- }
550
- const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
551
- let issueIdentityFromTask: ReturnType<typeof taskIssueIdentity>;
552
- try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent)); }
553
- catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
554
- if (!issueIdentityFromTask) {
555
- return platformResult('no-op', {
556
- error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
557
- });
558
- }
559
- let desired: RenderedChunk[];
560
- try {
561
- const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
562
- if (options.kind === 'task' && options.taskProjection
563
- && createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
564
- throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
565
- }
566
- desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
567
- } catch (error) {
568
- return platformResult('failed', {
569
- resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
570
- error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
571
- });
572
- }
573
- if (options.kind === 'task' && Buffer.byteLength(desired[0]!.body, 'utf8') > COMMENT_BYTE_LIMIT) {
574
- return platformResult('failed', {
575
- resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
576
- operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
577
- error: { code: 'COMMENT_PAYLOAD_TOO_LARGE', message: 'Projected task comment exceeds the platform byte limit', retryable: false }
578
- });
579
- }
580
- const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
581
- const context = loaded.ok ? loaded.value.context : loaded.context;
582
- if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
583
- const issue = resourceIdentityNumber(issueIdentityFromTask);
584
- const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
585
- if (!listed.ok) {
586
- return platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
587
- ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
588
- });
589
- }
590
-
591
- const existing = relatedComments(listed.value, markerPrefix(resolved.taskId, options));
592
- if (!validateRelatedMarkerSet(existing, markerPrefix(resolved.taskId, options)).ok) {
593
- return platformResult('failed', {
594
- ...contextFields(context),
595
- resource: { kind: 'issue', number: issue },
596
- error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple comments use the same registered marker', retryable: false }
597
- });
598
- }
599
-
600
- if (options.kind === 'task' && existing.length === 1) {
601
- const owner = existing[0]!.user?.login;
602
- if (!owner || !context.platform.currentUser || owner !== context.platform.currentUser) {
603
- return platformResult('blocked', {
604
- ...contextFields(context),
605
- resource: { kind: 'issue', number: issue, identity: issueIdentityFromTask },
606
- error: {
607
- code: 'COMMENT_OWNER_CONFLICT',
608
- message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable',
609
- retryable: false
610
- }
611
- });
612
- }
613
- }
614
-
615
- // Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
616
- if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
617
- const operations = existing.map((comment): PlatformOperation => ({
618
- name: `comment:${normalizeCommentContent(comment.body).split('\n', 1)[0]}`,
619
- status: 'no-op',
620
- reasonCode: 'BACKFILL_ALREADY_PRESENT'
621
- }));
622
- return platformResult('no-op', {
623
- ...contextFields(context),
624
- changed: false,
625
- resource: { kind: 'issue', number: issue },
626
- operations,
627
- comment: {
628
- kind: options.kind,
629
- marker: desired[0]!.marker,
630
- ids: existing.map((comment) => comment.id),
631
- parts: existing.length
632
- },
633
- error: null
634
- });
635
- }
881
+ function issueSummaryPosition(taskId: string, comments: readonly RemoteComment[], marker: string): boolean | null {
882
+ const managed = comments.filter((comment) => comment.body.split('\n', 1)[0]?.startsWith(`<!-- sync-issue:${taskId}:`));
883
+ const summary = managed.filter((comment) => comment.body.split('\n', 1)[0] === marker);
884
+ if (summary.length !== 1 || managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || comment.createdSequence === null)) return null;
885
+ return managed.every((comment) => summary[0]!.createdSequence! >= comment.createdSequence!);
886
+ }
636
887
 
888
+ async function executeCommentPlan(
889
+ strategy: CommentPlanStrategy,
890
+ client?: PlatformClient
891
+ ): Promise<PlatformResult> {
892
+ const prepared = await prepareCommentExecution(strategy, client);
893
+ if (!prepared.ok) return prepared.result;
894
+ const { loaded, context, parent, desired, resource, selection, result } = prepared.value;
895
+ if (result) return result;
896
+ if (strategy.dryRun) return platformResult('planned', {
897
+ ...contextFields(context), resource,
898
+ operations: desired.map((item) => ({ name: `comment:${item.marker}`, status: 'planned', reasonCode: null })), error: null
899
+ });
637
900
  const operations: PlatformOperation[] = [];
638
- const ids: Array<number | string> = [];
639
- for (const item of desired) {
640
- const current = existing.find((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
641
- if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
642
- ids.push(current.id);
643
- operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
644
- continue;
645
- }
646
- const written = loaded.value.provider.comments?.write
647
- ? await loaded.value.provider.comments.write({
648
- context: providerOperationContext(loaded.value),
649
- parent: issueIdentityFromTask,
650
- body: item.body,
651
- ...(current ? { existingComment: { kind: 'id' as const, value: String(current.id) } } : {}),
652
- mutation: { idempotencyKey: `comment:${resolved.taskId}:${item.marker}` }
653
- })
654
- : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
655
- if (!written.ok) {
656
- if (!current && isRetryable(written.error)) {
657
- // A provider owns reconciliation because only it knows how to address the resource.
658
- }
659
- return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
660
- ...contextFields(context),
661
- resource: { kind: 'issue', number: issue },
662
- operations,
663
- error: written.error
664
- });
665
- }
666
- ids.push(providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
667
- operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
668
- }
669
-
670
- const desiredMarkers = new Set(desired.map((item) => item.marker));
671
- const stale = existing.filter((comment) => !desiredMarkers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]!));
672
- for (const comment of stale) {
673
- const deleted = loaded.value.provider.comments?.delete
674
- ? await loaded.value.provider.comments.delete({
675
- context: providerOperationContext(loaded.value),
676
- parent: issueIdentityFromTask,
677
- comment: { kind: 'id', value: String(comment.id) },
678
- mutation: { idempotencyKey: `comment-delete:${resolved.taskId}:${comment.id}` }
679
- })
680
- : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
681
- if (!deleted.ok) {
682
- return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
683
- ...contextFields(context),
684
- resource: { kind: 'issue', number: issue },
685
- operations,
686
- error: deleted.error
687
- });
688
- }
689
- operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
690
- }
691
- if (options.kind === 'summary') {
692
- const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
693
- if (!refreshed.ok) {
694
- return platformResult(isRetryable(refreshed.error) ? 'blocked' : 'failed', {
695
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
696
- });
697
- }
698
- const position = summaryPosition(refreshed.value, resolved.taskId);
699
- if (!position.ok && !position.known) {
700
- return platformResult('blocked', {
701
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
702
- error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
703
- });
704
- }
705
- if (!position.ok && position.summary) {
706
- const owner = position.summary.user?.login;
707
- if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
708
- || !owner || owner !== context.platform.currentUser) {
709
- return platformResult('failed', {
710
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
711
- error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
712
- });
713
- }
714
- const deleted = loaded.value.provider.comments?.delete
715
- ? await loaded.value.provider.comments.delete({
716
- context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
717
- comment: { kind: 'id', value: String(position.summary.id) },
718
- mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
719
- })
720
- : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
721
- if (!deleted.ok) {
722
- return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
723
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
724
- });
725
- }
726
- const written = loaded.value.provider.comments?.write
727
- ? await loaded.value.provider.comments.write({
728
- context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0]!.body,
729
- mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
730
- })
731
- : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
732
- if (!written.ok) {
733
- return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
734
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
735
- });
736
- }
737
- ids.splice(0, ids.length, providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
738
- operations.push({ name: `comment:${desired[0]!.marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
739
- const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
740
- if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
741
- const error = reconciled.ok
742
- ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
743
- : reconciled.error;
744
- return platformResult(isRetryable(error) ? 'blocked' : 'failed', {
745
- ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
746
- });
747
- }
748
- }
901
+ const currentSummary = desired[0] ? selection.byMarker.get(desired[0].marker) : undefined;
902
+ if (selection.reposition) {
903
+ const repositioned = await repositionSummaryComment(strategy, loaded, context, parent, resource, currentSummary, operations);
904
+ if (repositioned) return repositioned;
749
905
  }
906
+ const written = await writeDesiredComments(strategy, loaded, context, parent, resource, desired, selection, operations);
907
+ if (!written.ok) return written.result;
908
+ const deleted = await deleteStaleComments(strategy, loaded, context, parent, resource, selection.existing, desired, operations);
909
+ if (deleted) return deleted;
910
+ const readback = await verifyIssueSummaryReadback(strategy, loaded, context, parent, resource, desired, operations);
911
+ if (readback) return readback;
750
912
  const changed = operations.some((operation) => operation.status === 'applied');
751
- const result = platformResult(changed ? 'applied' : 'no-op', {
752
- ...contextFields(context),
753
- changed,
754
- resource: { kind: 'issue', number: issue },
755
- operations,
756
- comment: { kind: options.kind, marker: desired[0]?.marker ?? markerPrefix(resolved.taskId, options), ids, parts: desired.length },
913
+ return platformResult(changed ? 'applied' : 'no-op', {
914
+ ...contextFields(context), changed, resource, operations,
915
+ comment: {
916
+ kind: strategy.kind,
917
+ marker: desired[0]?.marker ?? strategy.markerPrefix,
918
+ ids: written.ids,
919
+ parts: desired.length
920
+ },
757
921
  error: null
758
922
  });
759
- return result;
760
923
  }
761
924
 
762
- async function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
925
+ function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
763
926
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
764
- if (!resolved.ok) return syncPlatformCommentImpl(taskRef, options);
765
- const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
766
- if (!taskIssueIdentity(parseTaskFrontmatter(taskContent))) return syncPlatformCommentImpl(taskRef, options);
767
-
768
- let operation: { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string };
927
+ if (!resolved.ok) return Promise.resolve(platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }));
928
+ const readStrategy = (): CommentPlanStrategy => {
929
+ const prefix = markerPrefix(resolved.taskId, options);
930
+ return {
931
+ taskId: resolved.taskId, cwd: resolved.repoRoot, agent: options.agent, client: options.client,
932
+ parentKind: 'issue', kind: options.kind, markerPrefix: prefix,
933
+ backfill: options.backfill,
934
+ prepare: async () => {
935
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
936
+ const parent = taskIssueIdentity(parseTaskFrontmatter(taskContent));
937
+ if (!parent) throw Object.assign(new Error('Task has no valid platform issue identity'), { code: 'ISSUE_NOT_LINKED' });
938
+ const source = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
939
+ if (options.kind === 'task' && options.taskProjection && createHash('sha256').update(source).digest('hex') !== options.taskProjection.sha256) {
940
+ throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
941
+ }
942
+ const items = expectedComments(resolved.taskId, source, resolved.taskDir, resolved.repoRoot, options);
943
+ if (options.kind === 'task' && Buffer.byteLength(items[0]!.body, 'utf8') > COMMENT_BYTE_LIMIT) throw Object.assign(new Error('Projected task comment exceeds the platform byte limit'), { code: 'COMMENT_PAYLOAD_TOO_LARGE' });
944
+ return { parent, items };
945
+ },
946
+ summaryPosition: options.kind === 'summary',
947
+ verifyReadback: options.kind === 'summary' || options.kind === 'cancel',
948
+ authorizeReposition: (comment, currentUser) => options.summaryAuthorization
949
+ && options.summaryAuthorization.sha256 === createHash('sha256').update(options.body ?? '').digest('hex')
950
+ && comment.user?.login && currentUser && comment.user.login === currentUser
951
+ ? null : { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
952
+ };
953
+ };
954
+ let strategy: CommentPlanStrategy;
955
+ let operation: Omit<RecordOperationInput, 'state' | 'lastCode'>;
769
956
  try {
770
- const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
771
- const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
772
- const content = desired.map((chunk) => chunk.content).join('\0');
773
- const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
774
- : options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
957
+ strategy = readStrategy();
958
+ const source = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : fs.readFileSync(resolved.taskMdPath, 'utf8');
959
+ const expected = expectedComments(resolved.taskId, source, resolved.taskDir, resolved.repoRoot, options);
960
+ const digest = createHash('sha256').update(expected.map((item) => item.content).join('\0')).digest('hex');
961
+ const kind: JournalOperation['kind'] = options.kind === 'task' ? 'task-comment' : options.kind === 'artifact' ? 'artifact-comment' : options.kind === 'summary' ? 'summary-comment' : 'cancel-comment';
775
962
  const target = options.kind === 'artifact' ? options.artifact! : options.kind;
776
- operation = { kind, target, expectedDigest: createHash('sha256').update(content).digest('hex') };
777
- } catch {
778
- return syncPlatformCommentImpl(taskRef, options);
963
+ operation = { taskRef: resolved.taskId, cwd: resolved.repoRoot, kind, target, expectedDigest: digest, dependency: options.dependency ?? 'deferred' };
964
+ } catch (error) {
965
+ const value = error as { code?: string; message?: string };
966
+ return Promise.resolve(platformResult('failed', { error: { code: value.code || 'COMMENT_PAYLOAD_INVALID', message: value.message || String(error), retryable: false } }));
779
967
  }
780
-
781
- const execute = () => syncPlatformCommentImpl(taskRef, options);
782
- if (options.skipQueue) return execute();
783
- return coordinatePlatformWrite({
784
- operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
785
- agent: options.agent,
786
- execute,
787
- block: (error) => platformResult('blocked', { error }),
788
- persistenceFailure: (error) => platformResult('failed', { error })
789
- });
968
+ return syncCommentPlan(strategy, operation, options.queuedOperation);
790
969
  }
791
970
 
792
971
  function inspectPlatformCommentOperation(taskRef: string, options: SyncOptions): { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string; id: string } | null {
@@ -862,15 +1041,13 @@ export {
862
1041
  chunkArtifactComment,
863
1042
  inspectPlatformCommentOperation,
864
1043
  findMarkerComments,
865
- listRemoteComments,
866
1044
  checkPlatformCommentOwner,
867
1045
  listPlatformComments,
868
1046
  normalizeCommentContent,
869
1047
  renderTaskComment,
870
1048
  renderTaskCommentResult,
871
- isTaskCommentTooLarge,
1049
+ syncCommentPlan,
872
1050
  syncPlatformComment,
873
- validateRelatedMarkerSet,
874
- writeComment
1051
+ validateRelatedMarkerSet
875
1052
  };
876
- export type { CommentKind, RemoteComment, RenderedChunk, SyncOptions };
1053
+ export type { CommentKind, CommentPlanStrategy, RemoteComment, RenderedChunk, SyncOptions };