@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
@@ -1,13 +1,11 @@
1
1
  import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.js';
2
- import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
3
2
  import { resolveTaskRef } from '../task/resolve-ref.js';
4
3
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
5
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
5
  const USAGE = `Usage: agent-infra-internal task-warning <task-ref> <add|set-status|list> [intent flags] [--dry-run]\n`;
7
6
  const FLAGS = {
8
7
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
9
- '--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution',
10
- '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
8
+ '--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
11
9
  };
12
10
  function usageFailure(message) {
13
11
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'WARNING_PAYLOAD_INVALID', message } })}\n`);
@@ -66,9 +64,6 @@ async function taskWarning(args = []) {
66
64
  };
67
65
  const optional = kind === 'list' ? ['status'] : [];
68
66
  const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind], ...optional]);
69
- allowed.add('overrideTicket');
70
- allowed.add('overrideTarget');
71
- allowed.add('overrideScope');
72
67
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
73
68
  const missing = required[kind].find((key) => values[key] === undefined);
74
69
  if (unexpected) {
@@ -79,43 +74,14 @@ async function taskWarning(args = []) {
79
74
  usageFailure(`${kind} requires '${missing}'`);
80
75
  return;
81
76
  }
82
- const dryRunConflict = overrideDryRunConflict(values);
83
- if (dryRunConflict) {
84
- usageFailure(dryRunConflict.message);
85
- return;
86
- }
87
77
  const resolved = resolveTaskRef(taskRef);
88
78
  if (!resolved.ok) {
89
79
  usageFailure(resolved.message);
90
80
  return;
91
81
  }
92
82
  let result;
93
- let humanOverride = null;
94
83
  try {
95
- result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => {
96
- let current = applyWorkflowWarningIntent(values);
97
- if (current.status !== 'failed' || !values.overrideTicket)
98
- return current;
99
- if (!values.overrideTarget || !values.overrideScope) {
100
- usageFailure('override ticket requires target and scope');
101
- return current;
102
- }
103
- const consumed = await consumeHumanOverride({
104
- taskRef, ticketId: String(values.overrideTicket),
105
- failureId: failureId('workflow-warning', current.error?.code ?? 'TASK_STATE_MISMATCH'),
106
- target: String(values.overrideTarget), scope: String(values.overrideScope)
107
- }, {
108
- effectExecutor: (capability) => {
109
- const retried = applyWorkflowWarningIntent(values, { manualOverride: capability });
110
- current = retried;
111
- return retried.status === 'failed' || retried.status === 'planned'
112
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no workflow warning effect was committed' : `${retried.error?.code ?? 'WARNING_FAILED'}: ${retried.error?.message ?? 'manual warning effect failed'}` }
113
- : null;
114
- }
115
- });
116
- humanOverride = consumed;
117
- return current;
118
- });
84
+ result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => applyWorkflowWarningIntent(values));
119
85
  }
120
86
  catch (error) {
121
87
  if (!(error instanceof TaskExecutionLockError))
@@ -124,10 +90,8 @@ async function taskWarning(args = []) {
124
90
  process.exitCode = 1;
125
91
  return;
126
92
  }
127
- process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
128
- const overrideFailed = Boolean(humanOverride && typeof humanOverride === 'object' &&
129
- humanOverride.status === 'failed');
130
- if (result.status === 'failed' || overrideFailed)
93
+ process.stdout.write(`${JSON.stringify(result)}\n`);
94
+ if (result.status === 'failed')
131
95
  process.exitCode = 1;
132
96
  }
133
97
  export { taskWarning };
package/dist/lib/merge.js 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.js';
4
5
  import { validateCurrentTaskContract } from './task/current-contract.js';
5
6
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
@@ -44,7 +45,7 @@ function extractTitle(content) {
44
45
  .replace(/\|/g, '\\|') || null;
45
46
  }
46
47
  function normalizeTaskRecord(taskDir, taskFile, dateParts) {
47
- const taskId = path.basename(taskDir);
48
+ const taskId = path.basename(taskDir) === 'local' ? path.basename(path.dirname(taskDir)) : path.basename(taskDir);
48
49
  const content = fs.readFileSync(taskFile, 'utf8');
49
50
  const completedAt = extractField(content, 'completed_at');
50
51
  const updatedAt = extractField(content, 'updated_at');
@@ -85,7 +86,7 @@ function scanSourceTasks(sourceDir) {
85
86
  if (!taskEntry.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
86
87
  continue;
87
88
  }
88
- const taskDir = path.join(dayDir, taskEntry.name);
89
+ const taskDir = path.join(dayDir, taskEntry.name, 'local');
89
90
  const taskFile = path.join(taskDir, 'task.md');
90
91
  if (!fs.existsSync(taskFile)) {
91
92
  continue;
@@ -165,7 +166,7 @@ function collectArchiveEntries(archiveDir) {
165
166
  continue;
166
167
  }
167
168
  const taskDir = path.join(dayDir, taskEntry.name);
168
- const taskFile = path.join(taskDir, 'task.md');
169
+ const taskFile = path.join(taskDir, 'local', 'task.md');
169
170
  const relativePath = `${yearEntry.name}/${monthEntry.name}/${dayEntry.name}/${taskEntry.name}/`;
170
171
  let title = taskEntry.name;
171
172
  let type = 'unknown';
@@ -471,9 +472,10 @@ function validateSourceRoot(sourceRoot) {
471
472
  invalidSource('source path must be a real directory');
472
473
  }
473
474
  }
474
- function validateTaskDirectory(sourceRoot, taskDir, seenTaskIds) {
475
+ function validateTaskDirectory(sourceRoot, taskDir, seenTaskIds, archived = false) {
475
476
  const taskId = path.basename(taskDir);
476
- const taskFile = path.join(taskDir, 'task.md');
477
+ const contentDir = archived ? path.join(taskDir, 'local') : taskDir;
478
+ const taskFile = path.join(contentDir, 'task.md');
477
479
  const taskDirStat = sourcePathStat(sourceRoot, taskDir);
478
480
  if (!taskDirStat || !taskDirStat.isDirectory()) {
479
481
  invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} must be a directory`);
@@ -491,12 +493,63 @@ function validateTaskDirectory(sourceRoot, taskDir, seenTaskIds) {
491
493
  invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} id '${String(contract.metadata.id ?? 'missing')}' does not match directory '${taskId}'`);
492
494
  }
493
495
  validateTaskTree(sourceRoot, taskDir);
496
+ if (archived) {
497
+ for (const entry of fs.readdirSync(taskDir, { withFileTypes: true })) {
498
+ if (!['local', 'github', 'derived'].includes(entry.name) || !entry.isDirectory()) {
499
+ invalidSource(`${sourceRelativePath(sourceRoot, path.join(taskDir, entry.name))} is not an allowed archive source directory`);
500
+ }
501
+ }
502
+ validateArchiveSource(sourceRoot, contentDir);
503
+ const githubDir = path.join(taskDir, 'github');
504
+ if (fs.existsSync(githubDir)) {
505
+ for (const capture of fs.readdirSync(githubDir, { withFileTypes: true })) {
506
+ const captureDir = path.join(githubDir, capture.name);
507
+ if (!capture.isDirectory() || !/^capture-[A-Za-z0-9._-]+$/.test(capture.name) || !fs.existsSync(path.join(captureDir, 'capture.md'))) {
508
+ invalidSource(`${sourceRelativePath(sourceRoot, captureDir)} is not a valid GitHub capture`);
509
+ }
510
+ validateArchiveSource(sourceRoot, captureDir);
511
+ }
512
+ }
513
+ const derivedDir = path.join(taskDir, 'derived');
514
+ if (fs.existsSync(derivedDir)) {
515
+ for (const view of fs.readdirSync(derivedDir, { withFileTypes: true })) {
516
+ const viewDir = path.join(derivedDir, view.name);
517
+ if (!view.isDirectory() || !fs.existsSync(path.join(viewDir, 'inputs.md'))) {
518
+ invalidSource(`${sourceRelativePath(sourceRoot, viewDir)} requires inputs.md`);
519
+ }
520
+ }
521
+ }
522
+ }
494
523
  const previousPath = seenTaskIds.get(taskId);
495
524
  if (previousPath) {
496
525
  invalidSource(`duplicate task ID ${taskId} at ${previousPath} and ${sourceRelativePath(sourceRoot, taskDir)}`);
497
526
  }
498
527
  seenTaskIds.set(taskId, sourceRelativePath(sourceRoot, taskDir));
499
528
  }
529
+ function validateArchiveSource(sourceRoot, sourceDir) {
530
+ const hashFile = path.join(sourceDir, 'contents.sha256');
531
+ const hashStat = sourcePathStat(sourceRoot, hashFile);
532
+ if (!hashStat?.isFile())
533
+ invalidSource(`${sourceRelativePath(sourceRoot, sourceDir)} requires contents.sha256`);
534
+ const files = [];
535
+ const walk = (dir) => {
536
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
537
+ const target = path.join(dir, entry.name);
538
+ if (entry.name === 'manifest.md')
539
+ invalidSource(`${sourceRelativePath(sourceRoot, target)} is forbidden inside a TASK`);
540
+ if (entry.isDirectory())
541
+ walk(target);
542
+ else if (entry.isFile() && target !== hashFile)
543
+ files.push(target);
544
+ }
545
+ };
546
+ walk(sourceDir);
547
+ 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('/'))))
548
+ .map((file) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${path.relative(sourceDir, file).split(path.sep).join('/')}`);
549
+ const actual = fs.readFileSync(hashFile, 'utf8').trimEnd().split(/\r?\n/).filter(Boolean);
550
+ if (JSON.stringify(expected) !== JSON.stringify(actual))
551
+ invalidSource(`${sourceRelativePath(sourceRoot, hashFile)} does not match local files`);
552
+ }
500
553
  function validateMutableSection(sourceRoot, section, seenTaskIds) {
501
554
  const sectionDir = path.join(sourceRoot, section);
502
555
  const sectionStat = sourcePathStat(sourceRoot, sectionDir);
@@ -561,9 +614,6 @@ function validateArchiveSection(sourceRoot, seenTaskIds) {
561
614
  const monthDir = monthPath;
562
615
  for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
563
616
  const dayPath = path.join(monthDir, dayEntry.name);
564
- if (allowManifest(monthDir, dayEntry)) {
565
- continue;
566
- }
567
617
  const dayStat = sourcePathStat(sourceRoot, dayPath);
568
618
  if (!dayStat?.isDirectory() || !/^\d{2}$/.test(dayEntry.name)) {
569
619
  invalidSource(`${sourceRelativePath(sourceRoot, dayPath)} is not a DD directory or manifest.md`);
@@ -575,7 +625,7 @@ function validateArchiveSection(sourceRoot, seenTaskIds) {
575
625
  if (!taskStat?.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
576
626
  invalidSource(`${sourceRelativePath(sourceRoot, taskPath)} is not a TASK-YYYYMMDD-HHMMSS directory`);
577
627
  }
578
- validateTaskDirectory(sourceRoot, taskPath, seenTaskIds);
628
+ validateTaskDirectory(sourceRoot, taskPath, seenTaskIds, true);
579
629
  }
580
630
  }
581
631
  }
@@ -716,9 +766,10 @@ function mergeArchiveSection(sourceArchive, localArchive, report) {
716
766
  });
717
767
  continue;
718
768
  }
769
+ const sourceTaskDir = path.dirname(task.taskDir);
719
770
  const destinationDir = path.join(localArchive, task.relativePath);
720
771
  fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
721
- fs.cpSync(task.taskDir, destinationDir, { recursive: true });
772
+ fs.cpSync(sourceTaskDir, destinationDir, { recursive: true });
722
773
  recordArchive(report, 'copied', {
723
774
  action: 'copied',
724
775
  symbol: '✓',
@@ -829,6 +880,7 @@ async function cmdMerge(args) {
829
880
  const report = createReport(resolvedSource, backupRootRelative);
830
881
  detectSourceMode(resolvedSource);
831
882
  validateSourceWorkspace(resolvedSource);
883
+ validateArchiveSection(workspaceDir, new Map());
832
884
  for (const section of ALL_SECTIONS) {
833
885
  fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
834
886
  }
@@ -921,26 +921,21 @@ function createGitHubOperations(client) {
921
921
  }
922
922
  const mergedPullRequests = collected.pullRequests.map((item) => ({
923
923
  id: String(item.number), identity: { kind: 'number', value: Number(item.number) }, number: Number(item.number),
924
- state: 'closed', title: String(item.title || ''), body: String(item.body || ''),
924
+ title: String(item.title || ''), body: String(item.body || ''),
925
925
  mergedAt: new Date(String(item.mergedAt)).toISOString(), displayUrl: String(item.url || ''),
926
926
  labels: Array.isArray(item.labels) ? item.labels.map((label) => String(label.name || label)) : [],
927
- assignees: [], mergeCommitSha: null
927
+ author: item.author,
928
+ closingIssues: item.closingIssues.map((issue) => ({
929
+ id: String(issue.number), identity: { kind: 'number', value: Number(issue.number) }, number: Number(issue.number),
930
+ title: String(issue.title || ''), displayUrl: String(issue.url || ''), author: issue.author
931
+ }))
928
932
  }));
929
- const closingIssues = collected.pullRequests.flatMap((item) => Array.isArray(item.closingIssuesReferences) ? item.closingIssuesReferences.map((issue) => ({
930
- id: String(issue.number), identity: { kind: 'number', value: Number(issue.number) }, number: Number(issue.number),
931
- state: 'closed', title: String(issue.title || ''), body: '', labels: Array.isArray(issue.labels) ? issue.labels.map((label) => String(label.name || label)) : [],
932
- assignees: [], milestone: null, fields: {}, displayUrl: String(issue.url || '')
933
- })) : []);
934
- const actors = [...collected.authors.values()].flatMap((items) => items.map((item) => ({
935
- ...(item.login ? { id: String(item.login) } : {}), ...(item.name ? { name: String(item.name) } : {})
936
- })));
937
933
  return {
938
934
  ok: true,
939
935
  value: {
940
- history: commitOids.map((sha) => ({ sha, message: '', authoredAt: toTime, author: actors.find((actor) => actor.id) || null })),
936
+ history: collected.history,
937
+ commits: collected.commits,
941
938
  mergedPullRequests,
942
- closingIssues,
943
- actors
944
939
  }
945
940
  };
946
941
  }
@@ -1121,7 +1116,7 @@ function createGitHubOperations(client) {
1121
1116
  function createGitHubProvider(input, client = createGitHubClient()) {
1122
1117
  return {
1123
1118
  type: input.providerType,
1124
- contractVersion: 1,
1119
+ contractVersion: 2,
1125
1120
  identity: { issue: 'number', 'pull-request': 'number', comment: 'number', release: 'key' },
1126
1121
  ...createGitHubOperations(client),
1127
1122
  context: {
@@ -9,12 +9,22 @@ function normalizeGitHubActor(actor) {
9
9
  const login = platformLogin || noReplyLogin;
10
10
  return {
11
11
  name,
12
- email,
13
12
  login,
14
13
  bot: Boolean(login?.endsWith('[bot]')),
15
14
  resolution: platformLogin ? 'platform-user' : noReplyLogin ? 'platform-noreply' : 'unresolved'
16
15
  };
17
16
  }
17
+ function normalizePlatformAuthor(author) {
18
+ if (!author)
19
+ return null;
20
+ const login = author.login?.trim().toLowerCase() || null;
21
+ return {
22
+ name: login || '',
23
+ login,
24
+ bot: Boolean(login?.endsWith('[bot]')),
25
+ resolution: login ? 'platform-user' : 'unresolved'
26
+ };
27
+ }
18
28
  function failure(error) {
19
29
  return {
20
30
  status: error.retryable ? 'blocked' : 'failed',
@@ -66,31 +76,90 @@ function fetchGitHubReleaseNoteData(input, options = {}) {
66
76
  return failure(viewed.error);
67
77
  history.push({ tag: String(item.tagName), body: String(viewed.value.body || ''), url: viewed.value.url || null });
68
78
  }
69
- const prs = client.json(['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author,closingIssuesReferences'], { cwd: options.cwd });
79
+ const prs = client.json(['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author'], { cwd: options.cwd });
70
80
  if (!prs.ok)
71
81
  return failure(prs.error);
72
82
  const authors = new Map();
83
+ const fromTime = Date.parse(input.fromTime);
84
+ const toTime = Date.parse(input.toTime);
85
+ const pullRequests = prs.value.filter((item) => {
86
+ const mergedAt = Date.parse(String(item.mergedAt || ''));
87
+ return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
88
+ });
89
+ const eligiblePullRequestNumbers = new Set(pullRequests.map((item) => Number(item.number)));
90
+ const pullRequestNumbersByCommit = new Map();
73
91
  for (const oid of input.commitOids) {
74
- 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}}}}}}`;
92
+ 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}}}}}}`;
75
93
  const [owner, name] = input.repository.split('/');
76
94
  const result = client.json([
77
95
  'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `oid=${oid}`
78
96
  ], { cwd: options.cwd });
79
97
  if (!result.ok)
80
98
  return failure(result.error);
81
- const connection = result.value.data?.repository?.object?.authors;
99
+ const object = result.value.data?.repository?.object;
100
+ const connection = object?.authors;
82
101
  if (connection?.pageInfo?.hasNextPage) {
83
102
  return failure({ code: 'RELEASE_NOTES_AUTHORS_TRUNCATED', message: `Commit authors exceeded the supported page size for ${oid}`, retryable: false });
84
103
  }
85
104
  authors.set(oid, (connection?.nodes || []).map(normalizeGitHubActor));
105
+ const associatedPullRequests = object?.associatedPullRequests;
106
+ if (!associatedPullRequests || !Array.isArray(associatedPullRequests.nodes) || !associatedPullRequests.pageInfo || typeof associatedPullRequests.pageInfo.hasNextPage !== 'boolean') {
107
+ return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request data is incomplete for commit ${oid}`, retryable: false });
108
+ }
109
+ if (associatedPullRequests.pageInfo.hasNextPage) {
110
+ return failure({ code: 'RELEASE_NOTES_ASSOCIATED_PULL_REQUESTS_TRUNCATED', message: `Associated pull requests exceeded the supported page size for ${oid}`, retryable: false });
111
+ }
112
+ const numbers = [];
113
+ for (const association of associatedPullRequests.nodes) {
114
+ if (typeof association?.number !== 'number' || !Number.isSafeInteger(association.number)) {
115
+ return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request identity is incomplete for commit ${oid}`, retryable: false });
116
+ }
117
+ if (eligiblePullRequestNumbers.has(association.number))
118
+ numbers.push(association.number);
119
+ }
120
+ pullRequestNumbersByCommit.set(oid, [...new Set(numbers)]);
86
121
  }
87
- const fromTime = Date.parse(input.fromTime);
88
- const toTime = Date.parse(input.toTime);
89
- const pullRequests = prs.value.filter((item) => {
90
- const mergedAt = Date.parse(String(item.mergedAt || ''));
91
- return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
92
- });
93
- return { status: 'no-op', changed: false, history, pullRequests, authors, error: null };
122
+ const resolvedPullRequests = [];
123
+ const [owner, name] = input.repository.split('/');
124
+ 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}}}}}';
125
+ for (const item of pullRequests) {
126
+ const closingIssues = [];
127
+ let cursor = null;
128
+ for (;;) {
129
+ const args = ['api', 'graphql', '-f', `query=${closingIssuesQuery}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${String(item.number)}`];
130
+ if (cursor)
131
+ args.push('-F', `cursor=${cursor}`);
132
+ const response = client.json(args, { cwd: options.cwd });
133
+ if (!response.ok)
134
+ return failure(response.error);
135
+ const connection = response.value.data?.repository?.pullRequest?.closingIssuesReferences;
136
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo || typeof connection.pageInfo.hasNextPage !== 'boolean') {
137
+ return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Closing Issue data is incomplete for pull request ${String(item.number)}`, retryable: false });
138
+ }
139
+ closingIssues.push(...connection.nodes.map((issue) => ({
140
+ number: Number(issue.number), title: String(issue.title || ''), url: String(issue.url || ''),
141
+ author: normalizePlatformAuthor(issue.author)
142
+ })));
143
+ if (!connection.pageInfo.hasNextPage)
144
+ break;
145
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) {
146
+ return failure({ code: 'PAGINATION_INVALID', message: `Closing Issue pagination is incomplete for pull request ${String(item.number)}`, retryable: false });
147
+ }
148
+ cursor = connection.pageInfo.endCursor;
149
+ }
150
+ resolvedPullRequests.push({
151
+ ...item,
152
+ author: normalizePlatformAuthor(item.author || null),
153
+ closingIssues
154
+ });
155
+ }
156
+ const commits = input.commitOids.map((sha) => ({
157
+ sha,
158
+ url: `https://github.com/${input.repository}/commit/${sha}`,
159
+ pullRequestNumbers: pullRequestNumbersByCommit.get(sha) || [],
160
+ authors: authors.get(sha) || []
161
+ }));
162
+ return { status: 'no-op', changed: false, history, pullRequests: resolvedPullRequests, commits, error: null };
94
163
  }
95
164
  export { fetchGitHubReleaseNoteData, normalizeGitHubActor, publishGitHubReleaseNotes };
96
165
  //# sourceMappingURL=github-release-notes.js.map
@@ -1,7 +1,7 @@
1
1
  function createNoneProvider(input) {
2
2
  return {
3
3
  type: input.providerType,
4
- contractVersion: 1,
4
+ contractVersion: 2,
5
5
  context: {
6
6
  async resolve() {
7
7
  return {
@@ -2,13 +2,12 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { enumerateArtifacts } from '../task/artifacts.js';
4
4
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
5
- import { renderHumanOverrideAudit } from '../task/human-override.js';
6
5
  import { resolveTaskRef } from '../task/resolve-ref.js';
7
6
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
8
7
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.js';
9
8
  import { resolvePlatformProviderContext } from './context.js';
10
9
  import { normalizeCommentContent } from './issue-comments.js';
11
- import { CONTROL_MARKER_PATTERN, escapeHtmlText, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
10
+ import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
12
11
  import { platformResult } from './types.js';
13
12
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
14
13
  import { resourceIdentityEquals } from './resource-identity.js';
@@ -18,23 +17,6 @@ import { readManualValidationCompletion } from '../task/manual-validation-comple
18
17
  function summaryMarker(taskId) {
19
18
  return `<!-- sync-pr:${taskId}:summary -->`;
20
19
  }
21
- function escapeSummaryAuditText(value) {
22
- return escapeHtmlText(value).replace(/\r\n?/g, '\n');
23
- }
24
- function renderSummaryAuditLine(value) {
25
- const safe = escapeSummaryAuditText(value);
26
- const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
27
- const delimiter = '`'.repeat(longestRun + 1);
28
- return `${delimiter} ${safe} ${delimiter}`;
29
- }
30
- function renderSafeHumanOverrideAudit(value) {
31
- const normalized = value.trim().replace(/\r\n?/g, '\n');
32
- if (!normalized)
33
- return '';
34
- const [heading, ...rows] = normalized.split('\n');
35
- const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
36
- return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
37
- }
38
20
  function isSafeSummaryEnvelope(value, taskId) {
39
21
  return value.includes(summaryMarker(taskId))
40
22
  && (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
@@ -71,9 +53,8 @@ function warningResultForPrimary(primaryResult) {
71
53
  function platformError(error) {
72
54
  return { code: error.code, message: error.message, retryable: error.retryable ?? false };
73
55
  }
74
- function buildPullRequestSummary(taskId, body, headSha, humanOverrideAudit = '') {
75
- const sections = [body.replace(/\s+$/, ''), renderSafeHumanOverrideAudit(humanOverrideAudit)].filter(Boolean).join('\n\n');
76
- return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
56
+ function buildPullRequestSummary(taskId, body, headSha) {
57
+ return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
77
58
  }
78
59
  function reconcileSummaryComment(comments, taskId, desired) {
79
60
  const marker = summaryMarker(taskId);
@@ -586,7 +567,7 @@ async function syncPullRequestSummary(taskRef, options) {
586
567
  const renderedBody = manual?.phase === 'final'
587
568
  ? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`)
588
569
  : replaced.value;
589
- const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
570
+ const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
590
571
  if (!isSafeSummaryEnvelope(desired, resolved.taskId))
591
572
  return fail('failed', context, {
592
573
  code: 'PR_SUMMARY_RENDER_INVALID',
@@ -242,22 +242,45 @@ type RepositoryMetadataSnapshot = {
242
242
  }>;
243
243
  }>;
244
244
  };
245
+ type ReleaseNoteAuthor = {
246
+ name: string;
247
+ login: string | null;
248
+ bot: boolean;
249
+ resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
250
+ };
251
+ type ReleaseNoteIssue = {
252
+ id: string;
253
+ identity: ResourceIdentity;
254
+ number: number;
255
+ title: string;
256
+ displayUrl: string;
257
+ author: ReleaseNoteAuthor | null;
258
+ };
259
+ type ReleaseNotePullRequest = {
260
+ id: string;
261
+ identity: ResourceIdentity;
262
+ number: number;
263
+ title: string;
264
+ body: string;
265
+ mergedAt: string;
266
+ displayUrl: string;
267
+ labels: string[];
268
+ author: ReleaseNoteAuthor | null;
269
+ closingIssues: ReleaseNoteIssue[];
270
+ };
245
271
  type ReleaseNotesFacts = {
246
272
  history: Array<{
247
- sha: string;
248
- message: string;
249
- authoredAt: string;
250
- author: {
251
- id?: string;
252
- name?: string;
253
- } | null;
273
+ tag: string;
274
+ body: string;
275
+ url: string | null;
254
276
  }>;
255
- mergedPullRequests: Array<ChangeRequestSnapshot>;
256
- closingIssues: Array<IssueSnapshot>;
257
- actors: Array<{
258
- id?: string;
259
- name?: string;
277
+ commits: Array<{
278
+ sha: string;
279
+ url: string | null;
280
+ pullRequestNumbers: number[];
281
+ authors: ReleaseNoteAuthor[];
260
282
  }>;
283
+ mergedPullRequests: ReleaseNotePullRequest[];
261
284
  };
262
285
  type MilestoneReconciliation = {
263
286
  changed: boolean;
@@ -276,13 +299,13 @@ type VerificationRemoteFacts = {
276
299
  };
277
300
  type PlatformProviderFactoryInput = {
278
301
  providerType: string;
279
- contractVersion: 1;
302
+ contractVersion: 2;
280
303
  repositoryRoot: string;
281
304
  config: Readonly<Record<string, JsonValue>>;
282
305
  };
283
306
  type PlatformProvider = {
284
307
  type: string;
285
- contractVersion: 1;
308
+ contractVersion: 2;
286
309
  identity?: ProviderIdentityDeclaration;
287
310
  context: {
288
311
  resolve(input: {
@@ -548,8 +571,8 @@ type PlatformProvider = {
548
571
  };
549
572
  };
550
573
  type PlatformProviderFactory = (input: PlatformProviderFactoryInput) => Promise<PlatformProvider>;
551
- declare const PLATFORM_PROVIDER_CONTRACT_VERSION: 1;
574
+ declare const PLATFORM_PROVIDER_CONTRACT_VERSION: 2;
552
575
  declare function isPlatformProviderFactory(value: unknown): value is PlatformProviderFactory;
553
576
  declare function validatePlatformProvider(value: unknown, providerType: string): ProviderResult<PlatformProvider>;
554
577
  export { PLATFORM_PROVIDER_CONTRACT_VERSION, isPlatformProviderFactory, validatePlatformProvider };
555
- export type { ChangeRequestSnapshot, CheckLogSnapshot, CheckRunSnapshot, GitEvidenceSnapshot, IssueSnapshot, JsonValue, MilestoneReconciliation, MilestoneDefinition, MilestoneInitialization, LabelDefinition, LabelReconciliation, MutationIdentity, MutationReceipt, PlatformCapabilities, PlatformContextSnapshot, PlatformError, PlatformProvider, PlatformProviderFactory, PlatformProviderFactoryInput, ProviderOperationContext, ProviderResult, ReleaseSnapshot, RemoteCommentSnapshot, RequiredCheckSnapshot, RepositoryMetadataSnapshot, ReleaseNotesFacts, ResourceIdentity, SecurityAlertKind, SecurityAlertSnapshot, ReviewSnapshot, VerificationRemoteFacts };
578
+ export type { ChangeRequestSnapshot, CheckLogSnapshot, CheckRunSnapshot, GitEvidenceSnapshot, IssueSnapshot, JsonValue, MilestoneReconciliation, MilestoneDefinition, MilestoneInitialization, LabelDefinition, LabelReconciliation, MutationIdentity, MutationReceipt, PlatformCapabilities, PlatformContextSnapshot, PlatformError, PlatformProvider, PlatformProviderFactory, PlatformProviderFactoryInput, ProviderOperationContext, ProviderResult, ReleaseSnapshot, RemoteCommentSnapshot, RequiredCheckSnapshot, RepositoryMetadataSnapshot, ReleaseNotesFacts, ReleaseNoteAuthor, ReleaseNoteIssue, ReleaseNotePullRequest, ResourceIdentity, SecurityAlertKind, SecurityAlertSnapshot, ReviewSnapshot, VerificationRemoteFacts };
@@ -1,4 +1,4 @@
1
- const PLATFORM_PROVIDER_CONTRACT_VERSION = 1;
1
+ const PLATFORM_PROVIDER_CONTRACT_VERSION = 2;
2
2
  function isRecord(value) {
3
3
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4
4
  }