@fitlab-ai/agent-infra 0.11.4 → 0.11.6

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 (149) hide show
  1. package/dist/bin/internal-cli.js +0 -5
  2. package/dist/lib/decide.js +4 -41
  3. package/dist/lib/git/branch-worktree.js +45 -0
  4. package/dist/lib/internal/cli-route-inventory.js +0 -3
  5. package/dist/lib/internal/task-activity.js +7 -40
  6. package/dist/lib/internal/task-event.js +4 -37
  7. package/dist/lib/internal/task-finalization.js +9 -2
  8. package/dist/lib/internal/task-ledger.js +5 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +1 -4
  11. package/dist/lib/internal/task-review.js +5 -37
  12. package/dist/lib/internal/task-verify.js +3 -47
  13. package/dist/lib/internal/task-warning.js +4 -40
  14. package/dist/lib/merge.js +62 -10
  15. package/dist/lib/platform/github-provider.js +9 -14
  16. package/dist/lib/platform/github-release-notes.js +80 -11
  17. package/dist/lib/platform/none-provider.js +1 -1
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/provider-contract.d.ts +39 -16
  20. package/dist/lib/platform/provider-contract.js +1 -1
  21. package/dist/lib/platform/provider-validation.js +83 -13
  22. package/dist/lib/platform/release-notes.js +7 -8
  23. package/dist/lib/platform/verification-sync.js +12 -49
  24. package/dist/lib/sandbox/capture.js +1 -1
  25. package/dist/lib/sandbox/commands/create.js +46 -1
  26. package/dist/lib/sandbox/commands/enter.js +1 -1
  27. package/dist/lib/sandbox/commands/start.js +1 -1
  28. package/dist/lib/sandbox/config.js +10 -0
  29. package/dist/lib/sandbox/control/client.js +4 -1
  30. package/dist/lib/sandbox/control/executor.js +12 -4
  31. package/dist/lib/sandbox/control/lifecycle.js +18 -13
  32. package/dist/lib/sandbox/control/protocol.js +2 -1
  33. package/dist/lib/sandbox/removal.js +15 -8
  34. package/dist/lib/sandbox/task-resolver.js +62 -2
  35. package/dist/lib/task/activity-intent.js +1 -2
  36. package/dist/lib/task/artifact-operations.js +36 -2
  37. package/dist/lib/task/artifact-schema.js +7 -7
  38. package/dist/lib/task/control-authority.js +9 -166
  39. package/dist/lib/task/decision-intents.js +2 -4
  40. package/dist/lib/task/events.js +7 -28
  41. package/dist/lib/task/finalization-handoff.js +92 -0
  42. package/dist/lib/task/finalization.js +123 -12
  43. package/dist/lib/task/intermediate-cleanup.js +9 -148
  44. package/dist/lib/task/ledger-intents.js +3 -6
  45. package/dist/lib/task/lifecycle.js +50 -11
  46. package/dist/lib/task/local-artifact-finalization.js +13 -28
  47. package/dist/lib/task/resolve-ref.js +3 -3
  48. package/dist/lib/task/review-command.js +1 -2
  49. package/dist/lib/task/review-finalization.js +1 -2
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +16 -11
  52. package/dist/lib/task/verification.js +1 -13
  53. package/dist/lib/task/workflow-warning-intents.js +2 -4
  54. package/dist/lib/task/write.js +122 -3
  55. package/dist/lifecycle-build-manifest.json +3 -3
  56. package/lib/decide.ts +5 -35
  57. package/lib/git/branch-worktree.ts +41 -0
  58. package/lib/internal/cli-route-inventory.ts +0 -2
  59. package/lib/internal/task-activity.ts +6 -34
  60. package/lib/internal/task-event.ts +4 -37
  61. package/lib/internal/task-finalization.ts +9 -2
  62. package/lib/internal/task-ledger.ts +5 -38
  63. package/lib/internal/task-lifecycle.ts +2 -2
  64. package/lib/internal/task-operation-registry.ts +1 -4
  65. package/lib/internal/task-review.ts +5 -31
  66. package/lib/internal/task-verify.ts +3 -39
  67. package/lib/internal/task-warning.ts +3 -32
  68. package/lib/merge.ts +59 -10
  69. package/lib/platform/github-provider.ts +11 -18
  70. package/lib/platform/github-release-notes.ts +89 -14
  71. package/lib/platform/none-provider.ts +1 -1
  72. package/lib/platform/pr-summary.ts +4 -25
  73. package/lib/platform/provider-contract.ts +38 -12
  74. package/lib/platform/provider-validation.ts +83 -12
  75. package/lib/platform/release-notes.ts +11 -12
  76. package/lib/platform/verification-sync.ts +13 -53
  77. package/lib/sandbox/capture.ts +1 -1
  78. package/lib/sandbox/commands/create.ts +52 -1
  79. package/lib/sandbox/commands/enter.ts +1 -1
  80. package/lib/sandbox/commands/start.ts +1 -1
  81. package/lib/sandbox/config.ts +12 -0
  82. package/lib/sandbox/control/client.ts +5 -1
  83. package/lib/sandbox/control/executor.ts +13 -4
  84. package/lib/sandbox/control/lifecycle.ts +22 -13
  85. package/lib/sandbox/control/protocol.ts +3 -2
  86. package/lib/sandbox/removal.ts +23 -7
  87. package/lib/sandbox/task-resolver.ts +51 -2
  88. package/lib/task/activity-intent.ts +1 -2
  89. package/lib/task/artifact-operations.ts +58 -2
  90. package/lib/task/artifact-schema.ts +9 -7
  91. package/lib/task/control-authority.ts +14 -188
  92. package/lib/task/decision-intents.ts +2 -4
  93. package/lib/task/events.ts +8 -27
  94. package/lib/task/finalization-handoff.ts +104 -0
  95. package/lib/task/finalization.ts +120 -12
  96. package/lib/task/intermediate-cleanup.ts +9 -125
  97. package/lib/task/ledger-intents.ts +3 -6
  98. package/lib/task/lifecycle.ts +45 -13
  99. package/lib/task/local-artifact-finalization.ts +16 -35
  100. package/lib/task/resolve-ref.ts +3 -3
  101. package/lib/task/review-command.ts +2 -5
  102. package/lib/task/review-finalization.ts +1 -4
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +20 -10
  105. package/lib/task/verification.ts +1 -15
  106. package/lib/task/workflow-warning-intents.ts +2 -4
  107. package/lib/task/write.ts +116 -5
  108. package/package.json +1 -1
  109. package/templates/.agents/rules/evidence-reporting.en.md +1 -2
  110. package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  113. package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
  116. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  117. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  119. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  120. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  121. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  122. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
  125. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
  126. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  127. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  128. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  129. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  130. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  131. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  132. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  133. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  134. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  137. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  138. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  139. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  140. package/dist/lib/internal/task-override.js +0 -138
  141. package/dist/lib/task/artifact-recovery.js +0 -476
  142. package/dist/lib/task/artifact-repair-intent.js +0 -181
  143. package/dist/lib/task/guard-override.js +0 -31
  144. package/dist/lib/task/human-override.js +0 -1259
  145. package/lib/internal/task-override.ts +0 -144
  146. package/lib/task/artifact-recovery.ts +0 -572
  147. package/lib/task/artifact-repair-intent.ts +0 -227
  148. package/lib/task/guard-override.ts +0 -50
  149. package/lib/task/human-override.ts +0 -1522
package/lib/merge.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import crypto from 'node:crypto';
3
4
  import { removeDirRecursive } from './remove-dir.ts';
4
5
  import { validateCurrentTaskContract } from './task/current-contract.ts';
5
6
 
@@ -107,7 +108,7 @@ function extractTitle(content: string): string | null {
107
108
  }
108
109
 
109
110
  function normalizeTaskRecord(taskDir: string, taskFile: string, dateParts: DateParts): TaskRecord {
110
- const taskId = path.basename(taskDir);
111
+ const taskId = path.basename(taskDir) === 'local' ? path.basename(path.dirname(taskDir)) : path.basename(taskDir);
111
112
  const content = fs.readFileSync(taskFile, 'utf8');
112
113
  const completedAt = extractField(content, 'completed_at');
113
114
  const updatedAt = extractField(content, 'updated_at');
@@ -155,7 +156,7 @@ function scanSourceTasks(sourceDir: string): TaskRecord[] {
155
156
  continue;
156
157
  }
157
158
 
158
- const taskDir = path.join(dayDir, taskEntry.name);
159
+ const taskDir = path.join(dayDir, taskEntry.name, 'local');
159
160
  const taskFile = path.join(taskDir, 'task.md');
160
161
  if (!fs.existsSync(taskFile)) {
161
162
  continue;
@@ -251,7 +252,7 @@ function collectArchiveEntries(archiveDir: string): ArchiveManifestEntry[] {
251
252
  }
252
253
 
253
254
  const taskDir = path.join(dayDir, taskEntry.name);
254
- const taskFile = path.join(taskDir, 'task.md');
255
+ const taskFile = path.join(taskDir, 'local', 'task.md');
255
256
  const relativePath = `${yearEntry.name}/${monthEntry.name}/${dayEntry.name}/${taskEntry.name}/`;
256
257
  let title = taskEntry.name;
257
258
  let type = 'unknown';
@@ -624,10 +625,12 @@ function validateSourceRoot(sourceRoot: string): void {
624
625
  function validateTaskDirectory(
625
626
  sourceRoot: string,
626
627
  taskDir: string,
627
- seenTaskIds: Map<string, string>
628
+ seenTaskIds: Map<string, string>,
629
+ archived = false
628
630
  ): void {
629
631
  const taskId = path.basename(taskDir);
630
- const taskFile = path.join(taskDir, 'task.md');
632
+ const contentDir = archived ? path.join(taskDir, 'local') : taskDir;
633
+ const taskFile = path.join(contentDir, 'task.md');
631
634
  const taskDirStat = sourcePathStat(sourceRoot, taskDir);
632
635
  if (!taskDirStat || !taskDirStat.isDirectory()) {
633
636
  invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} must be a directory`);
@@ -646,6 +649,33 @@ function validateTaskDirectory(
646
649
  invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} id '${String(contract.metadata.id ?? 'missing')}' does not match directory '${taskId}'`);
647
650
  }
648
651
  validateTaskTree(sourceRoot, taskDir);
652
+ if (archived) {
653
+ for (const entry of fs.readdirSync(taskDir, { withFileTypes: true })) {
654
+ if (!['local', 'github', 'derived'].includes(entry.name) || !entry.isDirectory()) {
655
+ invalidSource(`${sourceRelativePath(sourceRoot, path.join(taskDir, entry.name))} is not an allowed archive source directory`);
656
+ }
657
+ }
658
+ validateArchiveSource(sourceRoot, contentDir);
659
+ const githubDir = path.join(taskDir, 'github');
660
+ if (fs.existsSync(githubDir)) {
661
+ for (const capture of fs.readdirSync(githubDir, { withFileTypes: true })) {
662
+ const captureDir = path.join(githubDir, capture.name);
663
+ if (!capture.isDirectory() || !/^capture-[A-Za-z0-9._-]+$/.test(capture.name) || !fs.existsSync(path.join(captureDir, 'capture.md'))) {
664
+ invalidSource(`${sourceRelativePath(sourceRoot, captureDir)} is not a valid GitHub capture`);
665
+ }
666
+ validateArchiveSource(sourceRoot, captureDir);
667
+ }
668
+ }
669
+ const derivedDir = path.join(taskDir, 'derived');
670
+ if (fs.existsSync(derivedDir)) {
671
+ for (const view of fs.readdirSync(derivedDir, { withFileTypes: true })) {
672
+ const viewDir = path.join(derivedDir, view.name);
673
+ if (!view.isDirectory() || !fs.existsSync(path.join(viewDir, 'inputs.md'))) {
674
+ invalidSource(`${sourceRelativePath(sourceRoot, viewDir)} requires inputs.md`);
675
+ }
676
+ }
677
+ }
678
+ }
649
679
 
650
680
  const previousPath = seenTaskIds.get(taskId);
651
681
  if (previousPath) {
@@ -654,6 +684,26 @@ function validateTaskDirectory(
654
684
  seenTaskIds.set(taskId, sourceRelativePath(sourceRoot, taskDir));
655
685
  }
656
686
 
687
+ function validateArchiveSource(sourceRoot: string, sourceDir: string): void {
688
+ const hashFile = path.join(sourceDir, 'contents.sha256');
689
+ const hashStat = sourcePathStat(sourceRoot, hashFile);
690
+ if (!hashStat?.isFile()) invalidSource(`${sourceRelativePath(sourceRoot, sourceDir)} requires contents.sha256`);
691
+ const files: string[] = [];
692
+ const walk = (dir: string) => {
693
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
694
+ const target = path.join(dir, entry.name);
695
+ if (entry.name === 'manifest.md') invalidSource(`${sourceRelativePath(sourceRoot, target)} is forbidden inside a TASK`);
696
+ if (entry.isDirectory()) walk(target);
697
+ else if (entry.isFile() && target !== hashFile) files.push(target);
698
+ }
699
+ };
700
+ walk(sourceDir);
701
+ const expected = files.sort((a, b) => Buffer.compare(Buffer.from(path.relative(sourceDir, a).split(path.sep).join('/')), Buffer.from(path.relative(sourceDir, b).split(path.sep).join('/'))))
702
+ .map((file) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${path.relative(sourceDir, file).split(path.sep).join('/')}`);
703
+ const actual = fs.readFileSync(hashFile, 'utf8').trimEnd().split(/\r?\n/).filter(Boolean);
704
+ if (JSON.stringify(expected) !== JSON.stringify(actual)) invalidSource(`${sourceRelativePath(sourceRoot, hashFile)} does not match local files`);
705
+ }
706
+
657
707
  function validateMutableSection(
658
708
  sourceRoot: string,
659
709
  section: MutableSection,
@@ -730,9 +780,6 @@ function validateArchiveSection(
730
780
  const monthDir = monthPath;
731
781
  for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
732
782
  const dayPath = path.join(monthDir, dayEntry.name);
733
- if (allowManifest(monthDir, dayEntry)) {
734
- continue;
735
- }
736
783
  const dayStat = sourcePathStat(sourceRoot, dayPath);
737
784
  if (!dayStat?.isDirectory() || !/^\d{2}$/.test(dayEntry.name)) {
738
785
  invalidSource(`${sourceRelativePath(sourceRoot, dayPath)} is not a DD directory or manifest.md`);
@@ -745,7 +792,7 @@ function validateArchiveSection(
745
792
  if (!taskStat?.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
746
793
  invalidSource(`${sourceRelativePath(sourceRoot, taskPath)} is not a TASK-YYYYMMDD-HHMMSS directory`);
747
794
  }
748
- validateTaskDirectory(sourceRoot, taskPath, seenTaskIds);
795
+ validateTaskDirectory(sourceRoot, taskPath, seenTaskIds, true);
749
796
  }
750
797
  }
751
798
  }
@@ -904,9 +951,10 @@ function mergeArchiveSection(sourceArchive: string, localArchive: string, report
904
951
  continue;
905
952
  }
906
953
 
954
+ const sourceTaskDir = path.dirname(task.taskDir);
907
955
  const destinationDir = path.join(localArchive, task.relativePath);
908
956
  fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
909
- fs.cpSync(task.taskDir, destinationDir, { recursive: true });
957
+ fs.cpSync(sourceTaskDir, destinationDir, { recursive: true });
910
958
  recordArchive(report, 'copied', {
911
959
  action: 'copied',
912
960
  symbol: '✓',
@@ -1045,6 +1093,7 @@ async function cmdMerge(args: string[]): Promise<void> {
1045
1093
  const report = createReport(resolvedSource, backupRootRelative);
1046
1094
  detectSourceMode(resolvedSource);
1047
1095
  validateSourceWorkspace(resolvedSource);
1096
+ validateArchiveSection(workspaceDir, new Map());
1048
1097
 
1049
1098
  for (const section of ALL_SECTIONS) {
1050
1099
  fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
@@ -923,30 +923,23 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
923
923
  if (collected.status === 'failed' || collected.status === 'blocked' || !('pullRequests' in collected)) {
924
924
  return { ok: false, error: collected.error || { code: 'RELEASE_NOTES_COLLECTION_FAILED', message: 'Release notes could not be collected', retryable: false } };
925
925
  }
926
- const mergedPullRequests = collected.pullRequests.map((item: any): ChangeRequestSnapshot => ({
927
- id: String(item.number), identity: { kind: 'number', value: Number(item.number) }, number: Number(item.number),
928
- state: 'closed', title: String(item.title || ''), body: String(item.body || ''),
926
+ const mergedPullRequests = collected.pullRequests.map((item: any) => ({
927
+ id: String(item.number), identity: { kind: 'number' as const, value: Number(item.number) }, number: Number(item.number),
928
+ title: String(item.title || ''), body: String(item.body || ''),
929
929
  mergedAt: new Date(String(item.mergedAt)).toISOString(), displayUrl: String(item.url || ''),
930
930
  labels: Array.isArray(item.labels) ? item.labels.map((label: any) => String(label.name || label)) : [],
931
- assignees: [], mergeCommitSha: null
932
- }));
933
- const closingIssues: IssueSnapshot[] = collected.pullRequests.flatMap((item: any) =>
934
- Array.isArray(item.closingIssuesReferences) ? item.closingIssuesReferences.map((issue: any) => ({
931
+ author: item.author,
932
+ closingIssues: item.closingIssues.map((issue: any) => ({
935
933
  id: String(issue.number), identity: { kind: 'number', value: Number(issue.number) }, number: Number(issue.number),
936
- state: 'closed' as const, title: String(issue.title || ''), body: '', labels: Array.isArray(issue.labels) ? issue.labels.map((label: any) => String(label.name || label)) : [],
937
- assignees: [], milestone: null, fields: {}, displayUrl: String(issue.url || '')
938
- })) : []
939
- );
940
- const actors = [...collected.authors.values()].flatMap((items) => items.map((item: any) => ({
941
- ...(item.login ? { id: String(item.login) } : {}), ...(item.name ? { name: String(item.name) } : {})
942
- })));
934
+ title: String(issue.title || ''), displayUrl: String(issue.url || ''), author: issue.author
935
+ }))
936
+ }));
943
937
  return {
944
938
  ok: true,
945
939
  value: {
946
- history: commitOids.map((sha) => ({ sha, message: '', authoredAt: toTime, author: actors.find((actor) => actor.id) || null })),
940
+ history: collected.history,
941
+ commits: collected.commits,
947
942
  mergedPullRequests,
948
- closingIssues,
949
- actors
950
943
  }
951
944
  };
952
945
  }
@@ -1116,7 +1109,7 @@ function createGitHubProvider(
1116
1109
  ): PlatformProvider {
1117
1110
  return {
1118
1111
  type: input.providerType,
1119
- contractVersion: 1,
1112
+ contractVersion: 2,
1120
1113
  identity: { issue: 'number', 'pull-request': 'number', comment: 'number', release: 'key' },
1121
1114
  ...createGitHubOperations(client),
1122
1115
  context: {
@@ -9,7 +9,6 @@ type GitHubActorInput = {
9
9
 
10
10
  type ReleaseNoteActor = {
11
11
  name: string;
12
- email: string | null;
13
12
  login: string | null;
14
13
  bot: boolean;
15
14
  resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
@@ -27,13 +26,23 @@ function normalizeGitHubActor(actor: GitHubActorInput): ReleaseNoteActor {
27
26
  const login = platformLogin || noReplyLogin;
28
27
  return {
29
28
  name,
30
- email,
31
29
  login,
32
30
  bot: Boolean(login?.endsWith('[bot]')),
33
31
  resolution: platformLogin ? 'platform-user' : noReplyLogin ? 'platform-noreply' : 'unresolved'
34
32
  };
35
33
  }
36
34
 
35
+ function normalizePlatformAuthor(author: { login?: string | null } | null | undefined): ReleaseNoteActor | null {
36
+ if (!author) return null;
37
+ const login = author.login?.trim().toLowerCase() || null;
38
+ return {
39
+ name: login || '',
40
+ login,
41
+ bot: Boolean(login?.endsWith('[bot]')),
42
+ resolution: login ? 'platform-user' : 'unresolved'
43
+ };
44
+ }
45
+
37
46
  function failure(error: { code: string; message: string; retryable: boolean }) {
38
47
  return {
39
48
  status: error.retryable ? 'blocked' as const : 'failed' as const,
@@ -89,7 +98,7 @@ function fetchGitHubReleaseNoteData(
89
98
  { cwd: options.cwd }
90
99
  );
91
100
  if (!releases.ok) return failure(releases.error);
92
- const history = [];
101
+ const history: Array<{ tag: string; body: string; url: string | null }> = [];
93
102
  for (const item of releases.value.filter((entry) => !entry.isDraft && !entry.isPrerelease).slice(0, input.historyLimit)) {
94
103
  const viewed = client.json<{ body?: string; url?: string }>(
95
104
  ['release', 'view', String(item.tagName), '--repo', input.repository, '--json', 'body,url'],
@@ -99,33 +108,99 @@ function fetchGitHubReleaseNoteData(
99
108
  history.push({ tag: String(item.tagName), body: String(viewed.value.body || ''), url: viewed.value.url || null });
100
109
  }
101
110
  const prs = client.json<Array<Record<string, unknown>>>(
102
- ['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author,closingIssuesReferences'],
111
+ ['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author'],
103
112
  { cwd: options.cwd }
104
113
  );
105
114
  if (!prs.ok) return failure(prs.error);
106
115
  const authors = new Map<string, ReleaseNoteActor[]>();
116
+ const fromTime = Date.parse(input.fromTime);
117
+ const toTime = Date.parse(input.toTime);
118
+ const pullRequests = prs.value.filter((item) => {
119
+ const mergedAt = Date.parse(String(item.mergedAt || ''));
120
+ return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
121
+ });
122
+ const eligiblePullRequestNumbers = new Set(pullRequests.map((item) => Number(item.number)));
123
+ const pullRequestNumbersByCommit = new Map<string, number[]>();
107
124
  for (const oid of input.commitOids) {
108
- const query = `query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{authors(first:100){nodes{name email user{login}} pageInfo{hasNextPage}}}}}}`;
125
+ const query = `query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{authors(first:100){nodes{name email user{login}} pageInfo{hasNextPage}} associatedPullRequests(first:100){nodes{number} pageInfo{hasNextPage}}}}}}`;
109
126
  const [owner, name] = input.repository.split('/');
110
127
  const result = client.json<{
111
- data?: { repository?: { object?: { authors?: { nodes?: GitHubActorInput[]; pageInfo?: { hasNextPage?: boolean } } } } };
128
+ data?: { repository?: { object?: {
129
+ authors?: { nodes?: GitHubActorInput[]; pageInfo?: { hasNextPage?: boolean } };
130
+ associatedPullRequests?: {
131
+ nodes?: Array<{ number?: number | null }>;
132
+ pageInfo?: { hasNextPage?: boolean };
133
+ };
134
+ } } };
112
135
  }>([
113
136
  'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `oid=${oid}`
114
137
  ], { cwd: options.cwd });
115
138
  if (!result.ok) return failure(result.error);
116
- const connection = result.value.data?.repository?.object?.authors;
139
+ const object = result.value.data?.repository?.object;
140
+ const connection = object?.authors;
117
141
  if (connection?.pageInfo?.hasNextPage) {
118
142
  return failure({ code: 'RELEASE_NOTES_AUTHORS_TRUNCATED', message: `Commit authors exceeded the supported page size for ${oid}`, retryable: false });
119
143
  }
120
144
  authors.set(oid, (connection?.nodes || []).map(normalizeGitHubActor));
145
+ const associatedPullRequests = object?.associatedPullRequests;
146
+ if (!associatedPullRequests || !Array.isArray(associatedPullRequests.nodes) || !associatedPullRequests.pageInfo || typeof associatedPullRequests.pageInfo.hasNextPage !== 'boolean') {
147
+ return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request data is incomplete for commit ${oid}`, retryable: false });
148
+ }
149
+ if (associatedPullRequests.pageInfo.hasNextPage) {
150
+ return failure({ code: 'RELEASE_NOTES_ASSOCIATED_PULL_REQUESTS_TRUNCATED', message: `Associated pull requests exceeded the supported page size for ${oid}`, retryable: false });
151
+ }
152
+ const numbers: number[] = [];
153
+ for (const association of associatedPullRequests.nodes) {
154
+ if (typeof association?.number !== 'number' || !Number.isSafeInteger(association.number)) {
155
+ return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request identity is incomplete for commit ${oid}`, retryable: false });
156
+ }
157
+ if (eligiblePullRequestNumbers.has(association.number)) numbers.push(association.number);
158
+ }
159
+ pullRequestNumbersByCommit.set(oid, [...new Set(numbers)]);
121
160
  }
122
- const fromTime = Date.parse(input.fromTime);
123
- const toTime = Date.parse(input.toTime);
124
- const pullRequests = prs.value.filter((item) => {
125
- const mergedAt = Date.parse(String(item.mergedAt || ''));
126
- return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
127
- });
128
- return { status: 'no-op' as const, changed: false, history, pullRequests, authors, error: null };
161
+ const resolvedPullRequests = [];
162
+ const [owner, name] = input.repository.split('/');
163
+ const closingIssuesQuery = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number title url author{login}} pageInfo{hasNextPage endCursor}}}}}';
164
+ for (const item of pullRequests) {
165
+ const closingIssues = [];
166
+ let cursor: string | null = null;
167
+ for (;;) {
168
+ const args = ['api', 'graphql', '-f', `query=${closingIssuesQuery}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${String(item.number)}`];
169
+ if (cursor) args.push('-F', `cursor=${cursor}`);
170
+ const response = client.json<{
171
+ data?: { repository?: { pullRequest?: { closingIssuesReferences?: {
172
+ nodes?: Array<{ number?: number; title?: string; url?: string; author?: { login?: string | null } | null }>;
173
+ pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
174
+ } } } };
175
+ }>(args, { cwd: options.cwd });
176
+ if (!response.ok) return failure(response.error);
177
+ const connection = response.value.data?.repository?.pullRequest?.closingIssuesReferences;
178
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo || typeof connection.pageInfo.hasNextPage !== 'boolean') {
179
+ return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Closing Issue data is incomplete for pull request ${String(item.number)}`, retryable: false });
180
+ }
181
+ closingIssues.push(...connection.nodes.map((issue) => ({
182
+ number: Number(issue.number), title: String(issue.title || ''), url: String(issue.url || ''),
183
+ author: normalizePlatformAuthor(issue.author)
184
+ })));
185
+ if (!connection.pageInfo.hasNextPage) break;
186
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) {
187
+ return failure({ code: 'PAGINATION_INVALID', message: `Closing Issue pagination is incomplete for pull request ${String(item.number)}`, retryable: false });
188
+ }
189
+ cursor = connection.pageInfo.endCursor;
190
+ }
191
+ resolvedPullRequests.push({
192
+ ...item,
193
+ author: normalizePlatformAuthor((item.author as { login?: string | null } | null) || null),
194
+ closingIssues
195
+ });
196
+ }
197
+ const commits = input.commitOids.map((sha) => ({
198
+ sha,
199
+ url: `https://github.com/${input.repository}/commit/${sha}`,
200
+ pullRequestNumbers: pullRequestNumbersByCommit.get(sha) || [],
201
+ authors: authors.get(sha) || []
202
+ }));
203
+ return { status: 'no-op' as const, changed: false, history, pullRequests: resolvedPullRequests, commits, error: null };
129
204
  }
130
205
 
131
206
  export { fetchGitHubReleaseNoteData, normalizeGitHubActor, publishGitHubReleaseNotes };
@@ -8,7 +8,7 @@ import type {
8
8
  function createNoneProvider(input: PlatformProviderFactoryInput): PlatformProvider {
9
9
  return {
10
10
  type: input.providerType,
11
- contractVersion: 1,
11
+ contractVersion: 2,
12
12
  context: {
13
13
  async resolve(): Promise<ProviderResult<PlatformContextSnapshot>> {
14
14
  return {
@@ -3,7 +3,6 @@ import path from 'node:path';
3
3
 
4
4
  import { enumerateArtifacts } from '../task/artifacts.ts';
5
5
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
6
- import { renderHumanOverrideAudit } from '../task/human-override.ts';
7
6
  import { resolveTaskRef } from '../task/resolve-ref.ts';
8
7
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
9
8
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
@@ -11,7 +10,7 @@ import type { PrDeliveryFact } from '../task/pr-delivery-fact.ts';
11
10
  import { resolvePlatformProviderContext } from './context.ts';
12
11
  import type { LoadedContext, PlatformClient } from './context.ts';
13
12
  import { normalizeCommentContent } from './issue-comments.ts';
14
- import { CONTROL_MARKER_PATTERN, escapeHtmlText, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
13
+ import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
15
14
  import { platformResult } from './types.ts';
16
15
  import type { PlatformResult } from './types.ts';
17
16
  import type { OperationWarning } from '../task/operation-outcome.ts';
@@ -74,25 +73,6 @@ function summaryMarker(taskId: string): string {
74
73
  return `<!-- sync-pr:${taskId}:summary -->`;
75
74
  }
76
75
 
77
- function escapeSummaryAuditText(value: string): string {
78
- return escapeHtmlText(value).replace(/\r\n?/g, '\n');
79
- }
80
-
81
- function renderSummaryAuditLine(value: string): string {
82
- const safe = escapeSummaryAuditText(value);
83
- const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
84
- const delimiter = '`'.repeat(longestRun + 1);
85
- return `${delimiter} ${safe} ${delimiter}`;
86
- }
87
-
88
- function renderSafeHumanOverrideAudit(value: string): string {
89
- const normalized = value.trim().replace(/\r\n?/g, '\n');
90
- if (!normalized) return '';
91
- const [heading, ...rows] = normalized.split('\n');
92
- const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
93
- return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
94
- }
95
-
96
76
  function isSafeSummaryEnvelope(value: string, taskId: string): boolean {
97
77
  return value.includes(summaryMarker(taskId))
98
78
  && (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
@@ -136,9 +116,8 @@ function platformError(error: { code: string; message: string; retryable?: boole
136
116
  return { code: error.code, message: error.message, retryable: error.retryable ?? false };
137
117
  }
138
118
 
139
- function buildPullRequestSummary(taskId: string, body: string, headSha: string, humanOverrideAudit = ''): string {
140
- const sections = [body.replace(/\s+$/, ''), renderSafeHumanOverrideAudit(humanOverrideAudit)].filter(Boolean).join('\n\n');
141
- return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
119
+ function buildPullRequestSummary(taskId: string, body: string, headSha: string): string {
120
+ return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
142
121
  }
143
122
 
144
123
  function reconcileSummaryComment(comments: SummaryComment[], taskId: string, desired: string):
@@ -643,7 +622,7 @@ async function syncPullRequestSummary(
643
622
  `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
644
623
  )
645
624
  : replaced.value;
646
- const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
625
+ const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
647
626
  if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
648
627
  code: 'PR_SUMMARY_RENDER_INVALID',
649
628
  message: 'Summary contains an invalid or duplicated reserved control marker',
@@ -221,16 +221,39 @@ type RepositoryMetadataSnapshot = {
221
221
  }>;
222
222
  };
223
223
 
224
+ type ReleaseNoteAuthor = {
225
+ name: string;
226
+ login: string | null;
227
+ bot: boolean;
228
+ resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
229
+ };
230
+
231
+ type ReleaseNoteIssue = {
232
+ id: string;
233
+ identity: ResourceIdentity;
234
+ number: number;
235
+ title: string;
236
+ displayUrl: string;
237
+ author: ReleaseNoteAuthor | null;
238
+ };
239
+
240
+ type ReleaseNotePullRequest = {
241
+ id: string;
242
+ identity: ResourceIdentity;
243
+ number: number;
244
+ title: string;
245
+ body: string;
246
+ mergedAt: string;
247
+ displayUrl: string;
248
+ labels: string[];
249
+ author: ReleaseNoteAuthor | null;
250
+ closingIssues: ReleaseNoteIssue[];
251
+ };
252
+
224
253
  type ReleaseNotesFacts = {
225
- history: Array<{
226
- sha: string;
227
- message: string;
228
- authoredAt: string;
229
- author: { id?: string; name?: string } | null;
230
- }>;
231
- mergedPullRequests: Array<ChangeRequestSnapshot>;
232
- closingIssues: Array<IssueSnapshot>;
233
- actors: Array<{ id?: string; name?: string }>;
254
+ history: Array<{ tag: string; body: string; url: string | null }>;
255
+ commits: Array<{ sha: string; url: string | null; pullRequestNumbers: number[]; authors: ReleaseNoteAuthor[] }>;
256
+ mergedPullRequests: ReleaseNotePullRequest[];
234
257
  };
235
258
 
236
259
  type MilestoneReconciliation = {
@@ -249,14 +272,14 @@ type VerificationRemoteFacts = {
249
272
 
250
273
  type PlatformProviderFactoryInput = {
251
274
  providerType: string;
252
- contractVersion: 1;
275
+ contractVersion: 2;
253
276
  repositoryRoot: string;
254
277
  config: Readonly<Record<string, JsonValue>>;
255
278
  };
256
279
 
257
280
  type PlatformProvider = {
258
281
  type: string;
259
- contractVersion: 1;
282
+ contractVersion: 2;
260
283
  identity?: ProviderIdentityDeclaration;
261
284
  context: {
262
285
  resolve(input: {
@@ -466,7 +489,7 @@ type PlatformProviderFactory = (
466
489
  input: PlatformProviderFactoryInput
467
490
  ) => Promise<PlatformProvider>;
468
491
 
469
- const PLATFORM_PROVIDER_CONTRACT_VERSION = 1 as const;
492
+ const PLATFORM_PROVIDER_CONTRACT_VERSION = 2 as const;
470
493
 
471
494
  function isRecord(value: unknown): value is Record<string, unknown> {
472
495
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -605,6 +628,9 @@ export type {
605
628
  RequiredCheckSnapshot,
606
629
  RepositoryMetadataSnapshot,
607
630
  ReleaseNotesFacts,
631
+ ReleaseNoteAuthor,
632
+ ReleaseNoteIssue,
633
+ ReleaseNotePullRequest,
608
634
  ResourceIdentity,
609
635
  SecurityAlertKind,
610
636
  SecurityAlertSnapshot,
@@ -16,6 +16,8 @@ import type {
16
16
  PlatformError,
17
17
  PlatformProvider,
18
18
  ProviderResult,
19
+ ReleaseNoteAuthor,
20
+ ReleaseNoteIssue,
19
21
  ReleaseNotesFacts,
20
22
  ReleaseSnapshot,
21
23
  RemoteCommentSnapshot,
@@ -109,6 +111,15 @@ function arrayValue(value: unknown, label: string): unknown[] {
109
111
  return value;
110
112
  }
111
113
 
114
+ function positiveNumberValue(value: unknown, label: string): number {
115
+ if (!Number.isSafeInteger(value) || Number(value) <= 0) throw new Error(`${label} must be a positive integer`);
116
+ return Number(value);
117
+ }
118
+
119
+ function stringArray(value: unknown, label: string): string[] {
120
+ return arrayValue(value, label).map((entry, index) => stringValue(entry, `${label}[${index}]`));
121
+ }
122
+
112
123
  function stableUnique<T>(values: T[], key: (value: T) => string, label: string): T[] {
113
124
  const seen = new Set<string>();
114
125
  for (const value of values) {
@@ -119,6 +130,16 @@ function stableUnique<T>(values: T[], key: (value: T) => string, label: string):
119
130
  return [...values].sort((left, right) => key(left).localeCompare(key(right)));
120
131
  }
121
132
 
133
+ function uniqueInOrder<T>(values: T[], key: (value: T) => string, label: string): T[] {
134
+ const seen = new Set<string>();
135
+ for (const value of values) {
136
+ const identityKey = key(value);
137
+ if (seen.has(identityKey)) throw new Error(`${label} contains duplicate identities`);
138
+ seen.add(identityKey);
139
+ }
140
+ return values;
141
+ }
142
+
122
143
  function declaredIdentityKind(
123
144
  declaration: ProviderIdentityDeclaration | undefined,
124
145
  resourceKind: 'issue' | 'pull-request' | 'comment' | 'release'
@@ -389,20 +410,70 @@ function validateMetadata(value: unknown): RepositoryMetadataSnapshot {
389
410
 
390
411
  function validateReleaseNotes(value: unknown, declaration?: ProviderIdentityDeclaration): ReleaseNotesFacts {
391
412
  const item = record(value, 'release notes');
392
- exactKeys(item, ['history', 'mergedPullRequests', 'closingIssues', 'actors'], 'release notes');
413
+ exactKeys(item, ['history', 'commits', 'mergedPullRequests'], 'release notes');
414
+ const releaseAuthor = (raw: unknown, label: string): ReleaseNoteAuthor | null => {
415
+ if (raw === null) return null;
416
+ const entry = record(raw, label);
417
+ exactKeys(entry, ['name', 'login', 'bot', 'resolution'], label);
418
+ if (typeof entry.bot !== 'boolean') throw new Error(`${label}.bot must be boolean`);
419
+ if (!['platform-user', 'platform-noreply', 'unresolved'].includes(String(entry.resolution))) {
420
+ throw new Error(`${label}.resolution is invalid`);
421
+ }
422
+ return {
423
+ name: stringValue(entry.name, `${label}.name`, true),
424
+ login: nullableString(entry.login, `${label}.login`),
425
+ bot: entry.bot,
426
+ resolution: entry.resolution as ReleaseNoteAuthor['resolution']
427
+ };
428
+ };
429
+ const issue = (raw: unknown, label: string): ReleaseNoteIssue => {
430
+ const entry = record(raw, label);
431
+ exactKeys(entry, ['id', 'identity', 'number', 'title', 'displayUrl', 'author'], label);
432
+ return {
433
+ id: stringValue(entry.id, `${label}.id`),
434
+ identity: identity(entry.identity, `${label}.identity`, declaredIdentityKind(declaration, 'issue')),
435
+ number: positiveNumberValue(entry.number, `${label}.number`),
436
+ title: stringValue(entry.title, `${label}.title`, true),
437
+ displayUrl: stringValue(entry.displayUrl, `${label}.displayUrl`, true),
438
+ author: releaseAuthor(entry.author, `${label}.author`)
439
+ };
440
+ };
393
441
  return {
394
- history: stableUnique(arrayValue(item.history, 'release notes.history').map((raw) => {
442
+ history: uniqueInOrder(arrayValue(item.history, 'release notes.history').map((raw) => {
395
443
  const entry = record(raw, 'release notes.history[]');
396
- exactKeys(entry, ['sha', 'message', 'authoredAt', 'author'], 'release notes.history[]');
397
- return { sha: stringValue(entry.sha, 'history.sha'), message: stringValue(entry.message, 'history.message', true), authoredAt: utcTimestamp(entry.authoredAt, 'history.authoredAt'), author: author(entry.author, 'history.author') };
398
- }), (entry) => entry.sha, 'release notes.history'),
399
- mergedPullRequests: stableUnique(arrayValue(item.mergedPullRequests, 'release notes.mergedPullRequests').map((entry) => validateChangeRequest(entry, declaredIdentityKind(declaration, 'pull-request'), true)), (entry) => serializeResourceIdentity(entry.identity!), 'release notes.mergedPullRequests'),
400
- closingIssues: stableUnique(arrayValue(item.closingIssues, 'release notes.closingIssues').map((entry) => validateIssue(entry, declaredIdentityKind(declaration, 'issue'), true)), (entry) => serializeResourceIdentity(entry.identity!), 'release notes.closingIssues'),
401
- actors: stableUnique(arrayValue(item.actors, 'release notes.actors').map((entry) => {
402
- const actor = author(entry, 'release notes.actor');
403
- if (!actor) throw new Error('release notes.actor must identify an actor');
404
- return actor;
405
- }), (entry) => entry.id || entry.name || '', 'release notes.actors')
444
+ exactKeys(entry, ['tag', 'body', 'url'], 'release notes.history[]');
445
+ return { tag: stringValue(entry.tag, 'history.tag'), body: stringValue(entry.body, 'history.body', true), url: nullableString(entry.url, 'history.url') };
446
+ }), (entry) => entry.tag, 'release notes.history'),
447
+ commits: stableUnique(arrayValue(item.commits, 'release notes.commits').map((raw) => {
448
+ const entry = record(raw, 'release notes.commits[]');
449
+ exactKeys(entry, ['sha', 'url', 'pullRequestNumbers', 'authors'], 'release notes.commits[]');
450
+ return {
451
+ sha: stringValue(entry.sha, 'commits.sha'),
452
+ url: nullableString(entry.url, 'commits.url'),
453
+ pullRequestNumbers: stableUnique(arrayValue(entry.pullRequestNumbers, 'commits.pullRequestNumbers').map((number) => positiveNumberValue(number, 'commits.pullRequestNumbers[]')), (number) => String(number), 'commits.pullRequestNumbers'),
454
+ authors: arrayValue(entry.authors, 'commits.authors').map((rawAuthor) => {
455
+ const normalized = releaseAuthor(rawAuthor, 'commits.author');
456
+ if (!normalized) throw new Error('commits.author must identify an author');
457
+ return normalized;
458
+ })
459
+ };
460
+ }), (entry) => entry.sha, 'release notes.commits'),
461
+ mergedPullRequests: stableUnique(arrayValue(item.mergedPullRequests, 'release notes.mergedPullRequests').map((raw) => {
462
+ const entry = record(raw, 'release notes.mergedPullRequests[]');
463
+ exactKeys(entry, ['id', 'identity', 'number', 'title', 'body', 'mergedAt', 'displayUrl', 'labels', 'author', 'closingIssues'], 'release notes.mergedPullRequests[]');
464
+ return {
465
+ id: stringValue(entry.id, 'pullRequest.id'),
466
+ identity: identity(entry.identity, 'pullRequest.identity', declaredIdentityKind(declaration, 'pull-request')),
467
+ number: positiveNumberValue(entry.number, 'pullRequest.number'),
468
+ title: stringValue(entry.title, 'pullRequest.title', true),
469
+ body: stringValue(entry.body, 'pullRequest.body', true),
470
+ mergedAt: utcTimestamp(entry.mergedAt, 'pullRequest.mergedAt'),
471
+ displayUrl: stringValue(entry.displayUrl, 'pullRequest.displayUrl', true),
472
+ labels: stringArray(entry.labels, 'pullRequest.labels'),
473
+ author: releaseAuthor(entry.author, 'pullRequest.author'),
474
+ closingIssues: stableUnique(arrayValue(entry.closingIssues, 'pullRequest.closingIssues').map((entry) => issue(entry, 'pullRequest.closingIssues[]')), (entry) => serializeResourceIdentity(entry.identity), 'pullRequest.closingIssues')
475
+ };
476
+ }), (entry) => serializeResourceIdentity(entry.identity), 'release notes.mergedPullRequests')
406
477
  };
407
478
  }
408
479