@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
@@ -0,0 +1,208 @@
1
+ import { computeInLabels } from './metadata-labels.ts';
2
+ import type { PlatformError } from './types.ts';
3
+
4
+ type InLabelPlan = {
5
+ current: string[];
6
+ target: string[];
7
+ labels: string[];
8
+ changed: boolean;
9
+ error: InLabelValidationError | null;
10
+ };
11
+
12
+ type InLabelResource = {
13
+ kind: 'issue' | 'pull-request';
14
+ number: number;
15
+ labels: string[];
16
+ };
17
+
18
+ type InLabelValidationError = {
19
+ code: string;
20
+ message: string;
21
+ retryable: boolean;
22
+ };
23
+
24
+ type LabelPrefix = string | readonly string[];
25
+
26
+ type LabelDelta = {
27
+ add: string[];
28
+ remove: string[];
29
+ };
30
+
31
+ type LabelDeltaResult = {
32
+ status: 'applied' | 'no-op' | 'failed' | 'blocked';
33
+ changed: boolean;
34
+ error: PlatformError | null;
35
+ };
36
+
37
+ type LabelSyncClient = {
38
+ json<T = unknown>(args: string[], options?: { cwd?: string; method?: string; input?: string }): any;
39
+ };
40
+
41
+ function inLabels(labels: readonly string[]): string[] {
42
+ return [...new Set(labels.filter((label) => label.startsWith('in:')))].sort();
43
+ }
44
+
45
+ function mergeInLabels(labels: readonly string[], target: readonly string[]): string[] {
46
+ return [...new Set([
47
+ ...labels.filter((label) => !label.startsWith('in:')),
48
+ ...target
49
+ ])].sort();
50
+ }
51
+
52
+ function invalidInLabelInput(code: string, message: string): { ok: false; error: InLabelValidationError } {
53
+ return { ok: false, error: { code, message, retryable: false } };
54
+ }
55
+
56
+ function validateInLabelMapping(value: unknown): { ok: true; value: Record<string, string[]> } | { ok: false; error: InLabelValidationError } {
57
+ if (value === undefined) return { ok: true, value: {} };
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
59
+ return invalidInLabelInput('IN_LABEL_SYNC_MAPPING_INVALID', 'labels.in must be an object of non-empty string arrays');
60
+ }
61
+ const normalized: Record<string, string[]> = {};
62
+ for (const [name, rawPrefixes] of Object.entries(value)) {
63
+ if (!name.trim() || !Array.isArray(rawPrefixes) || rawPrefixes.length === 0 || rawPrefixes.some((prefix) => typeof prefix !== 'string' || !prefix.trim())) {
64
+ return invalidInLabelInput('IN_LABEL_SYNC_MAPPING_INVALID', `labels.in entry '${name}' must be a non-empty string array`);
65
+ }
66
+ normalized[name] = rawPrefixes.map((prefix) => prefix.trim());
67
+ }
68
+ return { ok: true, value: normalized };
69
+ }
70
+
71
+ function repositoryLabelPages(value: unknown): unknown[][] | null {
72
+ if (!Array.isArray(value)) return null;
73
+ if (value.length === 0) return [];
74
+ if (value.every((page) => Array.isArray(page))) return value as unknown[][];
75
+ if (value.every((entry) => entry && typeof entry === 'object' && !Array.isArray(entry))) return [value];
76
+ return null;
77
+ }
78
+
79
+ function validateRepositoryLabelPayload(value: unknown): { ok: true; value: string[] } | { ok: false; error: InLabelValidationError } {
80
+ const pages = repositoryLabelPages(value);
81
+ if (!pages) return invalidInLabelInput('IN_LABEL_SYNC_LABELS_INVALID', 'Repository labels response must be an array of pages');
82
+ const names: string[] = [];
83
+ for (const page of pages) {
84
+ for (const entry of page) {
85
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof (entry as { name?: unknown }).name !== 'string' || !(entry as { name: string }).name.trim()) {
86
+ return invalidInLabelInput('IN_LABEL_SYNC_LABELS_INVALID', 'Repository labels response contains an entry without a valid name');
87
+ }
88
+ names.push((entry as { name: string }).name);
89
+ }
90
+ }
91
+ return { ok: true, value: [...new Set(names)].sort() };
92
+ }
93
+
94
+ function validateRepositoryLabelSet(value: unknown): { ok: true; value: Set<string> } | { ok: false; error: InLabelValidationError } {
95
+ if (!(value instanceof Set) || [...value].some((label) => typeof label !== 'string' || !label.trim())) {
96
+ return invalidInLabelInput('IN_LABEL_SYNC_LABELS_INVALID', 'Repository labels must be a set of non-empty names');
97
+ }
98
+ return { ok: true, value: new Set([...value] as string[]) };
99
+ }
100
+
101
+ function planInLabelUpdate(input: {
102
+ changedFiles: readonly string[];
103
+ currentLabels: readonly string[];
104
+ mapping: Record<string, unknown>;
105
+ repositoryLabels: Set<string>;
106
+ }): InLabelPlan {
107
+ const current = inLabels(input.currentLabels);
108
+ const mapping = validateInLabelMapping(input.mapping);
109
+ const repositoryLabels = validateRepositoryLabelSet(input.repositoryLabels);
110
+ if (!mapping.ok) return { current, target: [...current], labels: [...input.currentLabels].sort(), changed: false, error: mapping.error };
111
+ if (!repositoryLabels.ok) return { current, target: [...current], labels: [...input.currentLabels].sort(), changed: false, error: repositoryLabels.error };
112
+ const target = computeInLabels([...input.changedFiles], mapping.value, repositoryLabels.value);
113
+ const labels = mergeInLabels(input.currentLabels, target);
114
+ return { current, target, labels, changed: labels.join('\0') !== [...input.currentLabels].sort().join('\0'), error: null };
115
+ }
116
+
117
+ function startsWithPrefix(label: string, prefix: LabelPrefix): boolean {
118
+ return (Array.isArray(prefix) ? prefix : [prefix]).some((value) => label.startsWith(value));
119
+ }
120
+
121
+ function labelDelta(current: readonly string[], target: readonly string[], prefix: LabelPrefix = 'in:'): LabelDelta {
122
+ const currentLabels = [...new Set(current)].sort();
123
+ const targetLabels = [...new Set(target.filter((label) => startsWithPrefix(label, prefix)))].sort();
124
+ return {
125
+ add: targetLabels.filter((label) => !currentLabels.includes(label)),
126
+ remove: currentLabels.filter((label) => startsWithPrefix(label, prefix) && !targetLabels.includes(label))
127
+ };
128
+ }
129
+
130
+ function syncLabelDelta(
131
+ client: LabelSyncClient,
132
+ repository: string,
133
+ number: number,
134
+ cwd: string,
135
+ current: readonly string[],
136
+ target: readonly string[],
137
+ prefix: LabelPrefix = 'in:'
138
+ ): LabelDeltaResult {
139
+ const delta = labelDelta(current, target, prefix);
140
+ let changed = false;
141
+ const failure = (error: PlatformError): LabelDeltaResult => {
142
+ if (changed || error.retryable) {
143
+ return {
144
+ status: 'blocked',
145
+ changed,
146
+ error: { ...error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${error.message}` }
147
+ };
148
+ }
149
+ return { status: 'failed', changed: false, error };
150
+ };
151
+ for (const label of delta.remove) {
152
+ const removed = client.json<unknown>([
153
+ 'api', `repos/${repository}/issues/${number}/labels/${encodeURIComponent(label)}`, '-X', 'DELETE'
154
+ ], { cwd, method: 'DELETE' });
155
+ if (!removed.ok) return failure(removed.error);
156
+ changed = true;
157
+ }
158
+ for (const label of delta.add) {
159
+ const added = client.json<unknown>([
160
+ 'api', `repos/${repository}/issues/${number}/labels`, '-X', 'POST', '--input', '-'
161
+ ], { cwd, method: 'POST', input: JSON.stringify({ labels: [label] }) });
162
+ if (!added.ok) return failure(added.error);
163
+ changed = true;
164
+ }
165
+ return { status: changed ? 'applied' : 'no-op', changed, error: null };
166
+ }
167
+
168
+ function flattenPages(value: unknown): unknown[] {
169
+ if (!Array.isArray(value)) return [];
170
+ return value.flatMap((page) => Array.isArray(page) ? page : [page]);
171
+ }
172
+
173
+ function extractRepositoryLabelNames(value: unknown): string[] {
174
+ return flattenPages(value).flatMap((entry) => {
175
+ if (typeof entry === 'string') return [entry];
176
+ if (entry && typeof entry === 'object' && typeof (entry as { name?: unknown }).name === 'string') {
177
+ return [(entry as { name: string }).name];
178
+ }
179
+ return [];
180
+ }).filter(Boolean).sort();
181
+ }
182
+
183
+ function extractPullRequestFileNames(value: unknown): string[] | null {
184
+ const entries = flattenPages(value);
185
+ if (!Array.isArray(value) || entries.some((entry) => !entry || typeof entry !== 'object')) return null;
186
+ const names = entries.map((entry) => (entry as { filename?: unknown }).filename);
187
+ if (names.some((name) => typeof name !== 'string' || !name.trim())) return null;
188
+ return [...new Set(names as string[])].sort();
189
+ }
190
+
191
+ function resourceSnapshot(kind: InLabelResource['kind'], number: number, labels: readonly string[]): InLabelResource {
192
+ return { kind, number, labels: [...labels].sort() };
193
+ }
194
+
195
+ export {
196
+ extractPullRequestFileNames,
197
+ extractRepositoryLabelNames,
198
+ flattenPages,
199
+ inLabels,
200
+ labelDelta,
201
+ mergeInLabels,
202
+ planInLabelUpdate,
203
+ resourceSnapshot,
204
+ syncLabelDelta,
205
+ validateInLabelMapping,
206
+ validateRepositoryLabelPayload
207
+ };
208
+ export type { InLabelPlan, InLabelResource, InLabelValidationError, LabelDelta, LabelDeltaResult, LabelPrefix };
@@ -3,13 +3,21 @@ import path from 'node:path';
3
3
 
4
4
  import { parseTaskFrontmatter } from '../task/frontmatter.ts';
5
5
  import { resolveTaskRef } from '../task/resolve-ref.ts';
6
- import { createGitHubClient } from './github-client.ts';
7
- import type { GitHubClient } from './github-client.ts';
8
- import { resolvePlatformContext } from './context.ts';
6
+ import { resolvePlatformProviderContext } from './context.ts';
7
+ import type { PlatformClient } from './context.ts';
9
8
  import { platformResult } from './types.ts';
10
9
  import type { PlatformOperation, PlatformResult } from './types.ts';
11
-
12
- type RemoteComment = { id: number; body: string; user?: { login?: string } };
10
+ import {
11
+ providerError,
12
+ providerOperationContext,
13
+ providerStatus,
14
+ providerResourceToken,
15
+ unsupportedProviderOperation
16
+ } from './provider-bridge.ts';
17
+ import { resourceIdentityNumber } from './resource-identity.ts';
18
+ import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
19
+
20
+ type RemoteComment = { id: number | string; body: string; user?: { login?: string } };
13
21
  type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
14
22
  type CommentKind = 'task' | 'artifact' | 'summary' | 'cancel';
15
23
  type SyncOptions = {
@@ -19,9 +27,14 @@ type SyncOptions = {
19
27
  body?: string;
20
28
  cwd?: string;
21
29
  backfill?: boolean;
22
- client?: GitHubClient;
30
+ client?: PlatformClient;
31
+ runtimeVersion?: string;
23
32
  };
24
33
 
34
+ function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
35
+ return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
36
+ }
37
+
25
38
  const MARKERS = {
26
39
  task: (taskId: string) => `<!-- sync-issue:${taskId}:task -->`,
27
40
  artifact: (taskId: string, stem: string) => `<!-- sync-issue:${taskId}:${stem} -->`,
@@ -181,7 +194,7 @@ function flattenComments(value: unknown): RemoteComment[] {
181
194
  if (!Array.isArray(value)) return [];
182
195
  const flattened = value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
183
196
  return flattened.filter((entry): entry is RemoteComment =>
184
- Boolean(entry && typeof entry === 'object' && typeof entry.id === 'number' && typeof entry.body === 'string')
197
+ Boolean(entry && typeof entry === 'object' && (typeof entry.id === 'number' || typeof entry.id === 'string') && typeof entry.body === 'string')
185
198
  );
186
199
  }
187
200
 
@@ -191,7 +204,7 @@ function issueNumberFromTask(content: string): number | null {
191
204
  return Number.isInteger(number) && number > 0 ? number : null;
192
205
  }
193
206
 
194
- function listRemoteComments(client: GitHubClient, repo: string, issue: number, cwd: string) {
207
+ function listRemoteComments(client: PlatformClient, repo: string, issue: number, cwd: string) {
195
208
  const result = client.json<unknown>([
196
209
  'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
197
210
  ], { cwd });
@@ -291,7 +304,7 @@ function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): {
291
304
  }
292
305
 
293
306
  function writeComment(
294
- client: GitHubClient,
307
+ client: PlatformClient,
295
308
  repo: string,
296
309
  issue: number,
297
310
  cwd: string,
@@ -305,7 +318,7 @@ function writeComment(
305
318
  );
306
319
  }
307
320
 
308
- function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformResult {
321
+ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
309
322
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
310
323
  if (!resolved.ok) {
311
324
  return platformResult('failed', {
@@ -313,16 +326,30 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
313
326
  });
314
327
  }
315
328
  const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
316
- const issue = issueNumberFromTask(taskContent);
317
- if (!issue) {
329
+ let issueIdentityFromTask: ReturnType<typeof taskIssueIdentity>;
330
+ try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent), undefined, options.runtimeVersion); }
331
+ catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
332
+ if (!issueIdentityFromTask) {
318
333
  return platformResult('no-op', {
319
334
  error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
320
335
  });
321
336
  }
322
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client: options.client });
323
- if (!hasResolvedPlatformContext(context)) return context;
324
- const client = options.client || createGitHubClient();
325
- const listed = listRemoteComments(client, context.platform.repository!, issue, resolved.repoRoot);
337
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
338
+ const context = loaded.ok ? loaded.value.context : loaded.context;
339
+ if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
340
+ const issue = resourceIdentityNumber(issueIdentityFromTask);
341
+ const listed = loaded.value.provider.comments?.list
342
+ ? await loaded.value.provider.comments.list({
343
+ context: providerOperationContext(loaded.value),
344
+ parent: issueIdentityFromTask
345
+ }).then((response) => response.ok
346
+ ? { ok: true as const, value: response.value.map((comment) => ({
347
+ id: providerCommentId(comment.id, loaded.value.provider),
348
+ body: comment.body,
349
+ user: comment.author?.name ? { login: comment.author.name } : undefined
350
+ })) }
351
+ : response)
352
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
326
353
  if (!listed.ok) {
327
354
  return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
328
355
  ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
@@ -370,9 +397,8 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
370
397
  });
371
398
  }
372
399
 
373
- const repo = context.platform.repository!;
374
400
  const operations: PlatformOperation[] = [];
375
- const ids: number[] = [];
401
+ const ids: Array<number | string> = [];
376
402
  for (const item of desired) {
377
403
  const current = existing.find((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
378
404
  if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
@@ -380,16 +406,18 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
380
406
  operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
381
407
  continue;
382
408
  }
383
- const written = writeComment(client, repo, issue, resolved.repoRoot, item.body, current?.id);
409
+ const written = loaded.value.provider.comments?.write
410
+ ? await loaded.value.provider.comments.write({
411
+ context: providerOperationContext(loaded.value),
412
+ parent: issueIdentityFromTask,
413
+ body: item.body,
414
+ ...(current ? { existingComment: { kind: 'id' as const, value: String(current.id) } } : {}),
415
+ mutation: { idempotencyKey: `comment:${resolved.taskId}:${item.marker}` }
416
+ })
417
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
384
418
  if (!written.ok) {
385
419
  if (!current && written.error.retryable) {
386
- const reconciled = listRemoteComments(client, repo, issue, resolved.repoRoot);
387
- const found = reconciled.ok ? findMarkerComments(reconciled.value, item.marker) : [];
388
- if (found.length === 1) {
389
- ids.push(found[0]!.id);
390
- operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: 'CREATE_RECONCILED' });
391
- continue;
392
- }
420
+ // A provider owns reconciliation because only it knows how to address the resource.
393
421
  }
394
422
  return platformResult(written.error.retryable ? 'blocked' : 'failed', {
395
423
  ...contextFields(context),
@@ -398,16 +426,21 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
398
426
  error: written.error
399
427
  });
400
428
  }
401
- ids.push(current?.id || written.value.id || 0);
429
+ ids.push(providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
402
430
  operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
403
431
  }
404
432
 
405
433
  const desiredMarkers = new Set(desired.map((item) => item.marker));
406
434
  const stale = existing.filter((comment) => !desiredMarkers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]!));
407
435
  for (const comment of stale) {
408
- const deleted = client.text(['api', `repos/${repo}/issues/comments/${comment.id}`, '-X', 'DELETE'], {
409
- cwd: resolved.repoRoot, method: 'DELETE'
410
- });
436
+ const deleted = loaded.value.provider.comments?.delete
437
+ ? await loaded.value.provider.comments.delete({
438
+ context: providerOperationContext(loaded.value),
439
+ parent: issueIdentityFromTask,
440
+ comment: { kind: 'id', value: String(comment.id) },
441
+ mutation: { idempotencyKey: `comment-delete:${resolved.taskId}:${comment.id}` }
442
+ })
443
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
411
444
  if (!deleted.ok) {
412
445
  return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
413
446
  ...contextFields(context),
@@ -429,28 +462,45 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
429
462
  });
430
463
  }
431
464
 
432
- function listPlatformComments(issue: number, cwd = process.cwd(), client: GitHubClient = createGitHubClient()): PlatformResult & { comments?: RemoteComment[] } {
433
- const context = resolvePlatformContext({ cwd, client });
434
- if (!hasResolvedPlatformContext(context)) return context;
435
- const result = listRemoteComments(client, context.platform.repository!, issue, cwd);
436
- if (!result.ok) {
437
- return platformResult(result.error.retryable ? 'blocked' : 'failed', {
438
- ...contextFields(context), resource: { kind: 'issue', number: issue }, error: result.error
465
+ async function listPlatformComments(issue: string | number, cwd = process.cwd(), client?: PlatformClient): Promise<PlatformResult & { comments?: RemoteComment[] }> {
466
+ const loaded = await resolvePlatformProviderContext({ cwd, client });
467
+ const context = loaded.ok ? loaded.value.context : loaded.context;
468
+ if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
469
+ let identity;
470
+ try { identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'issue', String(issue)) : null; }
471
+ catch (error) {
472
+ return platformResult('failed', { ...contextFields(context), error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
473
+ }
474
+ if (!loaded.ok || !identity) return context;
475
+ const listed = loaded.value.provider.comments?.list
476
+ ? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: identity }).then((response) => response.ok
477
+ ? { ok: true as const, value: response.value.map((comment) => ({ id: comment.id, body: comment.body, user: comment.author?.name ? { login: comment.author.name } : undefined })) }
478
+ : response)
479
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
480
+ if (!listed.ok) {
481
+ return platformResult(providerStatus(listed.error), {
482
+ ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(identity), identity }, error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
439
483
  });
440
484
  }
441
- return { ...platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: issue }, error: null }), comments: result.value };
485
+ return { ...platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(identity), identity }, error: null }), comments: listed.value };
442
486
  }
443
487
 
444
- function checkPlatformCommentOwner(taskRef: string, options: { cwd?: string; client?: GitHubClient } = {}): PlatformResult {
488
+ async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: string; client?: PlatformClient; runtimeVersion?: string } = {}): Promise<PlatformResult> {
445
489
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
446
490
  if (!resolved.ok) return platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } });
447
491
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
448
- const issue = issueNumberFromTask(content);
449
- if (!issue) return platformResult('no-op', { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false } });
450
- const client = options.client || createGitHubClient();
451
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client });
452
- if (!hasResolvedPlatformContext(context)) return context;
453
- const listed = listRemoteComments(client, context.platform.repository!, issue, resolved.repoRoot);
492
+ let issueIdentity: ReturnType<typeof taskIssueIdentity>;
493
+ try { issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content), undefined, options.runtimeVersion); }
494
+ catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
495
+ if (!issueIdentity) return platformResult('no-op', { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
496
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
497
+ const context = loaded.ok ? loaded.value.context : loaded.context;
498
+ if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
499
+ const listed = loaded.value.provider.comments?.list
500
+ ? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: issueIdentity }).then((response) => response.ok
501
+ ? { ok: true as const, value: response.value.map((comment) => ({ id: comment.id, body: comment.body, user: comment.author?.name ? { login: comment.author.name } : undefined })) }
502
+ : response)
503
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
454
504
  if (!listed.ok) return platformResult(listed.error.retryable ? 'blocked' : 'failed', { ...contextFields(context), error: listed.error });
455
505
  const matches = findMarkerComments(listed.value, MARKERS.task(resolved.taskId));
456
506
  if (matches.length > 1) return platformResult('failed', { ...contextFields(context), error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple task comments use the registered marker', retryable: false } });
@@ -458,11 +508,11 @@ function checkPlatformCommentOwner(taskRef: string, options: { cwd?: string; cli
458
508
  if (owner && owner !== context.platform.currentUser && !context.capabilities.triage) {
459
509
  return platformResult('blocked', {
460
510
  ...contextFields(context),
461
- resource: { kind: 'issue', number: issue },
511
+ resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity },
462
512
  error: { code: 'COMMENT_OWNER_CONFLICT', message: `Task comment is owned by '${owner}'`, retryable: false }
463
513
  });
464
514
  }
465
- return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: issue }, error: null });
515
+ return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
466
516
  }
467
517
 
468
518
  export {