@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
@@ -7,10 +7,13 @@ import { requirementFieldLabels, renderTemplateBody } from '../task/issue-form.j
7
7
  import { resolveTaskRef } from '../task/resolve-ref.js';
8
8
  import { extractSection, findSectionHeading } from '../task/sections.js';
9
9
  import { writeTask } from '../task/write.js';
10
- import { resolvePlatformContext } from './context.js';
11
- import { createGitHubClient } from './github-client.js';
12
- import { DEFAULT_REQUIREMENT_SECTION_ANCHORS, chooseMilestone, computeInLabels, desiredIssueType, normalizeOption, planIssueMetadata } from './issue-metadata.js';
10
+ import { resolvePlatformProviderContext } from './context.js';
11
+ import { DEFAULT_REQUIREMENT_SECTION_ANCHORS, chooseMilestone, desiredIssueType, normalizeOption, planIssueMetadata } from './issue-metadata.js';
13
12
  import { platformResult } from './types.js';
13
+ import { labelDelta, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
14
+ import { providerError, providerOperationContext, providerStatus, providerResourceIdentity, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
15
+ import { resourceIdentityEquals, resourceIdentityNumber, resourceIdentityString, serializeResourceIdentity } from './resource-identity.js';
16
+ import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
14
17
  const ISSUE_TYPES_QUERY = `query($owner:String!){organization(login:$owner){issueTypes(first:20){nodes{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}}}}}`;
15
18
  const ISSUE_FIELDS_QUERY = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){id issueType{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{id name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{id name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{id name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{id name}}}}}}}}`;
16
19
  function result(status, taskId, issueNumber, overrides = {}) {
@@ -21,7 +24,7 @@ function result(status, taskId, issueNumber, overrides = {}) {
21
24
  ...overrides
22
25
  };
23
26
  }
24
- function resolvedContext(taskRef, options) {
27
+ async function resolvedContext(taskRef, options) {
25
28
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
26
29
  if (!resolved.ok)
27
30
  return { ok: false, output: result('failed', resolved.taskId, null, {
@@ -29,33 +32,126 @@ function resolvedContext(taskRef, options) {
29
32
  }) };
30
33
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
31
34
  const frontmatter = parseTaskFrontmatter(content);
32
- const rawIssue = Number(frontmatter.issue_number);
33
- const issueNumber = Number.isInteger(rawIssue) && rawIssue > 0 ? rawIssue : null;
34
- const client = options.client || createGitHubClient();
35
- const context = resolvePlatformContext({ cwd: resolved.repoRoot, client });
35
+ let issueIdentity;
36
+ try {
37
+ issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
38
+ }
39
+ catch (error) {
40
+ return { ok: false, output: result('failed', resolved.taskId, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
41
+ }
42
+ const issueNumber = resourceIdentityNumber(issueIdentity);
43
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
44
+ const context = loaded.ok ? loaded.value.context : loaded.context;
36
45
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
37
- if (!usable)
46
+ if (!usable || !loaded.ok)
38
47
  return { ok: false, output: result(context.status, resolved.taskId, issueNumber, {
39
48
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
40
49
  }) };
41
- return { ok: true, resolved, content, frontmatter, issueNumber, client, context };
50
+ return { ok: true, resolved, content, frontmatter, issueIdentity, issueNumber, client: options.client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
51
+ }
52
+ function normalizeProviderIssue(remote, repository, fallbackNumber) {
53
+ const identity = remote.identity || (remote.number ? { kind: 'number', value: remote.number } : { kind: 'id', value: remote.id });
54
+ const snapshot = {
55
+ repository,
56
+ number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber ?? 0,
57
+ identity,
58
+ databaseId: null,
59
+ nodeId: remote.id,
60
+ url: remote.displayUrl || '',
61
+ state: remote.state,
62
+ title: remote.title,
63
+ body: remote.body,
64
+ labels: [...remote.labels].sort(),
65
+ assignees: [...remote.assignees].sort(),
66
+ milestone: remote.milestone,
67
+ issueType: remote.issueType?.name || null,
68
+ fields: { ...remote.fields }
69
+ };
70
+ Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
71
+ if (remote.issueType)
72
+ Object.defineProperty(snapshot, 'issueTypeSnapshot', { value: remote.issueType, enumerable: false, configurable: true });
73
+ return snapshot;
74
+ }
75
+ function planProviderMetadata(remote, metadata, frontmatter, options, capabilities) {
76
+ if (!options.issueType && !options.fields)
77
+ return [];
78
+ if (!capabilities.push)
79
+ return [
80
+ ...(options.issueType ? [{ name: 'issue-type', status: 'skipped', reasonCode: 'PUSH_REQUIRED' }] : []),
81
+ ...(options.fields ? [{ name: 'fields', status: 'skipped', reasonCode: 'PUSH_REQUIRED' }] : [])
82
+ ];
83
+ const target = metadata.issueTypes.find((candidate) => candidate.name === desiredIssueType(frontmatter.type || ''));
84
+ if (!target)
85
+ return [
86
+ ...(options.issueType ? [{ name: 'issue-type', status: 'skipped', reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }] : []),
87
+ ...(options.fields ? [{ name: 'fields', status: 'skipped', reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }] : [])
88
+ ];
89
+ const operations = [];
90
+ if (options.issueType) {
91
+ operations.push(remote.issueType && resourceIdentityEquals(remote.issueType.identity, target.identity)
92
+ ? { name: 'issue-type', status: 'no-op', reasonCode: null }
93
+ : { name: 'issue-type', status: 'planned', reasonCode: null, value: target.name });
94
+ }
95
+ if (options.fields) {
96
+ const fields = target.fields.map((field) => ({
97
+ id: resourceIdentityString(field.identity) || '',
98
+ name: field.name,
99
+ kind: field.kind,
100
+ options: field.options.map((option) => ({ id: resourceIdentityString(option.identity) || '', name: option.name }))
101
+ }));
102
+ const desired = desiredFieldValues(frontmatter, fields);
103
+ const values = Object.fromEntries(desired.map((field) => [field.name, field.value]));
104
+ const changed = Object.entries(values).some(([name, value]) => remote.fields[name] !== value);
105
+ operations.push(changed
106
+ ? { name: 'fields', status: 'planned', reasonCode: null, value: values }
107
+ : { name: 'fields', status: 'no-op', reasonCode: null });
108
+ }
109
+ return operations;
110
+ }
111
+ function providerIssueError(base, error, issueIdentity) {
112
+ const issueNumber = resourceIdentityNumber(issueIdentity);
113
+ return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, issueNumber, {
114
+ changed: error.code === 'IN_LABEL_SYNC_PARTIAL',
115
+ platform: base.context.platform,
116
+ capabilities: base.context.capabilities,
117
+ resource: { kind: 'issue', number: issueNumber, ...(issueIdentity ? { identity: issueIdentity } : {}) },
118
+ error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
119
+ });
120
+ }
121
+ async function inspectExternalIssue(base, issueIdentity) {
122
+ const operation = base.provider.issues?.inspect
123
+ ? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: issueIdentity })
124
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
125
+ if (!operation.ok)
126
+ return providerIssueError(base, operation.error, issueIdentity);
127
+ const issue = normalizeProviderIssue(operation.value, base.context.platform.repository, resourceIdentityNumber(issueIdentity));
128
+ return result('no-op', base.resolved.taskId, resourceIdentityNumber(issueIdentity), {
129
+ platform: base.context.platform,
130
+ capabilities: base.context.capabilities,
131
+ resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity },
132
+ issue,
133
+ error: null
134
+ });
42
135
  }
43
- function normalizeIssue(remote, repository) {
44
- if (!Number.isInteger(remote.number) || Number(remote.number) <= 0 || !remote.node_id || remote.pull_request)
136
+ function normalizeIssue(remote, repository, fallbackNumber) {
137
+ const number = Number.isInteger(remote.number) && Number(remote.number) > 0
138
+ ? Number(remote.number)
139
+ : fallbackNumber;
140
+ if (!number || !Number.isSafeInteger(number) || remote.pull_request)
45
141
  return null;
46
142
  return {
47
143
  repository,
48
- number: Number(remote.number),
144
+ number,
49
145
  databaseId: Number.isInteger(remote.id) ? Number(remote.id) : null,
50
- nodeId: remote.node_id,
51
- url: remote.html_url || `https://github.com/${repository}/issues/${remote.number}`,
52
- state: remote.state === 'closed' ? 'closed' : 'open',
146
+ nodeId: remote.node_id || `issue-${number}`,
147
+ url: remote.html_url || `https://github.com/${repository}/issues/${number}`,
148
+ state: String(remote.state || '').toLowerCase() === 'closed' ? 'closed' : 'open',
53
149
  title: remote.title || '',
54
150
  body: remote.body || '',
55
151
  labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
56
152
  assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
57
153
  milestone: remote.milestone?.title || null,
58
- issueType: null,
154
+ issueType: remote.type?.name || null,
59
155
  fields: {}
60
156
  };
61
157
  }
@@ -93,15 +189,17 @@ function normalizeIssueTypes(value) {
93
189
  }
94
190
  function normalizeCurrentFields(value) {
95
191
  const issue = value?.data?.repository?.issue;
96
- const type = issue?.issueType?.id && issue.issueType.name ? {
97
- id: issue.issueType.id, name: issue.issueType.name, fields: normalizeFieldSchemas(issue.issueType.pinnedFields)
192
+ const type = issue?.issueType?.name ? {
193
+ id: issue.issueType.id || issue.issueType.name,
194
+ name: issue.issueType.name,
195
+ fields: normalizeFieldSchemas(issue.issueType.pinnedFields)
98
196
  } : null;
99
197
  const values = (issue?.issueFieldValues?.nodes || []).flatMap((raw) => {
100
198
  const item = raw;
101
199
  const kind = fieldKind(item);
102
200
  const value = kind === 'single-select' ? item.name : item.value;
103
- return item.field?.id && item.field.name && kind && value !== undefined
104
- ? [{ id: item.field.id, name: item.field.name, kind, value: value ?? null }]
201
+ return item.field?.name && kind && value !== undefined
202
+ ? [{ id: item.field.id || item.field.name, name: item.field.name, kind, value: value ?? null }]
105
203
  : [];
106
204
  });
107
205
  return { issueId: issue?.id || null, type, values };
@@ -124,7 +222,7 @@ function inspectGitHubIssue(client, repository, issue, cwd) {
124
222
  const fetched = fetchIssue(client, repository, issue, cwd);
125
223
  if (!fetched.ok)
126
224
  return fetched;
127
- const snapshot = normalizeIssue(fetched.value, repository);
225
+ const snapshot = normalizeIssue(fetched.value, repository, issue);
128
226
  return snapshot
129
227
  ? { ok: true, value: snapshot }
130
228
  : { ok: false, error: { code: 'ISSUE_IDENTITY_INVALID', message: 'Remote resource is not a valid Issue', retryable: false } };
@@ -144,33 +242,16 @@ function inspectGitHubIssueMetadata(client, repository, issue, cwd) {
144
242
  }
145
243
  };
146
244
  }
147
- function inspectPlatformIssue(taskRef, options = {}) {
148
- const base = resolvedContext(taskRef, options);
245
+ async function inspectPlatformIssue(taskRef, options = {}) {
246
+ const base = await resolvedContext(taskRef, options);
149
247
  if (!base.ok)
150
248
  return base.output;
151
- if (!base.issueNumber)
249
+ if (!base.issueIdentity)
152
250
  return result('no-op', base.resolved.taskId, null, {
153
251
  platform: base.context.platform, capabilities: base.context.capabilities,
154
- error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
155
- });
156
- const fetched = inspectGitHubIssue(base.client, base.context.platform.repository, base.issueNumber, base.resolved.repoRoot);
157
- if (!fetched.ok)
158
- return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, {
159
- platform: base.context.platform, capabilities: base.context.capabilities,
160
- resource: { kind: 'issue', number: base.issueNumber }, error: fetched.error
252
+ error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
161
253
  });
162
- const issue = fetched.value;
163
- if (base.context.capabilities.push) {
164
- const graph = graphState(base.client, base.context.platform.repository, base.issueNumber, base.resolved.repoRoot);
165
- if (graph) {
166
- issue.issueType = graph.type?.name || null;
167
- issue.fields = Object.fromEntries(graph.values.map((field) => [field.name, field.value]));
168
- }
169
- }
170
- return result('no-op', base.resolved.taskId, base.issueNumber, {
171
- platform: base.context.platform, capabilities: base.context.capabilities,
172
- resource: { kind: 'issue', number: base.issueNumber }, issue, error: null
173
- });
254
+ return inspectExternalIssue(base, base.issueIdentity);
174
255
  }
175
256
  function taskTitle(content) {
176
257
  return extractTitle(content).replace(/^(?:任务:|Task:\s*)/i, '');
@@ -239,114 +320,128 @@ function deterministicIssueBody(repoRoot, content, taskType) {
239
320
  }
240
321
  return buildDefaultBody(content);
241
322
  }
242
- function createPlatformIssue(taskRef, options) {
243
- const base = resolvedContext(taskRef, options);
323
+ async function createPlatformIssue(taskRef, options) {
324
+ const base = await resolvedContext(taskRef, options);
244
325
  if (!base.ok)
245
326
  return base.output;
246
- if (base.issueNumber)
327
+ if (base.issueIdentity)
247
328
  return inspectPlatformIssue(taskRef, options);
248
329
  const repository = base.context.platform.repository;
249
330
  let repositoryLabels = [];
250
331
  let milestones = [];
251
332
  if (base.context.capabilities.triage) {
252
- const labels = base.client.json(['api', '--paginate', '--slurp', `repos/${repository}/labels?per_page=100`], { cwd: base.resolved.repoRoot });
253
- if (labels.ok)
254
- repositoryLabels = listNames(labels.value);
255
- const listed = base.client.json(['api', '--paginate', '--slurp', `repos/${repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
256
- if (listed.ok)
257
- milestones = flattenPages(listed.value).map((item) => ({
258
- title: String(item.title || ''), number: Number(item.number)
259
- })).filter((item) => item.title && Number.isInteger(item.number));
333
+ const metadata = base.provider.issues?.describeRepository
334
+ ? await base.provider.issues.describeRepository({ context: providerOperationContext(base.loadedContext) })
335
+ : unsupportedProviderOperation(base.provider, 'issues.describeRepository');
336
+ if (!metadata.ok)
337
+ return providerIssueError(base, metadata.error, null);
338
+ repositoryLabels = metadata.value.labels.map((label) => label.name);
339
+ milestones = metadata.value.milestones.map((milestone) => milestone.title);
260
340
  }
261
341
  const typeLabel = {
262
342
  bug: 'type: bug', bugfix: 'type: bug', feature: 'type: feature', enhancement: 'type: enhancement',
263
343
  refactor: 'type: enhancement', refactoring: 'type: enhancement', documentation: 'type: documentation',
264
344
  docs: 'type: documentation', 'dependency-upgrade': 'type: dependency-upgrade', task: 'type: task', chore: 'type: task'
265
345
  }[base.frontmatter.type || ''];
266
- const explicitMilestone = base.frontmatter.milestone && milestones.some((item) => item.title === base.frontmatter.milestone)
346
+ const explicitMilestone = base.frontmatter.milestone && milestones.some((item) => item === base.frontmatter.milestone)
267
347
  ? base.frontmatter.milestone
268
- : chooseMilestone('initial', milestones.map((item) => item.title));
348
+ : chooseMilestone('initial', milestones);
269
349
  const payload = {
270
350
  title: conventionalTitle(base.content, base.frontmatter.type || 'task', base.resolved.repoRoot),
271
351
  body: deterministicIssueBody(base.resolved.repoRoot, base.content, base.frontmatter.type || 'task'),
272
352
  assignees: base.context.platform.currentUser ? [base.context.platform.currentUser] : [],
273
353
  ...(typeLabel && repositoryLabels.includes(typeLabel) ? { labels: [typeLabel] } : {}),
274
- ...(explicitMilestone ? { milestone: milestones.find((item) => item.title === explicitMilestone)?.number } : {})
354
+ ...(explicitMilestone ? { milestone: explicitMilestone } : {})
275
355
  };
276
356
  if (options.dryRun)
277
357
  return result('planned', base.resolved.taskId, null, {
278
358
  platform: base.context.platform, capabilities: base.context.capabilities,
279
359
  operations: [{ name: 'issue:create', status: 'planned', reasonCode: null }], error: null
280
360
  });
281
- const created = base.client.json(['api', `repos/${repository}/issues`, '-X', 'POST', '--input', '-'], {
282
- cwd: base.resolved.repoRoot, method: 'POST', input: JSON.stringify(payload)
283
- });
284
- if (!created.ok)
285
- return result(created.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, null, {
286
- platform: base.context.platform, capabilities: base.context.capabilities,
287
- operations: [{ name: 'issue:create', status: 'failed', reasonCode: created.error.code }], error: {
288
- ...created.error,
289
- code: created.error.retryable ? 'ISSUE_CREATE_OUTCOME_UNKNOWN' : created.error.code
290
- }
291
- });
292
- const issueNumber = Number(created.value.number);
293
- if (!Number.isInteger(issueNumber) || issueNumber <= 0 || !created.value.node_id || !created.value.html_url) {
294
- return result('failed', base.resolved.taskId, null, {
295
- platform: base.context.platform, capabilities: base.context.capabilities,
296
- error: { code: 'ISSUE_CREATE_RESPONSE_INVALID', message: 'Issue create response lacks validated identity', retryable: false }
361
+ {
362
+ const created = base.provider.issues?.create
363
+ ? await base.provider.issues.create({
364
+ context: providerOperationContext(base.loadedContext),
365
+ desired: {
366
+ title: payload.title,
367
+ body: payload.body,
368
+ labels: payload.labels || [],
369
+ assignees: payload.assignees,
370
+ milestone: typeof payload.milestone === 'string' ? payload.milestone : null,
371
+ fields: {}
372
+ },
373
+ mutation: { idempotencyKey: `issue:create:${base.resolved.taskId}` }
374
+ })
375
+ : unsupportedProviderOperation(base.provider, 'issues.create');
376
+ if (!created.ok)
377
+ return providerIssueError(base, created.error, null);
378
+ let createdIdentity;
379
+ try {
380
+ createdIdentity = providerResourceIdentity(base.provider, 'issue', created.value.remoteId);
381
+ }
382
+ catch (error) {
383
+ return result('failed', base.resolved.taskId, null, {
384
+ platform: base.context.platform, capabilities: base.context.capabilities,
385
+ error: { code: 'ISSUE_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
386
+ });
387
+ }
388
+ const inspected = base.provider.issues?.inspect
389
+ ? await base.provider.issues.inspect({
390
+ context: providerOperationContext(base.loadedContext),
391
+ target: createdIdentity
392
+ })
393
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
394
+ if (!inspected.ok)
395
+ return providerIssueError(base, inspected.error, createdIdentity);
396
+ const issueNumber = resourceIdentityNumber(inspected.value.identity || createdIdentity);
397
+ const written = writeTask({
398
+ taskRef: base.resolved.taskId, expectedState: 'active', mutations: [{ kind: 'frontmatter', set: { platform_issue_identity: serializeResourceIdentity(inspected.value.identity || createdIdentity) } }]
399
+ }, { repoRoot: base.resolved.repoRoot });
400
+ if (written.status === 'failed')
401
+ return result('failed', base.resolved.taskId, null, {
402
+ platform: base.context.platform, capabilities: base.context.capabilities,
403
+ resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
404
+ error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: false }
405
+ });
406
+ return result('applied', base.resolved.taskId, issueNumber, {
407
+ changed: true,
408
+ platform: base.context.platform,
409
+ capabilities: base.context.capabilities,
410
+ resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
411
+ operations: [{ name: 'issue:create', status: 'applied', reasonCode: null }, { name: 'task:bind', status: 'applied', reasonCode: null }],
412
+ issue: normalizeProviderIssue(inspected.value, repository, issueNumber),
413
+ error: null
297
414
  });
298
415
  }
299
- const written = writeTask({
300
- taskRef: base.resolved.taskId, expectedState: 'active', mutations: [{ kind: 'frontmatter', set: { issue_number: issueNumber } }]
301
- }, { repoRoot: base.resolved.repoRoot });
302
- if (written.status === 'failed')
303
- return result('failed', base.resolved.taskId, null, {
304
- platform: base.context.platform, capabilities: base.context.capabilities,
305
- resource: { kind: 'issue', number: issueNumber },
306
- operations: [{ name: 'issue:create', status: 'applied', reasonCode: null }, { name: 'task:bind', status: 'failed', reasonCode: written.error.code }],
307
- error: { code: 'ISSUE_CREATED_BIND_FAILED', message: `${created.value.html_url}: ${written.error.message}`, retryable: false }
308
- });
309
- return result('applied', base.resolved.taskId, issueNumber, {
310
- changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
311
- resource: { kind: 'issue', number: issueNumber },
312
- operations: [{ name: 'issue:create', status: 'applied', reasonCode: null }, { name: 'task:bind', status: 'applied', reasonCode: null }],
313
- issue: normalizeIssue({
314
- ...created.value,
315
- state: 'open',
316
- title: payload.title,
317
- body: payload.body,
318
- labels: [],
319
- assignees: payload.assignees.map((login) => ({ login })),
320
- milestone: null
321
- }, repository),
322
- error: null
323
- });
324
416
  }
325
- function bindPlatformIssue(taskRef, options) {
326
- const base = resolvedContext(taskRef, options);
417
+ async function bindPlatformIssue(taskRef, options) {
418
+ const base = await resolvedContext(taskRef, options);
327
419
  if (!base.ok)
328
420
  return base.output;
329
- if (!Number.isInteger(options.issue) || options.issue <= 0)
330
- return result('failed', base.resolved.taskId, base.issueNumber, {
331
- error: { code: 'ISSUE_NUMBER_INVALID', message: 'Issue number must be positive', retryable: false }
332
- });
333
- if (base.issueNumber && base.issueNumber !== options.issue)
421
+ let identity;
422
+ try {
423
+ identity = providerResourceToken(base.provider, 'issue', String(options.issue));
424
+ }
425
+ catch (error) {
426
+ return result('failed', base.resolved.taskId, base.issueNumber, { error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
427
+ }
428
+ if (base.issueIdentity && !resourceIdentityEquals(base.issueIdentity, identity))
334
429
  return result('failed', base.resolved.taskId, base.issueNumber, {
335
- error: { code: 'ISSUE_BIND_CONFLICT', message: `Task is already bound to Issue #${base.issueNumber}`, retryable: false }
430
+ error: { code: 'ISSUE_BIND_CONFLICT', message: 'Task is already bound to a different Issue identity', retryable: false }
336
431
  });
337
- const fetched = fetchIssue(base.client, base.context.platform.repository, options.issue, base.resolved.repoRoot);
432
+ const fetched = base.provider.issues?.inspect
433
+ ? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: identity })
434
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
338
435
  if (!fetched.ok)
339
- return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { error: fetched.error });
340
- const issue = normalizeIssue(fetched.value, base.context.platform.repository);
341
- if (!issue)
342
- return result('failed', base.resolved.taskId, base.issueNumber, { error: { code: 'ISSUE_IDENTITY_INVALID', message: 'Remote resource is not a valid Issue', retryable: false } });
343
- if (base.issueNumber === options.issue)
344
- return result('no-op', base.resolved.taskId, options.issue, { issue, error: null });
345
- const written = writeTask({ taskRef: base.resolved.taskId, expectedState: 'active', dryRun: options.dryRun, mutations: [{ kind: 'frontmatter', set: { issue_number: options.issue } }] }, { repoRoot: base.resolved.repoRoot });
436
+ return providerIssueError(base, fetched.error, identity);
437
+ const issue = normalizeProviderIssue(fetched.value, base.context.platform.repository, resourceIdentityNumber(identity));
438
+ if (base.issueIdentity && resourceIdentityEquals(base.issueIdentity, identity))
439
+ return result('no-op', base.resolved.taskId, resourceIdentityNumber(identity), { issue, error: null });
440
+ const written = writeTask({ taskRef: base.resolved.taskId, expectedState: 'active', dryRun: options.dryRun, mutations: [{ kind: 'frontmatter', set: { platform_issue_identity: serializeResourceIdentity(identity) } }] }, { repoRoot: base.resolved.repoRoot });
346
441
  if (written.status === 'failed')
347
442
  return result('failed', base.resolved.taskId, null, { issue, error: { code: written.error.code, message: written.error.message, retryable: false } });
348
- return result(options.dryRun ? 'planned' : 'applied', base.resolved.taskId, options.dryRun ? null : options.issue, {
349
- changed: !options.dryRun, issue, operations: [{ name: 'task:bind', status: options.dryRun ? 'planned' : 'applied', reasonCode: null }], error: null
443
+ return result(options.dryRun ? 'planned' : 'applied', base.resolved.taskId, options.dryRun ? null : resourceIdentityNumber(identity), {
444
+ changed: !options.dryRun, issue, resource: { kind: 'issue', number: resourceIdentityNumber(identity), identity }, operations: [{ name: 'task:bind', status: options.dryRun ? 'planned' : 'applied', reasonCode: null }], error: null
350
445
  });
351
446
  }
352
447
  function requirementsFromTask(content) {
@@ -398,20 +493,6 @@ function applyRestOperations(snapshot, operations) {
398
493
  for (const operation of operations) {
399
494
  if (operation.status !== 'planned')
400
495
  continue;
401
- if (operation.name === 'labels:status') {
402
- labels = [
403
- ...labels.filter((label) => !label.startsWith('status:')),
404
- ...operation.value.filter((label) => label.startsWith('status:'))
405
- ];
406
- labelsChanged = true;
407
- }
408
- if (operation.name === 'labels:in') {
409
- labels = [
410
- ...labels.filter((label) => !label.startsWith('in:')),
411
- ...operation.value.filter((label) => label.startsWith('in:'))
412
- ];
413
- labelsChanged = true;
414
- }
415
496
  if (operation.name === 'assignees')
416
497
  payload.assignees = operation.value;
417
498
  if (operation.name === 'requirements')
@@ -420,11 +501,31 @@ function applyRestOperations(snapshot, operations) {
420
501
  payload.state = operation.value;
421
502
  if (operation.name === 'milestone')
422
503
  payload.milestone = operation.value;
504
+ if (operation.name === 'labels:status') {
505
+ labels = applyLabelDeltaToSnapshot(labels, operation.value, 'status:');
506
+ labelsChanged = true;
507
+ }
508
+ if (operation.name === 'labels:in') {
509
+ labels = applyLabelDeltaToSnapshot(labels, operation.value, 'in:');
510
+ labelsChanged = true;
511
+ }
512
+ if (operation.name === 'issue-type')
513
+ payload.issueType = operation.value;
514
+ if (operation.name === 'fields')
515
+ payload.fields = operation.value;
423
516
  }
424
517
  if (labelsChanged)
425
- payload.labels = [...new Set(labels)].sort();
518
+ payload.labels = labels;
426
519
  return payload;
427
520
  }
521
+ function applyLabelDeltaToSnapshot(current, target, prefix) {
522
+ const delta = labelDelta(current, target, prefix);
523
+ const targetLabels = [...new Set(target.filter((label) => label.startsWith(prefix)))];
524
+ return [...new Set([
525
+ ...current.filter((label) => !delta.remove.includes(label)),
526
+ ...targetLabels
527
+ ])].sort();
528
+ }
428
529
  function desiredFieldValues(frontmatter, fields) {
429
530
  const mapping = [
430
531
  ['priority', 'Priority'], ['effort', 'Effort'], ['start_date', 'Start date'], ['target_date', 'Target date']
@@ -512,32 +613,31 @@ function executeGraphOperation(client, cwd, issueId, operation) {
512
613
  cwd, method: 'POST', input: JSON.stringify({ query, variables: { issueId, issueFields: operation.value } })
513
614
  });
514
615
  }
515
- function syncPlatformIssue(taskRef, options) {
516
- const base = resolvedContext(taskRef, options);
616
+ async function syncPlatformIssue(taskRef, options) {
617
+ const base = await resolvedContext(taskRef, options);
517
618
  if (!base.ok)
518
619
  return base.output;
519
- if (!base.issueNumber)
520
- return result('failed', base.resolved.taskId, null, { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false } });
620
+ if (!base.issueIdentity)
621
+ return result('failed', base.resolved.taskId, null, { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
521
622
  const repository = base.context.platform.repository;
522
- const fetched = fetchIssue(base.client, repository, base.issueNumber, base.resolved.repoRoot);
523
- if (!fetched.ok)
524
- return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { error: fetched.error });
525
- const snapshot = normalizeIssue(fetched.value, repository);
526
- if (!snapshot)
527
- return result('failed', base.resolved.taskId, base.issueNumber, { error: { code: 'ISSUE_IDENTITY_INVALID', message: 'Remote resource is not a valid Issue', retryable: false } });
623
+ const inspected = base.provider.issues?.inspect
624
+ ? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
625
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
626
+ if (!inspected.ok)
627
+ return providerIssueError(base, inspected.error, base.issueIdentity);
628
+ const snapshot = normalizeProviderIssue(inspected.value, repository, resourceIdentityNumber(base.issueIdentity));
629
+ let providerMetadata = null;
528
630
  let repositoryLabels = [];
529
- if (options.status !== undefined || options.inLabels !== undefined) {
530
- const labels = base.client.json(['api', '--paginate', '--slurp', `repos/${repository}/labels?per_page=100`], { cwd: base.resolved.repoRoot });
531
- if (!labels.ok)
532
- return result(labels.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, error: labels.error });
533
- repositoryLabels = listNames(labels.value).flatMap((name) => name);
534
- }
535
631
  let milestones = [];
536
- if (options.milestone !== undefined) {
537
- const listed = base.client.json(['api', '--paginate', '--slurp', `repos/${repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
538
- if (!listed.ok)
539
- return result(listed.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, error: listed.error });
540
- milestones = flattenPages(listed.value).map((item) => ({ title: String(item.title || ''), number: Number(item.number) })).filter((item) => item.title && Number.isInteger(item.number));
632
+ if (base.context.capabilities.triage && (options.status !== undefined || options.inLabels !== undefined || options.milestone !== undefined || options.issueType || options.fields)) {
633
+ const metadata = base.provider.issues?.describeRepository
634
+ ? await base.provider.issues.describeRepository({ context: providerOperationContext(base.loadedContext) })
635
+ : unsupportedProviderOperation(base.provider, 'issues.describeRepository');
636
+ if (!metadata.ok)
637
+ return providerIssueError(base, metadata.error, base.issueIdentity);
638
+ providerMetadata = metadata.value;
639
+ repositoryLabels = metadata.value.labels.map((label) => label.name);
640
+ milestones = metadata.value.milestones.map((milestone) => milestone.title);
541
641
  }
542
642
  const desired = {
543
643
  status: options.status,
@@ -550,7 +650,7 @@ function syncPlatformIssue(taskRef, options) {
550
650
  snapshot: options.milestone === 'specific'
551
651
  ? { ...snapshot, milestone: milestoneBasis(base.resolved.repoRoot, snapshot.milestone) }
552
652
  : snapshot,
553
- desired, repositoryLabels: new Set(repositoryLabels), milestones: milestones.map((item) => item.title),
653
+ desired, repositoryLabels: new Set(repositoryLabels), milestones,
554
654
  currentUser: base.context.platform.currentUser,
555
655
  requirementAnchors: requirementSectionAnchors(base.resolved.repoRoot, base.frontmatter.type || 'task'),
556
656
  capabilities: base.context.capabilities
@@ -564,21 +664,54 @@ function syncPlatformIssue(taskRef, options) {
564
664
  : { name: 'labels:in', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' });
565
665
  }
566
666
  if (options.inLabels === 'from-diff') {
567
- try {
568
- const changed = execFileSync('git', ['diff', `${options.base}...HEAD`, '--name-only'], { cwd: base.resolved.repoRoot, encoding: 'utf8' }).trim().split('\n').filter(Boolean);
569
- const config = JSON.parse(fs.readFileSync(`${base.resolved.repoRoot}/.agents/.airc.json`, 'utf8'));
570
- const targets = computeInLabels(changed, config?.labels?.in || {}, new Set(repositoryLabels));
571
- const labels = [...snapshot.labels.filter((label) => !label.startsWith('in:')), ...targets].sort();
572
- plan.operations.push(labels.join('\0') === snapshot.labels.join('\0')
573
- ? { name: 'labels:in', status: 'no-op', reasonCode: null }
574
- : { name: 'labels:in', status: 'planned', reasonCode: null, value: labels });
667
+ const taskBase = typeof base.frontmatter.delivery_base_ref === 'string'
668
+ ? base.frontmatter.delivery_base_ref.trim()
669
+ : '';
670
+ if (!taskBase) {
671
+ plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: 'IN_LABEL_SYNC_BASE_MISSING' });
575
672
  }
576
- catch {
577
- plan.operations.push({ name: 'labels:in', status: 'skipped', reasonCode: 'DIFF_UNAVAILABLE' });
673
+ else if (options.base && options.base !== taskBase) {
674
+ plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: 'IN_LABEL_SYNC_BASE_MISMATCH' });
675
+ }
676
+ else {
677
+ try {
678
+ const changed = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
679
+ cwd: base.resolved.repoRoot, encoding: 'utf8'
680
+ }).trim().split(/\r?\n/).filter(Boolean);
681
+ const config = JSON.parse(fs.readFileSync(`${base.resolved.repoRoot}/.agents/.airc.json`, 'utf8'));
682
+ const mapping = validateInLabelMapping(config.labels?.in);
683
+ if (!mapping.ok) {
684
+ plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: mapping.error.code });
685
+ }
686
+ else {
687
+ const planned = planInLabelUpdate({
688
+ changedFiles: changed,
689
+ currentLabels: snapshot.labels,
690
+ mapping: mapping.value,
691
+ repositoryLabels: new Set(repositoryLabels)
692
+ });
693
+ if (planned.error) {
694
+ plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: planned.error.code });
695
+ }
696
+ else {
697
+ plan.operations.push(planned.changed
698
+ ? { name: 'labels:in', status: 'planned', reasonCode: null, value: planned.labels }
699
+ : { name: 'labels:in', status: 'no-op', reasonCode: null });
700
+ }
701
+ }
702
+ }
703
+ catch {
704
+ if (!plan.operations.some((operation) => operation.name === 'labels:in' && operation.status === 'failed')) {
705
+ plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE' });
706
+ }
707
+ }
578
708
  }
579
709
  }
580
- const graph = planGraphMetadata(base.client, repository, base.issueNumber, base.resolved.repoRoot, base.frontmatter, options, base.context.capabilities);
581
- plan.operations.push(...graph.operations);
710
+ if (options.issueType || options.fields) {
711
+ plan.operations.push(...(providerMetadata
712
+ ? planProviderMetadata(inspected.value, providerMetadata, base.frontmatter, options, base.context.capabilities)
713
+ : [{ name: 'issue-type', status: 'skipped', reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }]));
714
+ }
582
715
  const failure = plan.operations.find((operation) => operation.status === 'failed');
583
716
  if (failure)
584
717
  return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
@@ -588,32 +721,45 @@ function syncPlatformIssue(taskRef, options) {
588
721
  return result(planned.length ? 'planned' : skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
589
722
  if (planned.length === 0)
590
723
  return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
591
- const restPlanned = planned.filter((operation) => !['issue-type', 'fields'].includes(operation.name));
592
- const graphPlanned = planned.filter((operation) => ['issue-type', 'fields'].includes(operation.name));
593
- const payload = applyRestOperations(snapshot, restPlanned);
594
- if (typeof payload.milestone === 'string')
595
- payload.milestone = milestones.find((item) => item.title === payload.milestone)?.number ?? payload.milestone;
596
- if (options.closeReason)
597
- payload.state_reason = options.closeReason;
598
- if (Object.keys(payload).length > 0) {
599
- const patched = base.client.json(['api', `repos/${repository}/issues/${base.issueNumber}`, '-X', 'PATCH', '--input', '-'], {
600
- cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
601
- });
602
- if (!patched.ok)
603
- return result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: patched.error });
604
- }
605
- for (const operation of graphPlanned) {
606
- if (!graph.issueId)
607
- return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: 'ISSUE_GRAPH_IDENTITY_INVALID', message: 'Issue GraphQL identity is unavailable', retryable: false } });
608
- const written = executeGraphOperation(base.client, base.resolved.repoRoot, graph.issueId, operation);
609
- if (!written.ok)
610
- return result(written.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: written.error });
724
+ const payload = applyRestOperations(snapshot, planned);
725
+ const updated = base.provider.issues?.update
726
+ ? await base.provider.issues.update({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity, currentLabels: snapshot.labels, patch: payload, mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}` } })
727
+ : unsupportedProviderOperation(base.provider, 'issues.update');
728
+ if (!updated.ok)
729
+ return providerIssueError(base, updated.error, base.issueIdentity);
730
+ const labelOperations = planned.filter((operation) => operation.name === 'labels:status' || operation.name === 'labels:in');
731
+ let finalIssue = snapshot;
732
+ if (labelOperations.length > 0) {
733
+ const reread = base.provider.issues?.inspect
734
+ ? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
735
+ : unsupportedProviderOperation(base.provider, 'issues.inspect');
736
+ if (!reread.ok)
737
+ return result('blocked', base.resolved.taskId, base.issueNumber, {
738
+ changed: updated.value.changed,
739
+ issue: snapshot,
740
+ operations: plan.operations,
741
+ error: { ...reread.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
742
+ });
743
+ finalIssue = normalizeProviderIssue(reread.value, repository, resourceIdentityNumber(base.issueIdentity));
744
+ let expectedLabels = [...snapshot.labels];
745
+ for (const operation of labelOperations) {
746
+ expectedLabels = applyLabelDeltaToSnapshot(expectedLabels, operation.value, operation.name === 'labels:status' ? 'status:' : 'in:');
747
+ }
748
+ const expected = expectedLabels.filter((label) => label.startsWith('status:') || label.startsWith('in:')).sort();
749
+ const actual = finalIssue.labels.filter((label) => label.startsWith('status:') || label.startsWith('in:')).sort();
750
+ if (expected.join('\0') !== actual.join('\0'))
751
+ return result('blocked', base.resolved.taskId, base.issueNumber, {
752
+ changed: updated.value.changed,
753
+ issue: finalIssue,
754
+ operations: plan.operations,
755
+ error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
756
+ });
611
757
  }
612
758
  return result(skipped ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, {
613
- changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
614
- resource: { kind: 'issue', number: base.issueNumber }, issue: snapshot,
759
+ changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
760
+ resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
615
761
  operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
616
762
  });
617
763
  }
618
- export { bindPlatformIssue, createPlatformIssue, inspectGitHubIssue, inspectGitHubIssueMetadata, inspectPlatformIssue, normalizeIssue, requirementSectionAnchors, syncPlatformIssue };
764
+ export { bindPlatformIssue, createPlatformIssue, graphState, inspectGitHubIssue, inspectGitHubIssueMetadata, inspectPlatformIssue, normalizeIssue, requirementSectionAnchors, syncPlatformIssue };
619
765
  //# sourceMappingURL=issues.js.map