@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
@@ -2,19 +2,22 @@ import fs from 'node:fs';
2
2
 
3
3
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
4
4
  import { resolveTaskRef } from '../task/resolve-ref.ts';
5
- import {
6
- inspectPlatformRequiredChecks,
7
- registerPlatformCapabilities
8
- } from './adapters.ts';
9
5
  import type { PlatformCheckSnapshot } from './adapters.ts';
10
- import { resolvePlatformContext } from './context.ts';
11
- import { createGitHubClient } from './github-client.ts';
12
- import type { GitHubClient } from './github-client.ts';
6
+ import { resolvePlatformProviderContext } from './context.ts';
7
+ import type { PlatformClient } from './context.ts';
13
8
  import { inspectPlatformPullRequest } from './pull-requests.ts';
14
9
  import { platformResult } from './types.ts';
15
10
  import type { PlatformResult } from './types.ts';
16
11
  import type { PullRequestSnapshot } from './pull-requests.ts';
17
12
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
13
+ import {
14
+ providerError,
15
+ providerOperationContext,
16
+ providerStatus,
17
+ resourceIdentity,
18
+ resourceIdentityNumber,
19
+ unsupportedProviderOperation
20
+ } from './provider-bridge.ts';
18
21
 
19
22
  type CheckBucket = 'pass' | 'fail' | 'pending' | 'cancel';
20
23
  type CheckState = 'passed' | 'failed' | 'pending' | 'timed-out' | 'cancelled' | 'no-required';
@@ -30,8 +33,9 @@ type ChecksResult = PlatformResult & {
30
33
  resolution?: { status: 'resolved' | 'missing' | 'ambiguous'; runId: number | null; jobId: number | null };
31
34
  logs?: { runId: number; jobId?: number; text: string };
32
35
  };
33
- type InspectionOptions = { cwd?: string; client?: unknown };
34
- type SharedOptions = { cwd?: string; client?: GitHubClient };
36
+ type GitHubClient = PlatformClient;
37
+ type InspectionOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
38
+ type SharedOptions = { cwd?: string; client?: PlatformClient };
35
39
 
36
40
  function classifyRequiredChecks(required: CheckSnapshot[]): ChecksSnapshot {
37
41
  if (required.length === 0) return { state: 'no-required', required };
@@ -81,8 +85,8 @@ function checksResult(status: PlatformResult['status'], overrides: Partial<Check
81
85
  };
82
86
  }
83
87
 
84
- function normalizeBucket(value: { bucket?: string; state?: string; conclusion?: string }): CheckBucket {
85
- const raw = String(value.bucket || value.conclusion || value.state || '').toLowerCase();
88
+ function normalizeBucket(value: { bucket?: string; status?: string; state?: string; conclusion?: string }): CheckBucket {
89
+ const raw = String(value.bucket || value.status || value.conclusion || value.state || '').toLowerCase();
86
90
  if (['pass', 'success', 'successful', 'neutral'].includes(raw)) return 'pass';
87
91
  if (['fail', 'failure', 'failed', 'error', 'timed_out', 'action_required'].includes(raw)) return 'fail';
88
92
  if (['cancel', 'cancelled', 'canceled', 'skipped', 'stale'].includes(raw)) return 'cancel';
@@ -106,85 +110,81 @@ function normalizeChecks(value: unknown): CheckSnapshot[] {
106
110
  });
107
111
  }
108
112
 
109
- registerPlatformCapabilities('github', {
110
- inspectRequiredChecks({ client, repository, number, cwd }) {
111
- const github = (client as GitHubClient | undefined) || createGitHubClient();
112
- const inspected = github.json<unknown>([
113
- 'pr', 'checks', String(number), '--repo', repository,
114
- '--json', 'name,state,bucket,link,workflow,startedAt,completedAt'
115
- ], { cwd });
116
- return inspected.ok
117
- ? { ok: true, value: normalizeChecks(inspected.value) }
118
- : { ok: false, error: inspected.error };
119
- }
120
- });
113
+ function inspectGitHubRequiredChecks(client: GitHubClient, repository: string, number: number, cwd: string) {
114
+ const inspected = client.json<unknown>([
115
+ 'pr', 'checks', String(number), '--repo', repository,
116
+ '--json', 'name,state,bucket,link,workflow,startedAt,completedAt'
117
+ ], { cwd });
118
+ return inspected.ok
119
+ ? { ok: true as const, value: normalizeChecks(inspected.value) }
120
+ : { ok: false as const, error: inspected.error };
121
+ }
121
122
 
122
- function resolvedTask(taskRef: string, options: InspectionOptions) {
123
+ async function resolvedTask(taskRef: string, options: InspectionOptions) {
123
124
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
124
125
  if (!resolved.ok) return { ok: false as const, output: checksResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }) };
125
126
  const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
126
- const fact = readPrDeliveryFact(frontmatter);
127
- if (fact.status === 'invalid') return { ok: false as const, output: checksResult('failed', { error: { code: 'PR_DELIVERY_FACT_INVALID', message: fact.error.message, retryable: false } }) };
128
- const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null;
129
- if (!prNumber) return { ok: false as const, output: checksResult('failed', { error: { code: fact.status === 'missing' ? 'PR_DELIVERY_FACT_MISSING' : 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }) };
130
- const client = (options.client as GitHubClient | undefined) || createGitHubClient();
131
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client });
127
+ const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
128
+ if (fact.status === 'invalid') return { ok: false as const, output: checksResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }) };
129
+ const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
130
+ const prNumber = resourceIdentityNumber(prIdentity);
131
+ if (!prIdentity) return { ok: false as const, output: checksResult('failed', { error: { code: fact.status === 'missing' ? 'PR_DELIVERY_FACT_MISSING' : 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }) };
132
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
133
+ const context = loaded.ok ? loaded.value.context : loaded.context;
132
134
  if (!context.platform.repository || !['no-op', 'degraded'].includes(context.status)) return { ok: false as const, output: checksResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error }) };
133
- const inspected = inspectPlatformPullRequest(taskRef, { cwd: resolved.repoRoot, client });
135
+ const inspected = await inspectPlatformPullRequest(taskRef, { cwd: resolved.repoRoot, client: options.client });
134
136
  if (!inspected.pullRequest) return { ok: false as const, output: checksResult(inspected.status, { platform: context.platform, capabilities: context.capabilities, error: inspected.error }) };
135
- return { ok: true as const, resolved, prNumber, client, context, pullRequest: inspected.pullRequest };
137
+ if (!loaded.ok) return { ok: false as const, output: checksResult('failed', { platform: context.platform, capabilities: context.capabilities, error: context.error }) };
138
+ return { ok: true as const, resolved, prNumber, prIdentity, client: options.client, context, pullRequest: inspected.pullRequest, provider: loaded.value.provider, loadedContext: loaded.value };
136
139
  }
137
140
 
138
- function inspectRequiredChecks(taskRef: string, options: InspectionOptions = {}): ChecksResult {
139
- const base = resolvedTask(taskRef, options);
141
+ async function inspectRequiredChecks(taskRef: string, options: InspectionOptions = {}): Promise<ChecksResult> {
142
+ const base = await resolvedTask(taskRef, options);
140
143
  if (!base.ok) return base.output;
141
144
  return inspectChecksForResolvedTask(base, options);
142
145
  }
143
146
 
144
- function inspectChecksForResolvedTask(
145
- base: Extract<ReturnType<typeof resolvedTask>, { ok: true }>,
147
+ async function inspectChecksForResolvedTask(
148
+ base: Extract<Awaited<ReturnType<typeof resolvedTask>>, { ok: true }>,
146
149
  options: InspectionOptions
147
- ): ChecksResult {
150
+ ): Promise<ChecksResult> {
148
151
  const repository = base.context.platform.repository!;
149
- const inspected = inspectPlatformRequiredChecks(base.context.platform.type, {
150
- cwd: base.resolved.repoRoot,
151
- repository,
152
- number: base.prNumber,
153
- headSha: base.pullRequest.head.sha,
154
- client: options.client || base.client
155
- });
156
- if (!inspected.ok || !inspected.value) {
157
- const error = inspected.error || {
158
- code: 'REQUIRED_CHECKS_INSPECTION_INVALID',
159
- message: 'Platform adapter returned no required-checks snapshot',
160
- retryable: false
161
- };
162
- return checksResult(error.retryable ? 'blocked' : 'failed', {
163
- platform: base.context.platform, capabilities: base.context.capabilities,
152
+ {
153
+ const inspected = base.provider.checks?.inspectRequired
154
+ ? await base.provider.checks.inspectRequired({
155
+ context: providerOperationContext(base.loadedContext),
156
+ changeRequest: base.prIdentity,
157
+ headSha: base.pullRequest.head.sha
158
+ })
159
+ : unsupportedProviderOperation(base.provider, 'checks.inspectRequired');
160
+ if (!inspected.ok) return checksResult(providerStatus(inspected.error), {
161
+ platform: base.context.platform,
162
+ capabilities: base.context.capabilities,
164
163
  resource: { kind: 'pull-request', number: base.prNumber },
165
- pullRequest: base.pullRequest, error
164
+ pullRequest: base.pullRequest,
165
+ error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
166
+ });
167
+ const required = normalizeChecks(inspected.value);
168
+ const classified = classifyRequiredChecks(required);
169
+ const status = classified.state === 'passed' || classified.state === 'no-required'
170
+ ? 'no-op'
171
+ : classified.state === 'failed' || classified.state === 'cancelled' ? 'failed' : 'blocked';
172
+ return checksResult(status, {
173
+ platform: base.context.platform, capabilities: base.context.capabilities,
174
+ resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
175
+ checks: classified,
176
+ error: status === 'no-op' ? null : {
177
+ code: classified.state === 'pending' ? 'REQUIRED_CHECKS_PENDING' : `REQUIRED_CHECKS_${classified.state.toUpperCase()}`,
178
+ message: `Required checks are ${classified.state}`, retryable: classified.state === 'pending'
179
+ }
166
180
  });
167
181
  }
168
- const classified = classifyRequiredChecks(inspected.value);
169
- const status = classified.state === 'passed' || classified.state === 'no-required'
170
- ? 'no-op'
171
- : classified.state === 'failed' || classified.state === 'cancelled' ? 'failed' : 'blocked';
172
- return checksResult(status, {
173
- platform: base.context.platform, capabilities: base.context.capabilities,
174
- resource: { kind: 'pull-request', number: base.prNumber },
175
- pullRequest: base.pullRequest, checks: classified,
176
- error: status === 'no-op' ? null : {
177
- code: classified.state === 'pending' ? 'REQUIRED_CHECKS_PENDING' : `REQUIRED_CHECKS_${classified.state.toUpperCase()}`,
178
- message: `Required checks are ${classified.state}`,
179
- retryable: classified.state === 'pending'
180
- }
181
- });
182
182
  }
183
183
 
184
- function inspectPullRequestReadiness(taskRef: string, options: InspectionOptions = {}): ChecksResult {
185
- const base = resolvedTask(taskRef, options);
184
+ async function inspectPullRequestReadiness(taskRef: string, options: InspectionOptions = {}): Promise<ChecksResult> {
185
+ const base = await resolvedTask(taskRef, options);
186
186
  if (!base.ok) return base.output;
187
- const checked = inspectChecksForResolvedTask(base, options);
187
+ const checked = await inspectChecksForResolvedTask(base, options);
188
188
  const classifiedCodes = new Set(['REQUIRED_CHECKS_PENDING', 'REQUIRED_CHECKS_FAILED', 'REQUIRED_CHECKS_CANCELLED']);
189
189
  if (checked.error && !classifiedCodes.has(checked.error.code)) return checked;
190
190
  const readiness = classifyPullRequestReadiness({
@@ -226,7 +226,7 @@ async function watchPullRequestReadiness(taskRef: string, options: InspectionOpt
226
226
  readiness: { state: 'cancelled', headSha: last?.pullRequest?.head.sha ?? '' },
227
227
  error: { code: 'PR_READINESS_CANCELLED', message: 'Pull request readiness watch was cancelled', retryable: false }
228
228
  });
229
- last = inspectPullRequestReadiness(taskRef, options);
229
+ last = await inspectPullRequestReadiness(taskRef, options);
230
230
  if (last.readiness?.state !== 'pending' || last.error?.code !== 'PR_READINESS_PENDING') return last;
231
231
  const elapsed = now() - started;
232
232
  if (elapsed >= options.deadlineSeconds * 1000) return checksResult('blocked', {
@@ -239,33 +239,40 @@ async function watchPullRequestReadiness(taskRef: string, options: InspectionOpt
239
239
  }
240
240
  }
241
241
 
242
- function resolvePlatformCheckRun(taskRef: string, options: SharedOptions & { checkName: string; detailsUrl?: string }): ChecksResult {
243
- const base = resolvedTask(taskRef, options);
242
+ async function resolvePlatformCheckRun(taskRef: string, options: SharedOptions & { checkName: string; detailsUrl?: string }): Promise<ChecksResult> {
243
+ const base = await resolvedTask(taskRef, options);
244
244
  if (!base.ok) return base.output;
245
- const direct = options.detailsUrl ? parseRunJobIdentity(options.detailsUrl) : null;
246
- if (direct) {
247
- const run = base.client.json<{ id?: number; head_sha?: string }>(['api', `repos/${base.context.platform.repository}/actions/runs/${direct.runId}`], { cwd: base.resolved.repoRoot });
248
- if (!run.ok) return checksResult(run.error.retryable ? 'blocked' : 'failed', { platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: base.pullRequest, error: run.error });
249
- if (run.value.head_sha !== base.pullRequest.head.sha) return checksResult('failed', { error: { code: 'CHECK_RUN_HEAD_MISMATCH', message: 'Resolved run does not belong to the PR head SHA', retryable: false } });
250
- return checksResult('no-op', { platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest, resolution: { status: 'resolved', ...direct }, error: null });
245
+ {
246
+ const resolved = base.provider.checks?.resolveRun
247
+ ? await base.provider.checks.resolveRun({
248
+ context: providerOperationContext(base.loadedContext),
249
+ changeRequest: base.prIdentity,
250
+ checkName: options.checkName,
251
+ ...(options.detailsUrl ? { detailsUrl: options.detailsUrl } : {})
252
+ })
253
+ : unsupportedProviderOperation(base.provider, 'checks.resolveRun');
254
+ if (!resolved.ok) return checksResult(providerStatus(resolved.error), {
255
+ platform: base.context.platform, capabilities: base.context.capabilities,
256
+ resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
257
+ error: providerError(resolved.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
258
+ });
259
+ const runId = Number(resolved.value.runId);
260
+ if (!Number.isSafeInteger(runId) || runId <= 0) return checksResult('failed', {
261
+ platform: base.context.platform, capabilities: base.context.capabilities,
262
+ resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
263
+ error: { code: 'CHECK_RUN_IDENTITY_INVALID', message: 'Provider returned an invalid check run identity', retryable: false }
264
+ });
265
+ const jobId = resolved.value.jobId ? Number(resolved.value.jobId) : null;
266
+ return checksResult('no-op', {
267
+ platform: base.context.platform, capabilities: base.context.capabilities,
268
+ resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
269
+ resolution: { status: 'resolved', runId, jobId: jobId !== null && Number.isSafeInteger(jobId) && jobId > 0 ? jobId : null }, error: null
270
+ });
251
271
  }
252
- const listed = base.client.json<{ check_runs?: Array<{ name?: string; details_url?: string }> }>(['api', `repos/${base.context.platform.repository}/commits/${base.pullRequest.head.sha}/check-runs`], { cwd: base.resolved.repoRoot });
253
- if (!listed.ok) return checksResult(listed.error.retryable ? 'blocked' : 'failed', { error: listed.error });
254
- const candidates = (listed.value.check_runs || []).flatMap((check) => {
255
- if (check.name !== options.checkName || !check.details_url) return [];
256
- const identity = parseRunJobIdentity(check.details_url);
257
- return identity ? [{ id: identity.runId, jobId: identity.jobId, name: check.name, headSha: base.pullRequest.head.sha }] : [];
258
- });
259
- const resolution = resolveRunCandidate(candidates, base.pullRequest.head.sha, options.checkName);
260
- return checksResult(resolution.status === 'resolved' ? 'no-op' : 'failed', {
261
- platform: base.context.platform, capabilities: base.context.capabilities,
262
- resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
263
- resolution,
264
- error: resolution.status === 'resolved' ? null : { code: resolution.status === 'ambiguous' ? 'CHECK_RUN_AMBIGUOUS' : 'CHECK_RUN_NOT_FOUND', message: `Check run is ${resolution.status}`, retryable: false }
265
- });
266
272
  }
267
273
 
268
274
  function fetchCheckLogText(client: GitHubClient, args: string[], cwd: string) {
275
+ if (!client.text) return { ok: false as const, error: { code: 'PLATFORM_CLIENT_TEXT_UNAVAILABLE', message: 'Platform client does not support text responses', retryable: false } };
269
276
  const fetched = client.text(args, { cwd });
270
277
  if (fetched.ok || args[0] !== 'api' || !/response contains terminal escape sequences/i.test(fetched.error.message)) {
271
278
  return fetched;
@@ -273,20 +280,32 @@ function fetchCheckLogText(client: GitHubClient, args: string[], cwd: string) {
273
280
  return client.text([...args, '--allow-escape-sequences'], { cwd });
274
281
  }
275
282
 
276
- function fetchPlatformCheckLogs(taskRef: string, options: SharedOptions & { run: number; job?: number }): ChecksResult {
277
- const base = resolvedTask(taskRef, options);
283
+ async function fetchPlatformCheckLogs(taskRef: string, options: SharedOptions & { run: number; job?: number }): Promise<ChecksResult> {
284
+ const base = await resolvedTask(taskRef, options);
278
285
  if (!base.ok) return base.output;
279
- const args = options.job
280
- ? ['api', `repos/${base.context.platform.repository}/actions/jobs/${options.job}/logs`]
281
- : ['run', 'view', String(options.run), '--repo', base.context.platform.repository!, '--log-failed'];
282
- const fetched = fetchCheckLogText(base.client, args, base.resolved.repoRoot);
283
- if (!fetched.ok) return checksResult(fetched.error.retryable ? 'blocked' : 'failed', { platform: base.context.platform, capabilities: base.context.capabilities, error: fetched.error });
284
- if (!fetched.value) return checksResult('failed', { error: { code: 'CHECK_LOGS_MISSING', message: 'No failed logs are available', retryable: false } });
285
- return checksResult('no-op', {
286
- platform: base.context.platform, capabilities: base.context.capabilities,
287
- resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
288
- logs: { runId: options.run, ...(options.job ? { jobId: options.job } : {}), text: fetched.value }, error: null
289
- });
286
+ {
287
+ const fetched = base.provider.checks?.fetchLogs
288
+ ? await base.provider.checks.fetchLogs({
289
+ context: providerOperationContext(base.loadedContext),
290
+ runId: String(options.run),
291
+ ...(options.job ? { jobId: String(options.job) } : {})
292
+ })
293
+ : unsupportedProviderOperation(base.provider, 'checks.fetchLogs');
294
+ if (!fetched.ok) return checksResult(providerStatus(fetched.error), {
295
+ platform: base.context.platform, capabilities: base.context.capabilities,
296
+ resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
297
+ error: providerError(fetched.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
298
+ });
299
+ if (!fetched.value.text) return checksResult('failed', {
300
+ platform: base.context.platform, capabilities: base.context.capabilities,
301
+ error: { code: 'CHECK_LOGS_MISSING', message: 'No failed logs are available', retryable: false }
302
+ });
303
+ return checksResult('no-op', {
304
+ platform: base.context.platform, capabilities: base.context.capabilities,
305
+ resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
306
+ logs: { runId: options.run, ...(options.job ? { jobId: options.job } : {}), text: fetched.value.text }, error: null
307
+ });
308
+ }
290
309
  }
291
310
 
292
311
  export {
@@ -294,6 +313,7 @@ export {
294
313
  classifyRequiredChecks,
295
314
  fetchCheckLogText,
296
315
  fetchPlatformCheckLogs,
316
+ inspectGitHubRequiredChecks,
297
317
  inspectRequiredChecks,
298
318
  inspectPullRequestReadiness,
299
319
  parseRunJobIdentity,
@@ -1,23 +1,22 @@
1
- import { createGitHubClient } from './github-client.ts';
2
- import type { GitHubClient } from './github-client.ts';
3
- import { resolvePlatformContext } from './context.ts';
1
+ import { resolvePlatformProviderContext } from './context.ts';
2
+ import type { PlatformClient } from './context.ts';
4
3
  import { platformResult } from './types.ts';
5
4
  import type { PlatformOperation, PlatformResult } from './types.ts';
5
+ import {
6
+ providerError,
7
+ providerOperationContext,
8
+ providerStatus,
9
+ providerResourceToken,
10
+ unsupportedProviderOperation
11
+ } from './provider-bridge.ts';
12
+ import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
13
+ import type { ResourceIdentity } from './resource-identity.ts';
6
14
 
7
15
  export type PrReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES';
8
- export type PrReviewIdentity = { scope: string; round: number; commitSha: string };
9
- export type PrReviewListEntry = { id: number; commitId: string; body: string; url: string };
16
+ export type PrReviewIdentity = { scope: string; round: number; commitSha: string; resource?: ResourceIdentity };
17
+ export type PrReviewListEntry = { id: number | string; commitId: string; body: string; url: string };
10
18
  export type PrReviewListResult = PlatformResult & { reviews: PrReviewListEntry[] };
11
19
 
12
- type RemoteReview = {
13
- id?: number;
14
- node_id?: string;
15
- commit_id?: string;
16
- body?: string | null;
17
- state?: string;
18
- html_url?: string;
19
- };
20
-
21
20
  const REVIEW_EVENTS: readonly PrReviewEvent[] = ['COMMENT', 'APPROVE', 'REQUEST_CHANGES'];
22
21
  // Marker-safe scope: a task id (`TASK-YYYYMMDD-HHMMSS`) or a bare PR number
23
22
  // (`pr{N}`). Any other value (e.g. one containing `\r\n` or `-->`) would break
@@ -25,7 +24,10 @@ const REVIEW_EVENTS: readonly PrReviewEvent[] = ['COMMENT', 'APPROVE', 'REQUEST_
25
24
  const REVIEW_SCOPE_PATTERN = /^(?:pr\d+|TASK-\d{8}-\d{6})$/;
26
25
 
27
26
  export function reviewMarker(identity: PrReviewIdentity): string {
28
- return `<!-- review-pr:${identity.scope}:r${identity.round} -->`;
27
+ const scope = identity.resource && isResourceIdentity(identity.resource) && identity.resource.kind !== 'number'
28
+ ? resourceReviewMarker(identity.resource)
29
+ : identity.scope;
30
+ return `<!-- review-pr:${scope}:r${identity.round} -->`;
29
31
  }
30
32
 
31
33
  export function reviewedCommitMarker(sha: string): string {
@@ -44,105 +46,98 @@ function hasUsableContext(context: PlatformResult): boolean {
44
46
  return (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository !== null;
45
47
  }
46
48
 
47
- function invalidPrNumber(prNumber: number): boolean {
48
- return !Number.isInteger(prNumber) || prNumber <= 0;
49
- }
49
+ type PrToken = string | number;
50
50
 
51
- export function listPrReviews(prNumber: number, options: { cwd?: string; client?: GitHubClient } = {}): PrReviewListResult {
52
- const client = options.client || createGitHubClient();
53
- const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
51
+ export async function listPrReviews(prNumber: PrToken, options: { cwd?: string; client?: PlatformClient } = {}): Promise<PrReviewListResult> {
52
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
53
+ const context = loaded.ok ? loaded.value.context : loaded.context;
54
54
  if (!hasUsableContext(context)) {
55
55
  return { ...platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error }), reviews: [] };
56
56
  }
57
- const repository = context.platform.repository!;
58
- if (invalidPrNumber(prNumber)) {
57
+ const identity = loaded.ok ? (() => { try { return providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)); } catch { return null; } })() : null;
58
+ if (!identity) {
59
59
  return {
60
60
  ...platformResult('failed', {
61
61
  platform: context.platform, capabilities: context.capabilities,
62
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
62
+ error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: 'Pull request token is invalid', retryable: false }
63
63
  }),
64
64
  reviews: []
65
65
  };
66
66
  }
67
- const fetched = client.json<RemoteReview[]>(['api', '--paginate', '--slurp', `repos/${repository}/pulls/${prNumber}/reviews?per_page=100`], { cwd: options.cwd || process.cwd() });
68
- if (!fetched.ok) {
67
+ const identityNumber = resourceIdentityNumber(identity);
68
+ if (loaded.ok) {
69
+ const fetched = loaded.value.provider.reviews?.list
70
+ ? await loaded.value.provider.reviews.list({ context: providerOperationContext(loaded.value), changeRequest: identity })
71
+ : unsupportedProviderOperation(loaded.value.provider, 'reviews.list');
72
+ if (!fetched.ok) return {
73
+ ...platformResult(providerStatus(fetched.error), {
74
+ platform: context.platform, capabilities: context.capabilities,
75
+ resource: { kind: 'pull-request', number: identityNumber, identity }, error: providerError(fetched.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
76
+ }), reviews: []
77
+ };
69
78
  return {
70
- ...platformResult(fetched.error.retryable ? 'blocked' : 'failed', {
79
+ ...platformResult('no-op', {
71
80
  platform: context.platform, capabilities: context.capabilities,
72
- resource: { kind: 'pull-request', number: prNumber }, error: fetched.error
81
+ resource: { kind: 'pull-request', number: identityNumber, identity }, error: null
73
82
  }),
74
- reviews: []
83
+ reviews: fetched.value.map((review) => ({ id: review.id, commitId: review.commitSha || '', body: review.body, url: review.displayUrl || '' }))
75
84
  };
76
85
  }
77
- const flat = (fetched.value || []).flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
78
- const reviews: PrReviewListEntry[] = flat
79
- .map((review) => ({
80
- id: review.id ?? 0,
81
- commitId: review.commit_id ?? '',
82
- body: review.body ?? '',
83
- url: review.html_url ?? ''
84
- }))
85
- .filter((review) => review.body !== '');
86
- return {
87
- ...platformResult('no-op', {
88
- platform: context.platform, capabilities: context.capabilities,
89
- resource: { kind: 'pull-request', number: prNumber }, error: null
90
- }),
91
- reviews
92
- };
86
+ return { ...platformResult('failed', { platform: context.platform, capabilities: context.capabilities, error: context.error }), reviews: [] };
93
87
  }
94
88
 
95
- export function publishPrReview(options: {
89
+ export async function publishPrReview(options: {
96
90
  cwd?: string;
97
- client?: GitHubClient;
91
+ client?: PlatformClient;
98
92
  dryRun?: boolean;
99
- prNumber: number;
93
+ prNumber: PrToken;
100
94
  identity: PrReviewIdentity;
101
95
  event: PrReviewEvent;
102
96
  body: string;
103
- }): PlatformResult {
104
- const client = options.client || createGitHubClient();
105
- const context = resolvePlatformContext({ cwd: options.cwd || process.cwd(), client });
97
+ }): Promise<PlatformResult> {
98
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
99
+ const context = loaded.ok ? loaded.value.context : loaded.context;
106
100
  if (!hasUsableContext(context)) {
107
101
  return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
108
102
  }
109
- const repository = context.platform.repository!;
110
- if (invalidPrNumber(options.prNumber)) {
103
+ const identity = loaded.ok ? (() => { try { return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber)); } catch { return null; } })() : null;
104
+ if (!identity) {
111
105
  return platformResult('failed', {
112
106
  platform: context.platform, capabilities: context.capabilities,
113
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
107
+ error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: 'Pull request token is invalid', retryable: false }
114
108
  });
115
109
  }
110
+ const identityNumber = resourceIdentityNumber(identity);
116
111
  if (!REVIEW_EVENTS.includes(options.event)) {
117
112
  return platformResult('failed', {
118
113
  platform: context.platform, capabilities: context.capabilities,
119
- resource: { kind: 'pull-request', number: options.prNumber },
114
+ resource: { kind: 'pull-request', number: identityNumber, identity },
120
115
  error: { code: 'REVIEW_EVENT_INVALID', message: `review event must be one of ${REVIEW_EVENTS.join('|')}`, retryable: false }
121
116
  });
122
117
  }
123
118
  if (!REVIEW_SCOPE_PATTERN.test(options.identity.scope) || !/^[0-9a-f]{7,40}$/i.test(options.identity.commitSha)) {
124
119
  return platformResult('failed', {
125
120
  platform: context.platform, capabilities: context.capabilities,
126
- resource: { kind: 'pull-request', number: options.prNumber },
121
+ resource: { kind: 'pull-request', number: identityNumber, identity },
127
122
  error: { code: 'REVIEW_IDENTITY_INVALID', message: 'review scope must be a task id or pr{N}; commitSha is required', retryable: false }
128
123
  });
129
124
  }
130
125
 
131
- const marker = reviewMarker(options.identity);
132
- const listed = listPrReviews(options.prNumber, { cwd: options.cwd, client });
126
+ const marker = reviewMarker({ ...options.identity, resource: identity });
127
+ const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
133
128
  if (listed.status === 'failed' || listed.status === 'blocked') return listed;
134
129
  const existing = listed.reviews.find((review) => firstLine(review.body) === marker) ?? null;
135
130
  if (existing) {
136
131
  if (existing.commitId === options.identity.commitSha) {
137
132
  return platformResult('no-op', {
138
133
  platform: context.platform, capabilities: context.capabilities,
139
- resource: { kind: 'pull-request', number: options.prNumber },
134
+ resource: { kind: 'pull-request', number: identityNumber, identity },
140
135
  operations: [{ name: 'review:publish', status: 'no-op', reasonCode: null }], error: null
141
136
  });
142
137
  }
143
138
  return platformResult('failed', {
144
139
  platform: context.platform, capabilities: context.capabilities,
145
- resource: { kind: 'pull-request', number: options.prNumber },
140
+ resource: { kind: 'pull-request', number: identityNumber, identity },
146
141
  operations: [{ name: 'review:publish', status: 'failed', reasonCode: 'REVIEW_MARKER_CONFLICT' }],
147
142
  error: { code: 'REVIEW_MARKER_CONFLICT', message: 'A review with the same marker targets a different commit; start a new round', retryable: false }
148
143
  });
@@ -159,43 +154,51 @@ export function publishPrReview(options: {
159
154
  if (options.dryRun) {
160
155
  return platformResult('planned', {
161
156
  platform: context.platform, capabilities: context.capabilities,
162
- resource: { kind: 'pull-request', number: options.prNumber },
157
+ resource: { kind: 'pull-request', number: identityNumber, identity },
163
158
  operations: [{ name: 'review:publish', status: 'planned', reasonCode: null }], error: null
164
159
  });
165
160
  }
166
161
 
167
- const posted = client.json<RemoteReview>(['api', `repos/${repository}/pulls/${options.prNumber}/reviews`, '-X', 'POST', '--input', '-'], {
168
- cwd: options.cwd || process.cwd(),
169
- method: 'POST',
170
- input: JSON.stringify({ commit_id: options.identity.commitSha, body: wrappedBody, event: options.event })
171
- });
172
- if (!posted.ok) {
173
- if (posted.error.retryable) {
174
- const reconciled = listPrReviews(options.prNumber, { cwd: options.cwd, client });
175
- const found = reconciled.reviews.find((review) => firstLine(review.body) === marker);
176
- if (found) {
177
- return platformResult('applied', {
162
+ if (loaded.ok) {
163
+ const published = loaded.value.provider.reviews?.publish
164
+ ? await loaded.value.provider.reviews.publish({
165
+ context: providerOperationContext(loaded.value),
166
+ changeRequest: identity,
167
+ identity: options.identity,
168
+ event: options.event,
169
+ body: wrappedBody,
170
+ mutation: { idempotencyKey: `review:publish:${marker}` }
171
+ })
172
+ : unsupportedProviderOperation(loaded.value.provider, 'reviews.publish');
173
+ if (!published.ok) {
174
+ if (published.error.retryable) {
175
+ const reconciled = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
176
+ const found = reconciled.reviews.find((review) => firstLine(review.body) === marker);
177
+ if (found) return platformResult('applied', {
178
178
  changed: true, platform: context.platform, capabilities: context.capabilities,
179
- resource: { kind: 'pull-request', number: options.prNumber },
179
+ resource: { kind: 'pull-request', number: identityNumber, identity },
180
180
  operations: [{ name: 'review:publish', status: 'applied', reasonCode: 'CREATE_RECONCILED' }], error: null
181
181
  });
182
+ return platformResult('blocked', {
183
+ platform: context.platform, capabilities: context.capabilities,
184
+ resource: { kind: 'pull-request', number: identityNumber, identity },
185
+ operations: [{ name: 'review:publish', status: 'failed', reasonCode: 'REVIEW_CREATE_OUTCOME_UNKNOWN' }],
186
+ error: { code: 'REVIEW_CREATE_OUTCOME_UNKNOWN', message: published.error.message, retryable: true }
187
+ });
182
188
  }
183
- return platformResult('blocked', {
189
+ return platformResult(providerStatus(published.error), {
184
190
  platform: context.platform, capabilities: context.capabilities,
185
- resource: { kind: 'pull-request', number: options.prNumber },
186
- operations: [{ name: 'review:publish', status: 'failed', reasonCode: 'REVIEW_CREATE_OUTCOME_UNKNOWN' }],
187
- error: { code: 'REVIEW_CREATE_OUTCOME_UNKNOWN', message: posted.error.message, retryable: true }
191
+ resource: { kind: 'pull-request', number: identityNumber, identity },
192
+ operations: [{ name: 'review:publish', status: 'failed', reasonCode: published.error.code }],
193
+ error: providerError(published.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
188
194
  });
189
195
  }
190
- return platformResult('failed', {
196
+ return platformResult('applied', {
197
+ changed: published.value.changed,
191
198
  platform: context.platform, capabilities: context.capabilities,
192
- resource: { kind: 'pull-request', number: options.prNumber },
193
- operations: [{ name: 'review:publish', status: 'failed', reasonCode: posted.error.code }], error: posted.error
199
+ resource: { kind: 'pull-request', number: identityNumber, identity },
200
+ operations: [{ name: 'review:publish', status: 'applied', reasonCode: null }], error: null
194
201
  });
195
202
  }
196
- return platformResult('applied', {
197
- changed: true, platform: context.platform, capabilities: context.capabilities,
198
- resource: { kind: 'pull-request', number: options.prNumber },
199
- operations: [{ name: 'review:publish', status: 'applied', reasonCode: null }], error: null
200
- });
203
+ return platformResult('failed', { platform: context.platform, capabilities: context.capabilities, error: context.error });
201
204
  }