@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
@@ -3,21 +3,22 @@ import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { createHash } from 'node:crypto';
5
5
 
6
- import { resolvePlatformContext } from './context.ts';
7
- import {
8
- fetchGitHubReleaseNoteData,
9
- publishGitHubReleaseNotes
10
- } from './github-release-notes.ts';
11
- import type { ReleaseNoteActor } from './github-release-notes.ts';
12
- import type { GitHubClient } from './github-client.ts';
6
+ import { resolvePlatformProviderContext } from './context.ts';
7
+ import type { PlatformClient } from './context.ts';
8
+ import { providerError, providerOperationContext, providerStatus, unsupportedProviderOperation } from './provider-bridge.ts';
9
+ import { resourceIdentityNumber } from './resource-identity.ts';
10
+ import type { ResourceIdentity } from './resource-identity.ts';
13
11
 
14
12
  type ReleaseNoteOptions = {
15
13
  cwd?: string;
16
14
  platformType?: string;
17
- client?: GitHubClient;
15
+ client?: PlatformClient;
18
16
  };
19
17
 
18
+ type ReleaseNoteActor = { id?: string; name?: string };
20
19
  type ReleaseNoteCommit = { oid: string; title: string; authors: ReleaseNoteActor[] };
20
+ type ReleaseNoteIssue = { number: number; identity?: ResourceIdentity; title: string; url: string; labels: string[]; author: { login: string; bot: boolean } | null };
21
+ type ReleaseNotePullRequest = { number: number; identity?: ResourceIdentity; title: string; body: string; url: string; mergedAt: string; labels: string[]; author: { login: string; bot: boolean } | null; closingIssues: ReleaseNoteIssue[] };
21
22
 
22
23
  const unsupportedError = {
23
24
  code: 'PLATFORM_RELEASE_NOTES_UNSUPPORTED',
@@ -130,7 +131,7 @@ function configuredPlatform(cwd: string): string | null {
130
131
  }
131
132
  }
132
133
 
133
- function releaseNoteContext(
134
+ async function releaseNoteContext(
134
135
  input: { fromTag: string; toTag: string; branch: string; historyLimit?: number },
135
136
  options: ReleaseNoteOptions = {}
136
137
  ) {
@@ -140,7 +141,13 @@ function releaseNoteContext(
140
141
  }
141
142
  const cwd = path.resolve(options.cwd || process.cwd());
142
143
  const platformType = options.platformType ?? configuredPlatform(cwd);
143
- if (platformType !== 'github') return baseResult('no-op');
144
+ const loaded = await resolvePlatformProviderContext({ cwd, platformType: platformType || undefined, client: options.client });
145
+ if (!loaded.ok) {
146
+ if (loaded.context.status === 'no-op') return baseResult('no-op');
147
+ const status = loaded.context.status === 'blocked' ? 'blocked' : 'failed';
148
+ return { ...baseResult(status), error: loaded.context.error };
149
+ }
150
+ if (!loaded.value.context.platform.repository) return baseResult('no-op');
144
151
  let fromOid: string;
145
152
  let toOid: string;
146
153
  let fromTime: string;
@@ -160,75 +167,61 @@ function releaseNoteContext(
160
167
  } catch {
161
168
  return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_RANGE_INVALID', message: 'The release tag range is missing or is not ancestral', retryable: false } };
162
169
  }
163
- const context = resolvePlatformContext({ cwd, platformType, client: options.client });
164
- if (!context.platform.repository) {
165
- return {
166
- ...baseResult(context.status === 'blocked' ? 'blocked' : 'failed'),
167
- error: context.error || { code: 'PLATFORM_CONTEXT_UNAVAILABLE', message: 'Platform repository is unavailable', retryable: context.status === 'blocked' }
168
- };
169
- }
170
- const platform = fetchGitHubReleaseNoteData(
171
- {
172
- repository: context.platform.repository,
170
+ const platform = loaded.value.provider.releases?.collectNotes
171
+ ? await loaded.value.provider.releases.collectNotes({
172
+ context: providerOperationContext(loaded.value),
173
173
  commitOids: commitFacts.map((item) => item.oid),
174
174
  branch: input.branch,
175
175
  historyLimit,
176
176
  fromTime,
177
177
  toTime
178
- },
179
- { cwd, client: options.client }
180
- );
181
- if (platform.status === 'blocked' || platform.status === 'failed' || !('authors' in platform)) {
182
- return { ...baseResult(platform.status), error: platform.error };
183
- }
178
+ })
179
+ : unsupportedProviderOperation(loaded.value.provider, 'releases.collectNotes');
180
+ if (!platform.ok) return { ...baseResult(platform.error.retryable ? 'blocked' : 'failed'), error: providerError(platform.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
184
181
  const commits = commitFacts.map((commit) => {
185
182
  const seen = new Set<string>();
186
- const authors = (platform.authors.get(commit.oid) || []).filter((actor) => {
187
- const key = actor.login || actor.email?.toLowerCase() || actor.name.toLowerCase();
183
+ const authors = platform.value.history.filter((item) => item.sha === commit.oid).flatMap((item) => item.author ? [item.author] : [])
184
+ .concat(platform.value.actors)
185
+ .filter((actor) => {
186
+ const key = actor.id || actor.name || '';
188
187
  if (seen.has(key)) return false;
189
188
  seen.add(key);
190
189
  return true;
191
- });
190
+ });
192
191
  return { ...commit, authors };
193
192
  });
194
- const pullRequests = platform.pullRequests
193
+ const pullRequests: ReleaseNotePullRequest[] = platform.value.mergedPullRequests
195
194
  .map((item) => ({
196
- number: Number(item.number),
197
- title: String(item.title || ''),
198
- body: String(item.body || ''),
199
- url: String(item.url || ''),
200
- mergedAt: String(item.mergedAt || ''),
201
- labels: Array.isArray(item.labels) ? item.labels.map((label) => String((label as { name?: string }).name || label)) : [],
202
- author: item.author && typeof item.author === 'object'
203
- ? { login: String((item.author as { login?: string }).login || '').toLowerCase(), bot: String((item.author as { login?: string }).login || '').endsWith('[bot]') }
204
- : null,
205
- closingIssues: Array.isArray(item.closingIssuesReferences)
206
- ? item.closingIssuesReferences.map((issue) => {
207
- const value = issue as Record<string, unknown>;
208
- return {
209
- number: Number(value.number),
210
- title: String(value.title || ''),
211
- url: String(value.url || ''),
212
- labels: Array.isArray(value.labels) ? value.labels.map((label) => String((label as { name?: string }).name || label)) : [],
213
- author: value.author && typeof value.author === 'object'
214
- ? { login: String((value.author as { login?: string }).login || '').toLowerCase(), bot: String((value.author as { login?: string }).login || '').endsWith('[bot]') }
215
- : null
216
- };
217
- }) : []
195
+ number: item.number || resourceIdentityNumber(item.identity) || 0,
196
+ ...(item.identity ? { identity: item.identity } : {}),
197
+ title: item.title,
198
+ body: item.body,
199
+ url: item.displayUrl || '',
200
+ mergedAt: item.mergedAt || '',
201
+ labels: item.labels || [],
202
+ author: item.author ? { login: item.author.name || item.author.id || '', bot: false } : null,
203
+ closingIssues: platform.value.closingIssues.filter((issue) => issue.id === item.id).map((issue) => ({
204
+ number: issue.number || resourceIdentityNumber(issue.identity) || 0,
205
+ ...(issue.identity ? { identity: issue.identity } : {}),
206
+ title: issue.title,
207
+ url: issue.displayUrl || '',
208
+ labels: issue.labels || [],
209
+ author: issue.author ? { login: issue.author.name || issue.author.id || '', bot: false } : null
210
+ }))
218
211
  }))
219
212
  .sort((left, right) => left.number - right.number);
220
213
  return {
221
214
  status: 'no-op' as const,
222
215
  changed: false,
223
216
  range: { fromTag: input.fromTag, toTag: input.toTag, fromOid, toOid },
224
- history: platform.history,
217
+ history: platform.value.history,
225
218
  pullRequests,
226
219
  commits,
227
220
  error: null
228
221
  };
229
222
  }
230
223
 
231
- function publishReleaseNotes(
224
+ async function publishReleaseNotes(
232
225
  input: { tag: string; title: string; notesFile: string; expectedSha256: string; dryRun?: boolean },
233
226
  options: ReleaseNoteOptions = {}
234
227
  ) {
@@ -246,12 +239,36 @@ function publishReleaseNotes(
246
239
  }
247
240
  const cwd = path.resolve(options.cwd || process.cwd());
248
241
  const platformType = options.platformType ?? configuredPlatform(cwd);
249
- if (platformType !== 'github') return { status: 'no-op' as const, changed: false, operation: null, url: null, error: unsupportedError };
250
- const context = resolvePlatformContext({ cwd, platformType, client: options.client });
251
- if (!context.platform.repository) {
252
- return { status: context.status === 'blocked' ? 'blocked' as const : 'failed' as const, changed: false, operation: null, url: null, error: context.error };
242
+ const loaded = await resolvePlatformProviderContext({ cwd, platformType: platformType || undefined, client: options.client });
243
+ if (!loaded.ok) {
244
+ const status = loaded.context.status === 'blocked'
245
+ ? 'blocked'
246
+ : loaded.context.status === 'no-op' ? 'no-op' : 'failed';
247
+ return {
248
+ ...baseResult(status),
249
+ error: loaded.context.error || { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
250
+ };
251
+ }
252
+ if (loaded.ok) {
253
+ const context = loaded.value.context;
254
+ if (!context.platform.repository) return { status: context.status === 'blocked' ? 'blocked' as const : 'failed' as const, changed: false, operation: null, url: null, error: context.error };
255
+ const published = loaded.value.provider.releases?.publishNotes
256
+ ? await loaded.value.provider.releases.publishNotes({
257
+ context: providerOperationContext(loaded.value),
258
+ release: { kind: 'key', value: input.tag },
259
+ title: input.title,
260
+ notes: {
261
+ text: fs.readFileSync(input.notesFile, 'utf8'),
262
+ sha256: actualSha256,
263
+ byteLength: fs.statSync(input.notesFile).size
264
+ },
265
+ mutation: { idempotencyKey: `release-notes:publish:${input.tag}` }
266
+ })
267
+ : unsupportedProviderOperation(loaded.value.provider, 'releases.publishNotes');
268
+ if (!published.ok) return { status: providerStatus(published.error), changed: false, operation: null, url: null, error: providerError(published.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
269
+ return { status: 'applied' as const, changed: published.value.changed, operation: 'publish-notes', url: null, error: null };
253
270
  }
254
- return publishGitHubReleaseNotes({ ...input, repository: context.platform.repository }, { cwd, client: options.client });
271
+ return { status: 'failed' as const, changed: false, operation: null, url: null, error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false } };
255
272
  }
256
273
 
257
274
  export {
@@ -1,52 +1,78 @@
1
- import { createGitHubClient } from './github-client.ts';
2
- import type { GitHubClient } from './github-client.ts';
3
- import { resolvePlatformContext } from './context.ts';
1
+ import fs from 'node:fs';
2
+ import crypto from 'node:crypto';
3
+ import { resolvePlatformProviderContext } from './context.ts';
4
+ import type { PlatformClient } from './context.ts';
4
5
  import { platformResult } from './types.ts';
6
+ import { providerError, providerOperationContext, providerStatus, unsupportedProviderOperation } from './provider-bridge.ts';
5
7
 
6
8
  type ReleaseSnapshot = { tag: string; published: boolean; draft: boolean; url: string | null };
7
- type ReleaseOptions = { cwd?: string; client?: GitHubClient };
9
+ type ReleaseOptions = { cwd?: string; client?: PlatformClient };
8
10
 
9
11
  function flattenPages(value: unknown): Array<Record<string, unknown>> {
10
12
  if (!Array.isArray(value)) return [];
11
13
  return value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]).filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === 'object');
12
14
  }
13
15
 
14
- function inspectPlatformRelease(tag: string, options: ReleaseOptions = {}) {
15
- const client = options.client ?? createGitHubClient();
16
- const context = resolvePlatformContext({ cwd: options.cwd, client });
16
+ async function inspectPlatformRelease(tag: string, options: ReleaseOptions = {}) {
17
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
18
+ const context = loaded.ok ? loaded.value.context : loaded.context;
17
19
  if (!context.platform.repository) return { ...platformResult(context.status), platform: context.platform, release: null, workflows: [], error: context.error };
18
- const result = client.json<Record<string, unknown>>(['release', 'view', tag, '--repo', context.platform.repository, '--json', 'tagName,isDraft,url'], { cwd: options.cwd });
19
- if (!result.ok) {
20
- if (result.error.code === 'RESOURCE_NOT_FOUND') return { ...platformResult('no-op'), platform: context.platform, release: null, workflows: [], error: null };
21
- return { ...platformResult(result.error.retryable ? 'blocked' : 'failed'), platform: context.platform, release: null, workflows: [], error: result.error };
20
+ if (loaded.ok) {
21
+ const inspected = loaded.value.provider.releases?.inspect
22
+ ? await loaded.value.provider.releases.inspect({ context: providerOperationContext(loaded.value), tag })
23
+ : unsupportedProviderOperation(loaded.value.provider, 'releases.inspect');
24
+ if (!inspected.ok) {
25
+ if (inspected.error.code === 'RESOURCE_NOT_FOUND') return { ...platformResult('no-op'), platform: context.platform, release: null, workflows: [], error: null };
26
+ return { ...platformResult(providerStatus(inspected.error)), platform: context.platform, release: null, workflows: [], error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
27
+ }
28
+ return {
29
+ ...platformResult('no-op'),
30
+ platform: context.platform,
31
+ release: { tag: inspected.value.tag, published: !inspected.value.draft, draft: inspected.value.draft, url: inspected.value.displayUrl || null },
32
+ workflows: inspected.value.workflows || [],
33
+ error: null
34
+ };
22
35
  }
23
- const release: ReleaseSnapshot = { tag: String(result.value.tagName || tag), published: !Boolean(result.value.isDraft), draft: Boolean(result.value.isDraft), url: result.value.url ? String(result.value.url) : null };
24
- const runs = client.json<unknown[]>(['run', 'list', '--repo', context.platform.repository, '--limit', '100', '--json', 'name,workflowName,displayTitle,event,headBranch,headSha,status,conclusion,createdAt,databaseId,attempt,url'], { cwd: options.cwd });
25
- return { ...platformResult(runs.ok ? 'no-op' : runs.error.retryable ? 'blocked' : 'degraded'), platform: context.platform, release, workflows: runs.ok ? runs.value : [], error: runs.ok ? null : runs.error };
36
+ return { ...platformResult('failed'), platform: context.platform, release: null, workflows: [], error: context.error };
26
37
  }
27
38
 
28
- function upsertPlatformRelease(input: { tag: string; title?: string; notesFile?: string }, options: ReleaseOptions = {}) {
29
- const inspected = inspectPlatformRelease(input.tag, options);
39
+ async function upsertPlatformRelease(input: { tag: string; title?: string; notesFile?: string }, options: ReleaseOptions = {}) {
40
+ const inspected = await inspectPlatformRelease(input.tag, options);
30
41
  if (inspected.release?.published) return { ...inspected, status: 'no-op' as const, changed: false, operations: [] };
31
42
  if (inspected.status === 'blocked' || inspected.status === 'failed') return { ...inspected, changed: false, operations: [] };
32
43
  if (!inspected.platform.repository) return { ...inspected, status: 'no-op' as const, changed: false, operations: [] };
33
- const client = options.client ?? createGitHubClient();
34
- const args = ['release', 'create', input.tag, '--repo', inspected.platform.repository!, '--title', input.title ?? input.tag];
35
- if (input.notesFile) args.push('--notes-file', input.notesFile); else args.push('--generate-notes');
36
- const created = client.text(args, { cwd: options.cwd, method: 'POST' });
37
- if (!created.ok) return { ...inspected, status: created.error.retryable ? 'blocked' as const : 'failed' as const, changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: created.error };
38
- return { ...inspectPlatformRelease(input.tag, options), status: 'applied' as const, changed: true, operations: [{ name: 'create-release', status: 'applied' }] };
44
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
45
+ if (loaded.ok) {
46
+ let notes: { text: string; sha256: string; byteLength: number } | undefined;
47
+ if (input.notesFile) {
48
+ try {
49
+ const bytes = fs.readFileSync(input.notesFile);
50
+ notes = { text: bytes.toString('utf8'), sha256: `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`, byteLength: bytes.byteLength };
51
+ } catch {
52
+ return { ...inspected, status: 'failed' as const, changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: { code: 'RELEASE_NOTES_FILE_UNREADABLE', message: 'notes-file could not be read', retryable: false } };
53
+ }
54
+ }
55
+ const created = loaded.value.provider.releases?.create
56
+ ? await loaded.value.provider.releases.create({
57
+ context: providerOperationContext(loaded.value),
58
+ tag: input.tag,
59
+ title: input.title ?? input.tag,
60
+ ...(notes ? { notes } : {}),
61
+ mutation: { idempotencyKey: `release:create:${input.tag}` }
62
+ })
63
+ : unsupportedProviderOperation(loaded.value.provider, 'releases.create');
64
+ if (!created.ok) return { ...inspected, status: providerStatus(created.error), changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: providerError(created.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
65
+ return { ...inspected, status: 'applied' as const, changed: created.value.changed, operations: [{ name: 'create-release', status: 'applied' }], error: null };
66
+ }
67
+ return { ...inspected, status: 'failed' as const, changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false } };
39
68
  }
40
69
 
41
- function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {}) {
70
+ async function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {}) {
42
71
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
43
72
  if (!match) return { status: 'failed' as const, changed: false, operations: [], error: { code: 'RELEASE_VERSION_INVALID', message: `Invalid version '${version}'` } };
44
- const client = options.client ?? createGitHubClient();
45
- const context = resolvePlatformContext({ cwd: options.cwd, client });
73
+ const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
74
+ const context = loaded.ok ? loaded.value.context : loaded.context;
46
75
  if (!context.platform.repository) return { ...platformResult(context.status), changed: false, operations: [], error: context.error };
47
- const listed = client.json<unknown>(['api', '--paginate', '--slurp', `repos/${context.platform.repository}/milestones?state=all&per_page=100`], { cwd: options.cwd });
48
- if (!listed.ok) return { ...platformResult(listed.error.retryable ? 'blocked' : 'failed'), changed: false, operations: [], error: listed.error };
49
- const milestones = flattenPages(listed.value).map((item) => ({ title: String(item.title || ''), number: Number(item.number), state: String(item.state || '') }));
50
76
  const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]);
51
77
  const nextPatch = `${major}.${minor}.${patch + 1}`;
52
78
  const currentLine = `${major}.${minor}.x`;
@@ -62,22 +88,30 @@ function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {
62
88
  { title: nextLine, description: `Issues that we want to resolve in ${major}.${minor + 1} line.` }
63
89
  );
64
90
  }
65
- const operations: Array<{ name: string; title: string; status: 'applied' | 'no-op' | 'failed' }> = [];
66
- const current = milestones.find((item) => item.title === version);
67
- if (current && current.state !== 'closed') {
68
- const closed = client.json(['api', '--method', 'PATCH', `repos/${context.platform.repository}/milestones/${current.number}`, '-f', 'state=closed'], { cwd: options.cwd });
69
- operations.push({ name: 'close-milestone', title: version, status: closed.ok ? 'applied' : 'failed' });
70
- } else operations.push({ name: 'close-milestone', title: version, status: 'no-op' });
71
- for (const { title, description } of ensure) {
72
- if (milestones.some((item) => item.title === title)) operations.push({ name: 'ensure-milestone', title, status: 'no-op' });
73
- else {
74
- const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`, '-f', `description=${description}`], { cwd: options.cwd });
75
- operations.push({ name: 'ensure-milestone', title, status: created.ok ? 'applied' : 'failed' });
76
- }
91
+ if (loaded.ok) {
92
+ const reconciled = loaded.value.provider.releases?.reconcileMilestones
93
+ ? await loaded.value.provider.releases.reconcileMilestones({
94
+ context: providerOperationContext(loaded.value),
95
+ version,
96
+ desired: [
97
+ { key: version, title: version, description: `Issues that we want to resolve in v${version}.`, state: 'closed' },
98
+ ...ensure.map((item) => ({ key: item.title, title: item.title, description: item.description, state: 'open' as const }))
99
+ ],
100
+ mutation: { idempotencyKey: `milestones:reconcile:${version}` }
101
+ })
102
+ : unsupportedProviderOperation(loaded.value.provider, 'releases.reconcileMilestones');
103
+ if (!reconciled.ok) return { status: providerStatus(reconciled.error), changed: false, operations: [], error: providerError(reconciled.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
104
+ return {
105
+ status: reconciled.value.changed ? 'applied' as const : 'no-op' as const,
106
+ changed: reconciled.value.changed,
107
+ operations: [
108
+ ...reconciled.value.closed.map((title) => ({ name: 'close-milestone', title, status: 'applied' as const })),
109
+ ...reconciled.value.created.map((title) => ({ name: 'ensure-milestone', title, status: 'applied' as const }))
110
+ ],
111
+ error: null
112
+ };
77
113
  }
78
- const failed = operations.some((operation) => operation.status === 'failed');
79
- const changed = operations.some((operation) => operation.status === 'applied');
80
- return { status: failed ? 'failed' as const : changed ? 'applied' as const : 'no-op' as const, changed, operations, error: failed ? { code: 'MILESTONE_RECONCILE_FAILED', message: 'One or more milestone operations failed' } : null };
114
+ return { status: 'failed' as const, changed: false, operations: [], error: { code: 'PLATFORM_CAPABILITY_UNSUPPORTED', message: 'Release milestone reconciliation is unavailable', retryable: false } };
81
115
  }
82
116
 
83
117
  export { inspectPlatformRelease, reconcileReleaseMilestones, upsertPlatformRelease };
@@ -0,0 +1,121 @@
1
+ type ResourceIdentityKind = 'id' | 'number' | 'key';
2
+ type ResourceIdentity =
3
+ | { kind: 'id'; value: string }
4
+ | { kind: 'number'; value: number }
5
+ | { kind: 'key'; value: string };
6
+ type CanonicalResourceIdentity = ResourceIdentity;
7
+
8
+ type PlatformResourceKind = 'issue' | 'pull-request' | 'comment' | 'release';
9
+ type ProviderIdentityDeclaration = Partial<Record<PlatformResourceKind, ResourceIdentityKind>>;
10
+
11
+ function identityError(message: string): Error & { code: string } {
12
+ return Object.assign(new Error(`PLATFORM_IDENTITY_INVALID: ${message}`), {
13
+ code: 'PLATFORM_IDENTITY_INVALID'
14
+ });
15
+ }
16
+
17
+ function isResourceIdentity(value: unknown): value is CanonicalResourceIdentity {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
19
+ const candidate = value as Record<string, unknown>;
20
+ if (Object.keys(candidate).length !== 2 || typeof candidate.kind !== 'string') return false;
21
+ if (candidate.kind === 'number') {
22
+ return Number.isSafeInteger(candidate.value) && (candidate.value as number) > 0;
23
+ }
24
+ return (candidate.kind === 'id' || candidate.kind === 'key')
25
+ && typeof candidate.value === 'string'
26
+ && candidate.value.length > 0
27
+ && candidate.value.trim() === candidate.value;
28
+ }
29
+
30
+ function parseResourceIdentity(value: unknown, label = 'identity'): CanonicalResourceIdentity {
31
+ if (!isResourceIdentity(value)) throw identityError(`${label} must contain exactly one valid kind/value pair`);
32
+ if (value.kind === 'number') return { kind: 'number', value: value.value };
33
+ if (value.kind === 'id') return { kind: 'id', value: value.value };
34
+ return { kind: 'key', value: value.value };
35
+ }
36
+
37
+ function serializeResourceIdentity(identity: ResourceIdentity): string {
38
+ return JSON.stringify(parseResourceIdentity(identity));
39
+ }
40
+
41
+ function resourceIdentityEquals(left: unknown, right: unknown): boolean {
42
+ return isResourceIdentity(left) && isResourceIdentity(right)
43
+ && left.kind === right.kind && left.value === right.value;
44
+ }
45
+
46
+ function primaryIdentityKind(
47
+ declaration: ProviderIdentityDeclaration | undefined,
48
+ resourceKind: PlatformResourceKind
49
+ ): ResourceIdentityKind {
50
+ const kind = declaration?.[resourceKind];
51
+ if (kind !== 'id' && kind !== 'number' && kind !== 'key') {
52
+ throw identityError(`provider does not declare a primary identity for ${resourceKind}`);
53
+ }
54
+ return kind;
55
+ }
56
+
57
+ function parseResourceToken(
58
+ token: string,
59
+ resourceKind: PlatformResourceKind,
60
+ declaration: ProviderIdentityDeclaration | undefined
61
+ ): ResourceIdentity {
62
+ if (typeof token !== 'string' || token.length === 0) throw identityError('resource token must be a non-empty string');
63
+ const kind = primaryIdentityKind(declaration, resourceKind);
64
+ if (kind === 'number') {
65
+ if (!/^[1-9]\d*$/u.test(token)) throw identityError(`${resourceKind} token must be a positive safe integer`);
66
+ const value = Number(token);
67
+ if (!Number.isSafeInteger(value) || value <= 0) throw identityError(`${resourceKind} token must be a positive safe integer`);
68
+ return { kind, value };
69
+ }
70
+ const value = token.trim();
71
+ if (!value) throw identityError(`${resourceKind} token must not be empty`);
72
+ return { kind, value };
73
+ }
74
+
75
+ function identityFromRemoteValue(
76
+ value: unknown,
77
+ resourceKind: PlatformResourceKind,
78
+ declaration: ProviderIdentityDeclaration | undefined
79
+ ): ResourceIdentity {
80
+ const kind = primaryIdentityKind(declaration, resourceKind);
81
+ if (isResourceIdentity(value)) {
82
+ if (value.kind !== kind) throw identityError(`${resourceKind} response identity kind does not match provider declaration`);
83
+ return value;
84
+ }
85
+ if (kind === 'number') {
86
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return { kind, value };
87
+ if (typeof value === 'string' && /^[1-9]\d*$/u.test(value)) return parseResourceToken(value, resourceKind, declaration);
88
+ throw identityError(`${resourceKind} response does not contain a valid number identity`);
89
+ }
90
+ if (typeof value !== 'string' || !value.trim()) throw identityError(`${resourceKind} response does not contain a valid string identity`);
91
+ return { kind, value: value.trim() };
92
+ }
93
+
94
+ function resourceIdentityNumber(identity: ResourceIdentity | null | undefined): number | null {
95
+ return identity?.kind === 'number' ? identity.value : null;
96
+ }
97
+
98
+ function resourceIdentityString(identity: ResourceIdentity | null | undefined): string | null {
99
+ return identity && (identity.kind === 'id' || identity.kind === 'key') ? identity.value : null;
100
+ }
101
+
102
+ function reviewMarker(identity: ResourceIdentity): string {
103
+ const parsed = parseResourceIdentity(identity);
104
+ if (parsed.kind === 'number') return `pr${parsed.value}`;
105
+ const encoded = Buffer.from(serializeResourceIdentity(parsed), 'utf8').toString('base64url');
106
+ return `pr:${encoded}`;
107
+ }
108
+
109
+ export {
110
+ identityFromRemoteValue,
111
+ isResourceIdentity,
112
+ parseResourceIdentity,
113
+ parseResourceToken,
114
+ primaryIdentityKind,
115
+ resourceIdentityEquals,
116
+ resourceIdentityNumber,
117
+ resourceIdentityString,
118
+ reviewMarker,
119
+ serializeResourceIdentity
120
+ };
121
+ export type { CanonicalResourceIdentity, PlatformResourceKind, ProviderIdentityDeclaration, ResourceIdentity, ResourceIdentityKind };
@@ -0,0 +1,40 @@
1
+ import type { ProviderIdentityDeclaration, ResourceIdentity } from './resource-identity.ts';
2
+ import { parseResourceIdentity } from './resource-identity.ts';
3
+ import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
4
+ import { parse as parseYaml } from 'yaml';
5
+
6
+ function taskIssueIdentity(
7
+ frontmatter: Record<string, string | number | boolean | null>,
8
+ _declaration?: ProviderIdentityDeclaration,
9
+ runtimeVersion = VERSION
10
+ ): ResourceIdentity | null {
11
+ const serialized = frontmatter.platform_issue_identity;
12
+ if (typeof serialized === 'string' && serialized.trim()) {
13
+ try {
14
+ const decoded = parseYaml(serialized);
15
+ return parseResourceIdentity(
16
+ JSON.parse(typeof decoded === 'string' ? decoded : serialized),
17
+ 'platform_issue_identity'
18
+ );
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ const legacy = frontmatter.issue_number;
24
+ const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
25
+ // TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
26
+ if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
27
+ throw legacyCompatibilityError('issue_number', runtimeVersion);
28
+ }
29
+ return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
30
+ }
31
+
32
+ function taskIssueIdentityError(error: unknown): { code: string; message: string } {
33
+ const value = error && typeof error === 'object' ? error as { code?: unknown } : {};
34
+ return {
35
+ code: typeof value.code === 'string' ? value.code : 'PLATFORM_IDENTITY_INVALID',
36
+ message: error instanceof Error ? error.message : String(error)
37
+ };
38
+ }
39
+
40
+ export { taskIssueIdentity, taskIssueIdentityError };
@@ -23,15 +23,17 @@ type PlatformOperation = {
23
23
  type CommentIdentity = {
24
24
  kind: 'task' | 'artifact' | 'summary' | 'cancel';
25
25
  marker: string;
26
- ids: number[];
26
+ ids: Array<number | string>;
27
27
  parts: number;
28
28
  };
29
29
 
30
+ import type { ResourceIdentity } from './resource-identity.ts';
31
+
30
32
  type PlatformResult = {
31
33
  status: PlatformStatus;
32
34
  changed: boolean;
33
35
  platform: { type: string | null; repository: string | null; currentUser: string | null };
34
- resource: { kind: 'repository' | 'issue' | 'pull-request'; number: number | null };
36
+ resource: { kind: 'repository' | 'issue' | 'pull-request'; number: number | null; identity?: ResourceIdentity };
35
37
  capabilities: PlatformCapabilities;
36
38
  operations: PlatformOperation[];
37
39
  comment: CommentIdentity | null;
@@ -2,20 +2,15 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
 
5
- import { hasPlatformCapability } from "./adapters.ts";
6
5
  import { inspectRequiredChecks } from "./pr-checks.ts";
7
- import { resolvePlatformContext } from "./context.ts";
6
+ import { resolvePlatformProviderContext } from "./context.ts";
8
7
  import { resolveReviewedHeadRelation } from "./merged-pr-equivalence.ts";
9
8
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
9
+ import { resourceIdentityNumber } from "./resource-identity.ts";
10
10
 
11
11
  const CHECK_TYPE = "required-checks";
12
12
  const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
13
13
 
14
- function validPrNumber(value: any): any {
15
- const parsed = Number(value);
16
- return Number.isInteger(parsed) && parsed > 0;
17
- }
18
-
19
14
  function readPrFlow(repoRoot: any): any {
20
15
  try {
21
16
  const config = JSON.parse(fs.readFileSync(path.join(repoRoot, ".agents", ".airc.json"), "utf8"));
@@ -40,8 +35,9 @@ export function evaluateRequiredChecks(context: any, shared: any): any {
40
35
  return shared.failResult(CHECK_TYPE, factRead.error.message, "check_failed");
41
36
  }
42
37
  const fact = factRead.status === "valid" ? factRead.fact : null;
43
- const prNumber = fact?.state === "bound" ? fact.identity.number : null;
44
- if (prFlow === "disabled" || fact?.state === "skipped" || !validPrNumber(prNumber)) {
38
+ const prIdentity = fact?.state === "bound" ? fact.identity.resource : null;
39
+ const prNumber = resourceIdentityNumber(prIdentity);
40
+ if (prFlow === "disabled" || fact?.state === "skipped" || !prIdentity) {
45
41
  return shared.passResult(CHECK_TYPE, "Skipped: required checks are not applicable to this task");
46
42
  }
47
43
 
@@ -90,7 +86,7 @@ export function evaluateRequiredChecks(context: any, shared: any): any {
90
86
  return shared.failResult(CHECK_TYPE, inspection.error?.message || `Required checks are ${state || "unavailable"}`, "check_failed");
91
87
  }
92
88
 
93
- export function check({ taskDir }: any, shared: any): any {
89
+ export async function check({ taskDir }: any, shared: any): Promise<any> {
94
90
  const task = shared.loadTask(taskDir);
95
91
  if (!task.ok) return shared.failResult(CHECK_TYPE, task.message);
96
92
  const prFlow = readPrFlow(shared.repoRoot);
@@ -100,8 +96,10 @@ export function check({ taskDir }: any, shared: any): any {
100
96
  if (prFlow === "disabled" || (factRead.status === "valid" && factRead.fact.state === "skipped") || !bound) {
101
97
  return evaluateRequiredChecks({ metadata: task.metadata, localHead: null, inspection: null, prFlow }, shared);
102
98
  }
103
- const platform = resolvePlatformContext({ cwd: shared.repoRoot });
104
- if (!hasPlatformCapability(platform.platform.type, "required-checks")) {
99
+ const loaded = await resolvePlatformProviderContext({ cwd: shared.repoRoot });
100
+ const platform = loaded.ok ? loaded.value.context : loaded.context;
101
+ const supportsChecks = loaded.ok && Boolean(loaded.value.provider.checks?.inspectRequired);
102
+ if (!supportsChecks) {
105
103
  return shared.blockedResult(
106
104
  CHECK_TYPE,
107
105
  `Platform '${platform.platform.type || "none"}' does not provide required-checks inspection`,
@@ -110,7 +108,7 @@ export function check({ taskDir }: any, shared: any): any {
110
108
  }
111
109
  const localHead = readHead(shared.repoRoot);
112
110
  if (!localHead) return evaluateRequiredChecks({ metadata: task.metadata, localHead, inspection: null, prFlow }, shared);
113
- const inspection = inspectRequiredChecks(task.metadata.id, { cwd: shared.repoRoot });
111
+ const inspection = await inspectRequiredChecks(task.metadata.id, { cwd: shared.repoRoot });
114
112
  return evaluateRequiredChecks({
115
113
  metadata: task.metadata,
116
114
  localHead,