@fitlab-ai/agent-infra 0.9.12 → 0.9.13

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 (239) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +11 -11
  3. package/dist/bin/internal-cli.js +24 -19
  4. package/dist/lib/agent-clients/next-steps.js +6 -1
  5. package/dist/lib/decide.js +3 -3
  6. package/dist/lib/internal/platform-checks.js +3 -3
  7. package/dist/lib/internal/platform-comment.js +9 -9
  8. package/dist/lib/internal/platform-context.js +2 -2
  9. package/dist/lib/internal/platform-issue.js +8 -9
  10. package/dist/lib/internal/platform-pr-review.js +10 -10
  11. package/dist/lib/internal/platform-pr.js +63 -24
  12. package/dist/lib/internal/platform-release-notes.js +3 -3
  13. package/dist/lib/internal/pr-review-grade.js +11 -9
  14. package/dist/lib/internal/release-workflow.js +4 -4
  15. package/dist/lib/internal/sandbox-control.js +30 -4
  16. package/dist/lib/internal/task-activity.js +3 -3
  17. package/dist/lib/internal/task-artifact.js +76 -24
  18. package/dist/lib/internal/task-create.js +2 -2
  19. package/dist/lib/internal/task-event.js +6 -3
  20. package/dist/lib/internal/task-finalization.js +2 -2
  21. package/dist/lib/internal/task-invalidation.js +60 -0
  22. package/dist/lib/internal/task-ledger.js +9 -6
  23. package/dist/lib/internal/task-lifecycle.js +2 -2
  24. package/dist/lib/internal/task-override.js +4 -4
  25. package/dist/lib/internal/task-review.js +3 -3
  26. package/dist/lib/internal/task-verify.js +6 -6
  27. package/dist/lib/internal/task-warning.js +3 -3
  28. package/dist/lib/platform/adapters.js +1 -3
  29. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  30. package/dist/lib/platform/completion-backfill.js +6 -5
  31. package/dist/lib/platform/context.js +70 -130
  32. package/dist/lib/platform/github-client.js +1 -1
  33. package/dist/lib/platform/github-provider.js +903 -0
  34. package/dist/lib/platform/in-label-sync.js +143 -0
  35. package/dist/lib/platform/issue-comments.js +94 -41
  36. package/dist/lib/platform/issues.js +335 -189
  37. package/dist/lib/platform/metadata-labels.js +6 -2
  38. package/dist/lib/platform/none-provider.js +28 -0
  39. package/dist/lib/platform/pr-change-report.js +551 -0
  40. package/dist/lib/platform/pr-checks.js +119 -104
  41. package/dist/lib/platform/pr-review.js +88 -72
  42. package/dist/lib/platform/pr-summary.js +520 -50
  43. package/dist/lib/platform/provider-bridge.js +45 -0
  44. package/dist/lib/platform/provider-contract.d.ts +487 -0
  45. package/dist/lib/platform/provider-contract.js +95 -0
  46. package/dist/lib/platform/provider-loader.js +251 -0
  47. package/dist/lib/platform/provider-validation.js +504 -0
  48. package/dist/lib/platform/pull-request-metadata.js +8 -6
  49. package/dist/lib/platform/pull-requests.js +783 -537
  50. package/dist/lib/platform/release-notes.js +78 -55
  51. package/dist/lib/platform/releases.js +79 -50
  52. package/dist/lib/platform/resource-identity.d.ts +26 -0
  53. package/dist/lib/platform/resource-identity.js +92 -0
  54. package/dist/lib/platform/task-identities.js +31 -0
  55. package/dist/lib/platform/verification-required.js +11 -12
  56. package/dist/lib/platform/verification-sync.js +112 -238
  57. package/dist/lib/pr-review/evidence-grading.js +19 -6
  58. package/dist/lib/run/index.js +63 -23
  59. package/dist/lib/sandbox/control/client.js +96 -15
  60. package/dist/lib/sandbox/control/executor.js +3 -3
  61. package/dist/lib/sandbox/control/protocol.js +77 -0
  62. package/dist/lib/sandbox/control/server.js +398 -25
  63. package/dist/lib/sandbox/control/state.js +204 -1
  64. package/dist/lib/server/protocol.js +38 -1
  65. package/dist/lib/task/activity-log.js +8 -1
  66. package/dist/lib/task/artifact-lifecycle.js +33 -5
  67. package/dist/lib/task/capabilities.js +239 -0
  68. package/dist/lib/task/commands/status.js +32 -0
  69. package/dist/lib/task/commit-operation.js +12 -1
  70. package/dist/lib/task/control-authority.js +8 -5
  71. package/dist/lib/task/create-service.js +8 -8
  72. package/dist/lib/task/current-contract.js +5 -1
  73. package/dist/lib/task/decision-details.js +1 -1
  74. package/dist/lib/task/events.js +297 -17
  75. package/dist/lib/task/finalization.js +37 -13
  76. package/dist/lib/task/human-override.js +58 -36
  77. package/dist/lib/task/invalidation-command.js +86 -0
  78. package/dist/lib/task/invalidation.js +260 -0
  79. package/dist/lib/task/ledger-intents.js +37 -0
  80. package/dist/lib/task/local-artifact-finalization.js +374 -0
  81. package/dist/lib/task/orchestration.js +49 -93
  82. package/dist/lib/task/pr-delivery-fact.js +102 -101
  83. package/dist/lib/task/review-fingerprint.js +24 -2
  84. package/dist/lib/task/rework-intent.js +138 -0
  85. package/dist/lib/task/skill-scope.js +23 -0
  86. package/dist/lib/task/task-execution-lock.js +27 -17
  87. package/dist/lib/task/verification-engine.js +43 -16
  88. package/dist/lib/task/verification.js +3 -3
  89. package/dist/lib/task/workflow-warning-intents.js +1 -1
  90. package/dist/lib/task/write.js +11 -0
  91. package/dist/lib/version.js +11 -1
  92. package/dist/lifecycle-build-manifest.json +3 -3
  93. package/lib/agent-clients/next-steps.ts +6 -1
  94. package/lib/decide.ts +3 -3
  95. package/lib/init.ts +1 -1
  96. package/lib/internal/platform-checks.ts +3 -3
  97. package/lib/internal/platform-comment.ts +8 -8
  98. package/lib/internal/platform-context.ts +2 -2
  99. package/lib/internal/platform-issue.ts +7 -8
  100. package/lib/internal/platform-pr-review.ts +9 -9
  101. package/lib/internal/platform-pr.ts +47 -22
  102. package/lib/internal/platform-release-notes.ts +3 -3
  103. package/lib/internal/pr-review-grade.ts +10 -8
  104. package/lib/internal/release-workflow.ts +4 -4
  105. package/lib/internal/sandbox-control.ts +27 -3
  106. package/lib/internal/task-activity.ts +3 -3
  107. package/lib/internal/task-artifact.ts +65 -22
  108. package/lib/internal/task-create.ts +2 -2
  109. package/lib/internal/task-event.ts +6 -3
  110. package/lib/internal/task-finalization.ts +3 -3
  111. package/lib/internal/task-invalidation.ts +39 -0
  112. package/lib/internal/task-ledger.ts +11 -8
  113. package/lib/internal/task-lifecycle.ts +2 -2
  114. package/lib/internal/task-override.ts +6 -6
  115. package/lib/internal/task-review.ts +3 -3
  116. package/lib/internal/task-verify.ts +6 -6
  117. package/lib/internal/task-warning.ts +3 -3
  118. package/lib/platform/adapters.ts +3 -2
  119. package/lib/platform/change-request-git-evidence.ts +36 -9
  120. package/lib/platform/completion-backfill.ts +9 -8
  121. package/lib/platform/context.ts +97 -134
  122. package/lib/platform/github-client.ts +2 -2
  123. package/lib/platform/github-provider.ts +910 -0
  124. package/lib/platform/in-label-sync.ts +208 -0
  125. package/lib/platform/issue-comments.ts +97 -47
  126. package/lib/platform/issues.ts +344 -174
  127. package/lib/platform/metadata-labels.ts +5 -2
  128. package/lib/platform/none-provider.ts +35 -0
  129. package/lib/platform/pr-change-report.ts +645 -0
  130. package/lib/platform/pr-checks.ts +126 -106
  131. package/lib/platform/pr-review.ts +88 -85
  132. package/lib/platform/pr-summary.ts +560 -53
  133. package/lib/platform/provider-bridge.ts +82 -0
  134. package/lib/platform/provider-contract.ts +536 -0
  135. package/lib/platform/provider-loader.ts +307 -0
  136. package/lib/platform/provider-validation.ts +529 -0
  137. package/lib/platform/pull-request-metadata.ts +10 -7
  138. package/lib/platform/pull-requests.ts +826 -543
  139. package/lib/platform/release-notes.ts +76 -59
  140. package/lib/platform/releases.ts +77 -43
  141. package/lib/platform/resource-identity.ts +121 -0
  142. package/lib/platform/task-identities.ts +40 -0
  143. package/lib/platform/types.ts +4 -2
  144. package/lib/platform/verification-required.ts +11 -13
  145. package/lib/platform/verification-sync.ts +112 -292
  146. package/lib/pr-review/evidence-grading.ts +25 -8
  147. package/lib/run/index.ts +57 -22
  148. package/lib/sandbox/control/client.ts +105 -15
  149. package/lib/sandbox/control/executor.ts +4 -4
  150. package/lib/sandbox/control/protocol.ts +113 -0
  151. package/lib/sandbox/control/server.ts +431 -23
  152. package/lib/sandbox/control/state.ts +251 -1
  153. package/lib/server/protocol.ts +36 -1
  154. package/lib/task/activity-log.ts +8 -0
  155. package/lib/task/artifact-lifecycle.ts +32 -6
  156. package/lib/task/capabilities.ts +262 -0
  157. package/lib/task/commands/status.ts +38 -0
  158. package/lib/task/commit-operation.ts +12 -1
  159. package/lib/task/control-authority.ts +11 -8
  160. package/lib/task/create-service.ts +8 -8
  161. package/lib/task/current-contract.ts +6 -1
  162. package/lib/task/decision-details.ts +6 -2
  163. package/lib/task/events.ts +296 -16
  164. package/lib/task/finalization.ts +50 -14
  165. package/lib/task/human-override.ts +70 -46
  166. package/lib/task/invalidation-command.ts +86 -0
  167. package/lib/task/invalidation.ts +332 -0
  168. package/lib/task/ledger-intents.ts +35 -1
  169. package/lib/task/local-artifact-finalization.ts +563 -0
  170. package/lib/task/orchestration.ts +52 -85
  171. package/lib/task/pr-delivery-fact.ts +131 -148
  172. package/lib/task/review-fingerprint.ts +23 -2
  173. package/lib/task/rework-intent.ts +165 -0
  174. package/lib/task/skill-scope.ts +24 -0
  175. package/lib/task/task-execution-lock.ts +23 -14
  176. package/lib/task/verification-engine.ts +52 -17
  177. package/lib/task/verification.ts +4 -4
  178. package/lib/task/workflow-warning-intents.ts +1 -1
  179. package/lib/task/write.ts +19 -0
  180. package/lib/update.ts +1 -1
  181. package/lib/version.ts +15 -1
  182. package/package.json +7 -1
  183. package/templates/.agents/rules/README.en.md +1 -1
  184. package/templates/.agents/rules/README.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  186. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  187. package/templates/.agents/rules/issue-sync.en.md +2 -0
  188. package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
  189. package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
  190. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
  191. package/templates/.agents/rules/pr-sync.en.md +10 -4
  192. package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
  193. package/templates/.agents/rules/task-short-id.en.md +1 -4
  194. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  195. package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
  196. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
  197. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  198. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  199. package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
  200. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
  201. package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
  202. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
  203. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  204. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  205. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
  206. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
  207. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
  208. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
  209. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
  210. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
  211. package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
  212. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
  213. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
  214. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
  215. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  216. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  217. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  218. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  219. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  220. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  221. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  222. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  223. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
  224. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
  225. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  226. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  227. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  229. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  230. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  231. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
  232. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
  233. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  234. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  235. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  236. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  237. package/templates/.agents/templates/task.en.md +18 -1
  238. package/templates/.agents/templates/task.zh-CN.md +18 -1
  239. package/templates/.github/workflows/pr-label.yml +7 -26
@@ -1,14 +1,65 @@
1
1
  import fs from 'node:fs';
2
- import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
3
  import { enumerateArtifacts } from '../task/artifacts.js';
4
4
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
5
5
  import { renderHumanOverrideAudit } from '../task/human-override.js';
6
6
  import { resolveTaskRef } from '../task/resolve-ref.js';
7
- import { resolvePlatformContext } from './context.js';
8
- import { createGitHubClient } from './github-client.js';
9
- import { listRemoteComments, normalizeCommentContent, writeComment } from './issue-comments.js';
10
- import { platformResult } from './types.js';
7
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
11
8
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.js';
9
+ import { resolvePlatformProviderContext } from './context.js';
10
+ import { normalizeCommentContent } from './issue-comments.js';
11
+ import { platformResult } from './types.js';
12
+ import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
13
+ import { buildPrChangeReport, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
14
+ function summaryMarker(taskId) {
15
+ return `<!-- sync-pr:${taskId}:summary -->`;
16
+ }
17
+ function escapeSummaryAuditText(value) {
18
+ return value
19
+ .replace(/&/g, '&amp;')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/\r\n?/g, '\n');
23
+ }
24
+ function renderSummaryAuditLine(value) {
25
+ const safe = escapeSummaryAuditText(value);
26
+ const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
27
+ const delimiter = '`'.repeat(longestRun + 1);
28
+ return `${delimiter} ${safe} ${delimiter}`;
29
+ }
30
+ function renderSafeHumanOverrideAudit(value) {
31
+ const normalized = value.trim().replace(/\r\n?/g, '\n');
32
+ if (!normalized)
33
+ return '';
34
+ const [heading, ...rows] = normalized.split('\n');
35
+ const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
36
+ return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
37
+ }
38
+ function isSafeSummaryEnvelope(value, taskId) {
39
+ return value.includes(summaryMarker(taskId))
40
+ && (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
41
+ && (value.match(/<!--\s*last-commit:/gi) || []).length === 1
42
+ && !/<!--\s*canonical-pr-change-report\b/i.test(value);
43
+ }
44
+ function taskReportPath(taskDir) {
45
+ return path.join(taskDir, 'pr-change-report.json');
46
+ }
47
+ function readBoundTaskSnapshot(taskMdPath) {
48
+ const taskContent = fs.readFileSync(taskMdPath, 'utf8');
49
+ const frontmatter = parseTypedTaskFrontmatter(taskContent);
50
+ const fact = readPrDeliveryFact(frontmatter);
51
+ const boundFact = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact : null;
52
+ return { taskContent, boundFact };
53
+ }
54
+ function boundPrNumberForWarning(taskMdPath) {
55
+ try {
56
+ const { boundFact } = readBoundTaskSnapshot(taskMdPath);
57
+ return boundFact ? resourceIdentityNumber(boundFact.identity.resource) : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
12
63
  function warningResultForPrimary(primaryResult) {
13
64
  if (primaryResult === 'pr_created')
14
65
  return 'pr_created_with_warnings';
@@ -16,11 +67,11 @@ function warningResultForPrimary(primaryResult) {
16
67
  return 'pr_reused_with_warnings';
17
68
  return 'no_op_with_warnings';
18
69
  }
19
- function summaryMarker(taskId) {
20
- return `<!-- sync-pr:${taskId}:summary -->`;
70
+ function platformError(error) {
71
+ return { code: error.code, message: error.message, retryable: error.retryable ?? false };
21
72
  }
22
73
  function buildPullRequestSummary(taskId, body, headSha, humanOverrideAudit = '') {
23
- const sections = [body.replace(/\s+$/, ''), humanOverrideAudit.trim()].filter(Boolean).join('\n\n');
74
+ const sections = [body.replace(/\s+$/, ''), renderSafeHumanOverrideAudit(humanOverrideAudit)].filter(Boolean).join('\n\n');
24
75
  return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
25
76
  }
26
77
  function reconcileSummaryComment(comments, taskId, desired) {
@@ -47,26 +98,308 @@ function canonicalArtifacts(taskDir) {
47
98
  if (!current || round > current.round)
48
99
  byFamily.set(family, { family, name: artifact.name, path: artifact.path, round });
49
100
  }
50
- return [...byFamily.values()].map(({ family, name, path }) => ({ family, name, path }));
101
+ return [...byFamily.values()].map(({ family, name, path: artifactPath }) => ({ family, name, path: artifactPath }));
102
+ }
103
+ function basePlatformResult(status, context, taskId, prNumber, error = null) {
104
+ return platformResult(status, {
105
+ platform: context.platform,
106
+ capabilities: context.capabilities,
107
+ operations: context.operations,
108
+ resource: { kind: 'pull-request', number: prNumber },
109
+ error,
110
+ ...(taskId ? { task: { id: taskId, prNumber } } : {})
111
+ });
112
+ }
113
+ function isHardSummaryFailure(error) {
114
+ return error?.code === 'PR_SUMMARY_HEAD_RACE'
115
+ || error?.code === 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED'
116
+ || error?.code === 'PR_SUMMARY_RACE_COMPENSATION_FAILED';
117
+ }
118
+ function inspectionError(error) {
119
+ return error.retryable ? 'blocked' : 'failed';
120
+ }
121
+ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContext) {
122
+ if (!context.platform.repository) {
123
+ return { ok: false, status: context.status, error: context.error };
124
+ }
125
+ const loaded = loadedContext
126
+ ? { ok: true, value: loadedContext }
127
+ : await resolvePlatformProviderContext({ cwd: repoRoot });
128
+ if (!loaded.ok)
129
+ return { ok: false, status: loaded.context.status, error: loaded.context.error };
130
+ const target = providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber));
131
+ const inspected = loaded.value.provider.changeRequests?.inspect
132
+ ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target })
133
+ : unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
134
+ if (!inspected.ok) {
135
+ const error = providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED');
136
+ return { ok: false, status: inspectionError(error), error };
137
+ }
138
+ const remote = inspected.value;
139
+ const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? prNumber;
140
+ const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
141
+ const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
142
+ return {
143
+ ok: true,
144
+ value: {
145
+ repository: context.platform.repository,
146
+ number,
147
+ nodeId: remote.id,
148
+ url: remote.displayUrl || '',
149
+ state: remote.state === 'closed' ? 'closed' : 'open',
150
+ title: remote.title,
151
+ body: remote.body,
152
+ draft: remote.draft ?? false,
153
+ head: { ...head, sha: remote.headSha || head.sha },
154
+ base: { ...base, sha: remote.baseSha || base.sha },
155
+ mergedAt: remote.mergedAt ?? null,
156
+ mergeCommitSha: remote.mergeCommitSha ?? null,
157
+ labels: [...(remote.labels || [])].sort(),
158
+ assignees: [...(remote.assignees || [])].sort(),
159
+ milestone: remote.milestone ?? null,
160
+ mergeability: remote.mergeability || { state: 'unknown', detail: null }
161
+ }
162
+ };
163
+ }
164
+ function sameIdentity(left, right) {
165
+ return JSON.stringify({
166
+ repository: left.repository,
167
+ number: left.number,
168
+ base: left.base,
169
+ head: left.head
170
+ }) === JSON.stringify({
171
+ repository: right.repository,
172
+ number: right.number,
173
+ base: right.base,
174
+ head: right.head
175
+ });
176
+ }
177
+ function identityFromSnapshot(snapshot) {
178
+ return {
179
+ repository: snapshot.repository,
180
+ number: snapshot.number,
181
+ base: { ...snapshot.base },
182
+ head: { ...snapshot.head }
183
+ };
184
+ }
185
+ function readJsonInput(file) {
186
+ try {
187
+ return { ok: true, value: JSON.parse(fs.readFileSync(file, 'utf8')) };
188
+ }
189
+ catch (error) {
190
+ const errno = error.code;
191
+ return {
192
+ ok: false,
193
+ error: {
194
+ code: errno === 'ENOENT' ? 'PR_CHANGE_REPORT_INPUT_MISSING' : 'PR_CHANGE_REPORT_INPUT_INVALID',
195
+ message: error instanceof Error ? error.message : String(error),
196
+ retryable: false
197
+ }
198
+ };
199
+ }
200
+ }
201
+ function sameMechanical(left, right) {
202
+ return JSON.stringify(left) === JSON.stringify(right);
203
+ }
204
+ function currentReportCheck(report, snapshot, taskContent, repoRoot) {
205
+ const digest = taskIntentDigest(taskContent);
206
+ if (!digest.ok)
207
+ return { ok: false, error: platformError(digest.error) };
208
+ const reportIdentity = report.identity;
209
+ if (reportIdentity.repository !== snapshot.repository || reportIdentity.number !== snapshot.number ||
210
+ JSON.stringify(reportIdentity.base) !== JSON.stringify(snapshot.base) || JSON.stringify(reportIdentity.head) !== JSON.stringify(snapshot.head))
211
+ return {
212
+ ok: false,
213
+ error: { code: 'PR_CHANGE_REPORT_STALE', message: 'Change report identity does not match the authoritative pull request snapshot', retryable: false }
214
+ };
215
+ if (report.inputs.taskIntentSha256 !== digest.value.sha256)
216
+ return {
217
+ ok: false,
218
+ error: { code: 'PR_CHANGE_REPORT_STALE', message: 'Change report task intent digest is stale', retryable: false }
219
+ };
220
+ let mechanical;
221
+ try {
222
+ mechanical = runMechanicalChangeReport(repoRoot, snapshot.base.sha, snapshot.head.sha);
223
+ }
224
+ catch (error) {
225
+ return { ok: false, error: { code: 'PR_CHANGE_REPORT_GIT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false } };
226
+ }
227
+ const reportMechanical = {
228
+ version: 1,
229
+ base: snapshot.base.sha,
230
+ head: snapshot.head.sha,
231
+ mergeBase: report.diff.mergeBase,
232
+ patchSha256: report.diff.patchSha256,
233
+ files: report.diff.files,
234
+ totals: report.diff.totals
235
+ };
236
+ if (!sameMechanical(reportMechanical, mechanical))
237
+ return {
238
+ ok: false,
239
+ error: { code: 'PR_CHANGE_REPORT_STALE', message: 'Change report patch or mechanical statistics are stale', retryable: false }
240
+ };
241
+ return { ok: true, digest: digest.value.sha256, mechanical };
242
+ }
243
+ function summaryReportStatus(reportPath, snapshot, taskContent, repoRoot) {
244
+ const digest = taskIntentDigest(taskContent);
245
+ if (!digest.ok)
246
+ return { status: 'invalid', taskIntentSha256: null, reason: digest.error.message };
247
+ const report = readPrChangeReport(reportPath);
248
+ if (!report.ok)
249
+ return { status: report.error.code === 'PR_CHANGE_REPORT_MISSING' ? 'missing' : 'invalid', taskIntentSha256: digest.value.sha256, reason: report.error.message };
250
+ if (!snapshot)
251
+ return { status: 'stale', taskIntentSha256: digest.value.sha256, reason: 'No bound pull request snapshot is available' };
252
+ const checked = currentReportCheck(report.value, snapshot, taskContent, repoRoot);
253
+ return checked.ok
254
+ ? { status: 'ready', taskIntentSha256: checked.digest }
255
+ : { status: checked.error.code === 'PR_CHANGE_REPORT_STALE' ? 'stale' : 'invalid', taskIntentSha256: digest.value.sha256, reason: checked.error.message };
51
256
  }
52
- function summaryContext(taskRef, options = {}) {
257
+ async function summaryContext(taskRef, options = {}) {
53
258
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
54
259
  if (!resolved.ok)
55
- return { ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }), task: { id: resolved.taskId, prNumber: null }, artifacts: [] };
56
- const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
57
- const fact = readPrDeliveryFact(frontmatter);
58
- const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null;
59
- return { ...platformResult('no-op'), task: { id: resolved.taskId, prNumber }, artifacts: canonicalArtifacts(resolved.taskDir) };
260
+ return {
261
+ ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }),
262
+ task: { id: resolved.taskId, prNumber: null }, pullRequest: null,
263
+ changeReport: { status: 'invalid', path: '', taskIntentSha256: null, reason: resolved.message }, artifacts: []
264
+ };
265
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
266
+ const frontmatter = parseTypedTaskFrontmatter(taskContent);
267
+ const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
268
+ const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? resourceIdentityNumber(fact.fact.identity.resource) : null;
269
+ const reportPath = taskReportPath(resolved.taskDir);
270
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
271
+ const context = loaded.ok ? loaded.value.context : loaded.context;
272
+ let pullRequest = null;
273
+ if (loaded.ok && prNumber && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
274
+ const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
275
+ if (inspected.ok)
276
+ pullRequest = inspected.value;
277
+ }
278
+ const report = summaryReportStatus(reportPath, pullRequest, taskContent, resolved.repoRoot);
279
+ return {
280
+ ...platformResult(context.status, {
281
+ platform: context.platform,
282
+ capabilities: context.capabilities,
283
+ operations: context.operations,
284
+ error: context.error
285
+ }),
286
+ task: { id: resolved.taskId, prNumber },
287
+ pullRequest,
288
+ changeReport: { status: report.status, path: reportPath, taskIntentSha256: report.taskIntentSha256, ...(report.reason ? { reason: report.reason } : {}) },
289
+ artifacts: canonicalArtifacts(resolved.taskDir)
290
+ };
291
+ }
292
+ async function reportWrite(taskRef, options) {
293
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
294
+ if (!resolved.ok)
295
+ return { ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }), report: null };
296
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
297
+ const context = loaded.ok ? loaded.value.context : loaded.context;
298
+ if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status))
299
+ return { ...context, report: null };
300
+ let knownPrNumber = null;
301
+ try {
302
+ return await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, options.agent, async () => {
303
+ const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
304
+ if (!boundFact)
305
+ return {
306
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
307
+ report: null
308
+ };
309
+ const boundPrNumber = resourceIdentityNumber(boundFact.identity.resource);
310
+ if (!boundPrNumber)
311
+ return {
312
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false }),
313
+ report: null
314
+ };
315
+ knownPrNumber = boundPrNumber;
316
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
317
+ if (!initial.ok)
318
+ return { ...basePlatformResult(initial.status, context, resolved.taskId, boundPrNumber, initial.error), report: null };
319
+ const digest = taskIntentDigest(taskContent);
320
+ if (!digest.ok)
321
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(digest.error)), report: null };
322
+ const mechanicalInput = readJsonInput(path.resolve(resolved.repoRoot, options.mechanicalFile));
323
+ if (!mechanicalInput.ok)
324
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, mechanicalInput.error), report: null };
325
+ const mechanical = validateMechanicalReport(mechanicalInput.value);
326
+ if (!mechanical.ok)
327
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(mechanical.error)), report: null };
328
+ const candidateInput = readJsonInput(path.resolve(resolved.repoRoot, options.precheckFile));
329
+ if (!candidateInput.ok)
330
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, candidateInput.error), report: null };
331
+ const candidate = validatePrecheckCandidate(candidateInput.value);
332
+ if (!candidate.ok)
333
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(candidate.error)), report: null };
334
+ let recomputed;
335
+ try {
336
+ recomputed = runMechanicalChangeReport(resolved.repoRoot, initial.value.base.sha, initial.value.head.sha);
337
+ }
338
+ catch (error) {
339
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, { code: 'PR_CHANGE_REPORT_GIT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }), report: null };
340
+ }
341
+ if (!sameMechanical(mechanical.value, recomputed))
342
+ return {
343
+ ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, { code: 'PR_CHANGE_REPORT_MECHANICAL_MISMATCH', message: 'Mechanical report input does not match the current complete diff', retryable: false }),
344
+ report: null
345
+ };
346
+ const built = buildPrChangeReport(identityFromSnapshot(initial.value), digest.value.sha256, mechanical.value, candidate.value);
347
+ if (!built.ok)
348
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(built.error)), report: null };
349
+ const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
350
+ if (!final.ok)
351
+ return { ...basePlatformResult(final.status, context, resolved.taskId, boundPrNumber, final.error), report: null };
352
+ if (!sameIdentity(initial.value, final.value))
353
+ return {
354
+ ...basePlatformResult('blocked', context, resolved.taskId, boundPrNumber, { code: 'PR_CHANGE_REPORT_HEAD_RACE', message: 'Pull request identity changed while generating the change report', retryable: true }),
355
+ report: null
356
+ };
357
+ const target = taskReportPath(resolved.taskDir);
358
+ const existing = readPrChangeReport(target);
359
+ const reportInfo = {
360
+ path: target,
361
+ precheckVerdict: built.value.precheck.verdict,
362
+ nextAction: built.value.precheck.route
363
+ };
364
+ if (existing.ok && JSON.stringify(existing.value) === JSON.stringify(built.value))
365
+ return {
366
+ ...basePlatformResult('no-op', context, resolved.taskId, boundPrNumber),
367
+ report: { ...reportInfo, status: 'no-op' }
368
+ };
369
+ if (options.dryRun)
370
+ return {
371
+ ...basePlatformResult('planned', context, resolved.taskId, boundPrNumber),
372
+ operations: [{ name: 'change-report:write', status: 'planned', reasonCode: null }],
373
+ report: { ...reportInfo, status: 'planned' }
374
+ };
375
+ try {
376
+ writePrChangeReportAtomic(target, built.value);
377
+ }
378
+ catch (error) {
379
+ return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, { code: 'PR_CHANGE_REPORT_WRITE_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }), report: null };
380
+ }
381
+ return {
382
+ ...basePlatformResult('applied', context, resolved.taskId, boundPrNumber),
383
+ operations: [{ name: 'change-report:write', status: 'applied', reasonCode: null }],
384
+ report: { ...reportInfo, status: 'written' }
385
+ };
386
+ });
387
+ }
388
+ catch (error) {
389
+ const lockError = error instanceof TaskExecutionLockError
390
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
391
+ : { code: 'PR_CHANGE_REPORT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
392
+ return { ...basePlatformResult(lockError.retryable ? 'blocked' : 'failed', context, resolved.taskId, knownPrNumber, lockError), report: null };
393
+ }
60
394
  }
61
- function syncPullRequestSummary(taskRef, options) {
395
+ async function syncPullRequestSummary(taskRef, options) {
62
396
  const warningResult = warningResultForPrimary(options.primaryResult);
63
397
  let knownPrNumber = null;
64
398
  const softenFailure = (output) => {
65
399
  const prNumber = output.resource.kind === 'pull-request' && output.resource.number
66
400
  ? output.resource.number
67
401
  : knownPrNumber;
68
- const warning = output.error && prNumber
69
- && output.error.code !== 'PR_NOT_LINKED'
402
+ const warning = output.error && prNumber && output.error.code !== 'PR_NOT_LINKED'
70
403
  ? {
71
404
  code: output.error.code,
72
405
  message: output.error.message,
@@ -88,45 +421,182 @@ function syncPullRequestSummary(taskRef, options) {
88
421
  }
89
422
  : { ...output, result: null, warnings: [] };
90
423
  };
424
+ const preserveFailure = (output) => options.strict
425
+ ? { ...output, result: null, warnings: [] }
426
+ : softenFailure(output);
91
427
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
92
428
  if (!resolved.ok)
93
429
  return softenFailure(platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }));
430
+ const fail = (status, context, error, prNumber = knownPrNumber) => {
431
+ const output = basePlatformResult(status, context, resolved.taskId, prNumber, error);
432
+ return options.strict || isHardSummaryFailure(error)
433
+ ? { ...output, result: null, warnings: [] }
434
+ : { ...softenFailure(output) };
435
+ };
94
436
  const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
95
- const fact = readPrDeliveryFact(frontmatter);
437
+ const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
96
438
  if (fact.status === 'invalid')
97
- return softenFailure(platformResult('failed', { error: { code: 'PR_DELIVERY_FACT_INVALID', message: fact.error.message, retryable: false } }));
98
- const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null;
99
- if (!prNumber)
100
- return softenFailure(platformResult('failed', { error: { code: fact.status === 'missing' ? 'PR_DELIVERY_FACT_MISSING' : 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }));
101
- knownPrNumber = prNumber;
102
- const client = options.client || createGitHubClient();
103
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client });
104
- if (!context.platform.repository || !['no-op', 'degraded'].includes(context.status))
105
- return softenFailure(context);
106
- let headSha;
439
+ return preserveFailure(platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }));
440
+ if (!options.changeReportFile)
441
+ return preserveFailure(platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_MISSING', message: 'summary-sync requires --change-report-file', retryable: false } }));
442
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
443
+ const context = loaded.ok ? loaded.value.context : loaded.context;
444
+ if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status)) {
445
+ // This diagnostic-only read preserves the known PR target when context resolution fails before the execution lock.
446
+ knownPrNumber = boundPrNumberForWarning(resolved.taskMdPath);
447
+ return preserveFailure(context);
448
+ }
107
449
  try {
108
- headSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: resolved.repoRoot, encoding: 'utf8' }).trim();
450
+ return await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, options.agent, async () => {
451
+ const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
452
+ if (!boundFact)
453
+ return {
454
+ ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
455
+ result: null, warnings: []
456
+ };
457
+ const prIdentity = boundFact.identity.resource;
458
+ const boundPrNumber = resourceIdentityNumber(prIdentity);
459
+ if (!boundPrNumber)
460
+ return {
461
+ ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false } }),
462
+ result: null, warnings: []
463
+ };
464
+ knownPrNumber = boundPrNumber;
465
+ const prNumber = boundPrNumber;
466
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
467
+ if (!initial.ok)
468
+ return fail(initial.status, context, initial.error);
469
+ const expectedReportPath = taskReportPath(resolved.taskDir);
470
+ const suppliedReportPath = path.resolve(resolved.repoRoot, options.changeReportFile);
471
+ if (suppliedReportPath !== expectedReportPath)
472
+ return fail('failed', context, { code: 'PR_CHANGE_REPORT_PATH_INVALID', message: 'summary-sync must consume the task-bound pr-change-report.json', retryable: false });
473
+ const report = readPrChangeReport(suppliedReportPath);
474
+ if (!report.ok)
475
+ return fail('failed', context, platformError(report.error));
476
+ const checked = currentReportCheck(report.value, initial.value, taskContent, resolved.repoRoot);
477
+ if (!checked.ok)
478
+ return fail('failed', context, checked.error);
479
+ const replaced = replaceCanonicalReportPlaceholder(options.body, report.value);
480
+ if (!replaced.ok)
481
+ return fail('failed', context, platformError(replaced.error));
482
+ const desired = buildPullRequestSummary(resolved.taskId, replaced.value, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
483
+ if (!isSafeSummaryEnvelope(desired, resolved.taskId))
484
+ return fail('failed', context, {
485
+ code: 'PR_SUMMARY_RENDER_INVALID',
486
+ message: 'Summary contains an invalid or duplicated reserved control marker',
487
+ retryable: false
488
+ });
489
+ const listed = loaded.value.provider.comments?.list
490
+ ? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: prIdentity }).then((response) => response.ok
491
+ ? { ok: true, value: response.value.map((comment) => ({ id: /^\d+$/.test(comment.id) ? Number(comment.id) : comment.id, body: comment.body })) }
492
+ : response)
493
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
494
+ if (!listed.ok)
495
+ return fail(providerStatus(listed.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'));
496
+ const reconciliation = reconcileSummaryComment(listed.value, resolved.taskId, desired);
497
+ if (reconciliation.action === 'conflict')
498
+ return fail('failed', context, { code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple PR comments contain the summary marker', retryable: false });
499
+ const previousBody = reconciliation.action === 'update'
500
+ ? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
501
+ : null;
502
+ const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route };
503
+ const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
504
+ if (!beforeWrite.ok)
505
+ return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
506
+ if (!sameIdentity(initial.value, beforeWrite.value))
507
+ return fail('blocked', context, {
508
+ code: 'PR_SUMMARY_HEAD_RACE',
509
+ message: 'Pull request head changed before publishing the summary',
510
+ retryable: true
511
+ }, prNumber);
512
+ if (reconciliation.action === 'no-op')
513
+ return {
514
+ ...basePlatformResult('no-op', context, resolved.taskId, prNumber),
515
+ comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: [reconciliation.commentId], parts: 1 },
516
+ result: null, warnings: [], ...info
517
+ };
518
+ if (options.dryRun)
519
+ return {
520
+ ...basePlatformResult('planned', context, resolved.taskId, prNumber),
521
+ operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
522
+ result: null, warnings: [], ...info
523
+ };
524
+ const existingComment = reconciliation.commentId === null
525
+ ? undefined
526
+ : providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
527
+ const written = loaded.value.provider.comments?.write
528
+ ? await loaded.value.provider.comments.write({
529
+ context: providerOperationContext(loaded.value),
530
+ parent: prIdentity,
531
+ body: desired,
532
+ ...(existingComment ? { existingComment } : {}),
533
+ mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
534
+ })
535
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
536
+ if (!written.ok)
537
+ return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
538
+ const after = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
539
+ const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
540
+ const compensateWrittenSummary = async () => reconciliation.action === 'create'
541
+ ? written.value.remoteId && loaded.value.provider.comments?.delete
542
+ ? await loaded.value.provider.comments.delete({
543
+ context: providerOperationContext(loaded.value),
544
+ parent: prIdentity,
545
+ comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
546
+ mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
547
+ })
548
+ : { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
549
+ : reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
550
+ ? await loaded.value.provider.comments.write({
551
+ context: providerOperationContext(loaded.value),
552
+ parent: prIdentity,
553
+ body: previousBody,
554
+ existingComment: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId)),
555
+ mutation: { idempotencyKey: `pr-summary-restore:${resolved.taskId}` }
556
+ })
557
+ : { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Previous summary body is unavailable for restoration', retryable: true } };
558
+ if (!after.ok) {
559
+ const compensated = await compensateWrittenSummary();
560
+ if (!compensated.ok)
561
+ return fail('blocked', context, {
562
+ code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
563
+ message: `Summary post-write verification failed and compensation failed: ${compensated.error.message}`,
564
+ retryable: true
565
+ }, prNumber);
566
+ return fail('blocked', context, {
567
+ code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
568
+ message: `Summary was written but pull-request verification failed: ${after.error?.message || 'unknown verification error'}; the previous summary was restored or the new comment was deleted`,
569
+ retryable: true
570
+ }, prNumber);
571
+ }
572
+ if (!sameIdentity(initial.value, after.value)) {
573
+ const compensated = await compensateWrittenSummary();
574
+ if (!compensated.ok)
575
+ return fail('blocked', context, {
576
+ code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
577
+ message: `Pull-request head changed and summary compensation failed: ${compensated.error.message}`,
578
+ retryable: true
579
+ }, prNumber);
580
+ return fail('blocked', context, {
581
+ code: 'PR_SUMMARY_HEAD_RACE',
582
+ message: 'Pull-request head changed while publishing; the previous summary was restored or the new comment was deleted; rerun report and summary sync',
583
+ retryable: true
584
+ }, prNumber);
585
+ }
586
+ return {
587
+ ...basePlatformResult('applied', context, resolved.taskId, prNumber),
588
+ comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 },
589
+ operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
590
+ result: null, warnings: [], ...info
591
+ };
592
+ });
109
593
  }
110
594
  catch (error) {
111
- return softenFailure(platformResult('failed', { platform: context.platform, capabilities: context.capabilities, error: { code: 'GIT_HEAD_UNRESOLVED', message: error instanceof Error ? error.message : String(error), retryable: false } }));
595
+ const lockError = error instanceof TaskExecutionLockError
596
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
597
+ : { code: 'PR_SUMMARY_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
598
+ return fail(lockError.retryable ? 'blocked' : 'failed', context, lockError);
112
599
  }
113
- const desired = buildPullRequestSummary(resolved.taskId, options.body, headSha, renderHumanOverrideAudit(fs.readFileSync(resolved.taskMdPath, 'utf8')));
114
- const listed = listRemoteComments(client, context.platform.repository, prNumber, resolved.repoRoot);
115
- if (!listed.ok)
116
- return softenFailure(platformResult(listed.error.retryable ? 'blocked' : 'failed', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: listed.error }));
117
- const reconciliation = reconcileSummaryComment(listed.value, resolved.taskId, desired);
118
- if (reconciliation.action === 'conflict')
119
- return softenFailure(platformResult('failed', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: { code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple PR comments contain the summary marker', retryable: false } }));
120
- if (reconciliation.action === 'no-op')
121
- return softenFailure(platformResult('no-op', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: [reconciliation.commentId], parts: 1 }, error: null }));
122
- if (options.dryRun)
123
- return softenFailure(platformResult('planned', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }], error: null }));
124
- const written = writeComment(client, context.platform.repository, prNumber, resolved.repoRoot, desired, reconciliation.commentId || undefined);
125
- if (!written.ok)
126
- return softenFailure(platformResult(written.error.retryable ? 'blocked' : 'failed', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: written.error }));
127
- const id = Number(written.value.id || reconciliation.commentId);
128
- return softenFailure(platformResult('applied', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 }, operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }], error: null }));
129
- }
130
- export { buildPullRequestSummary, reconcileSummaryComment, summaryContext, summaryMarker, syncPullRequestSummary };
131
- export { warningResultForPrimary };
600
+ }
601
+ export { buildPullRequestSummary, reconcileSummaryComment, reportWrite, summaryContext, summaryMarker, syncPullRequestSummary, warningResultForPrimary };
132
602
  //# sourceMappingURL=pr-summary.js.map