@fitlab-ai/agent-infra 0.11.7 → 0.11.9

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 (128) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +8 -2
  4. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  5. package/dist/lib/agent-clients/config.js +11 -3
  6. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  7. package/dist/lib/agent-clients/next-steps.js +1 -1
  8. package/dist/lib/agent-clients/reconcile.js +3 -3
  9. package/dist/lib/agent-clients/schema.js +18 -10
  10. package/dist/lib/defaults.json +6 -3
  11. package/dist/lib/init.js +2 -2
  12. package/dist/lib/internal/agent-client.js +3 -3
  13. package/dist/lib/internal/cli-route-inventory.js +3 -0
  14. package/dist/lib/internal/platform-pr-review.js +20 -3
  15. package/dist/lib/internal/task-operation-registry.js +3 -1
  16. package/dist/lib/internal/task-platform-recovery.js +56 -0
  17. package/dist/lib/merge.js +25 -5
  18. package/dist/lib/platform/completion-backfill.js +2 -1
  19. package/dist/lib/platform/in-label-sync.js +12 -1
  20. package/dist/lib/platform/issue-comments.js +58 -4
  21. package/dist/lib/platform/issues.js +105 -5
  22. package/dist/lib/platform/operation-coordinator.js +74 -0
  23. package/dist/lib/platform/pr-review.js +107 -1
  24. package/dist/lib/platform/pr-summary.js +79 -57
  25. package/dist/lib/platform/pull-requests.js +96 -4
  26. package/dist/lib/sandbox/commands/create.js +6 -3
  27. package/dist/lib/sandbox/commands/ls.js +4 -3
  28. package/dist/lib/sandbox/commands/rm.js +1 -1
  29. package/dist/lib/sandbox/config.js +22 -4
  30. package/dist/lib/sandbox/git-metadata.js +50 -0
  31. package/dist/lib/sandbox/index.js +1 -1
  32. package/dist/lib/sandbox/mounts.js +12 -0
  33. package/dist/lib/sandbox/recovery.js +12 -22
  34. package/dist/lib/sandbox/removal.js +58 -20
  35. package/dist/lib/sandbox/tools.js +31 -7
  36. package/dist/lib/task/activity-intent.js +4 -3
  37. package/dist/lib/task/create-service.js +5 -3
  38. package/dist/lib/task/create.js +22 -5
  39. package/dist/lib/task/finalization-artifacts.js +4 -1
  40. package/dist/lib/task/finalization.js +243 -113
  41. package/dist/lib/task/lifecycle.js +38 -2
  42. package/dist/lib/task/platform-operation-journal.js +152 -0
  43. package/dist/lib/task/platform-operation-recovery.js +459 -0
  44. package/dist/lib/task/verification.js +0 -1
  45. package/dist/lib/task/workflow-warning-intents.js +7 -1
  46. package/dist/lib/update.js +2 -2
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapters/codex.ts +1 -1
  49. package/lib/agent-clients/config.ts +9 -3
  50. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  51. package/lib/agent-clients/next-steps.ts +3 -3
  52. package/lib/agent-clients/reconcile.ts +3 -3
  53. package/lib/agent-clients/schema.ts +18 -10
  54. package/lib/defaults.json +6 -3
  55. package/lib/init.ts +2 -2
  56. package/lib/internal/agent-client.ts +3 -3
  57. package/lib/internal/cli-route-inventory.ts +2 -0
  58. package/lib/internal/platform-pr-review.ts +14 -3
  59. package/lib/internal/task-operation-registry.ts +3 -1
  60. package/lib/internal/task-platform-recovery.ts +53 -0
  61. package/lib/merge.ts +26 -6
  62. package/lib/platform/completion-backfill.ts +2 -1
  63. package/lib/platform/in-label-sync.ts +12 -0
  64. package/lib/platform/issue-comments.ts +59 -3
  65. package/lib/platform/issues.ts +103 -5
  66. package/lib/platform/operation-coordinator.ts +96 -0
  67. package/lib/platform/pr-review.ts +113 -1
  68. package/lib/platform/pr-summary.ts +22 -1
  69. package/lib/platform/pull-requests.ts +105 -5
  70. package/lib/sandbox/commands/create.ts +7 -8
  71. package/lib/sandbox/commands/ls.ts +4 -3
  72. package/lib/sandbox/commands/rm.ts +1 -1
  73. package/lib/sandbox/config.ts +23 -5
  74. package/lib/sandbox/git-metadata.ts +66 -0
  75. package/lib/sandbox/index.ts +1 -1
  76. package/lib/sandbox/mounts.ts +14 -0
  77. package/lib/sandbox/recovery.ts +13 -25
  78. package/lib/sandbox/removal.ts +57 -19
  79. package/lib/sandbox/tool-types.ts +2 -1
  80. package/lib/sandbox/tools.ts +38 -7
  81. package/lib/task/activity-intent.ts +4 -3
  82. package/lib/task/create-service.ts +5 -3
  83. package/lib/task/create.ts +17 -5
  84. package/lib/task/finalization-artifacts.ts +4 -1
  85. package/lib/task/finalization.ts +242 -106
  86. package/lib/task/lifecycle.ts +35 -3
  87. package/lib/task/platform-operation-journal.ts +230 -0
  88. package/lib/task/platform-operation-recovery.ts +435 -0
  89. package/lib/task/verification.ts +1 -2
  90. package/lib/task/workflow-warning-intents.ts +9 -1
  91. package/lib/update.ts +2 -3
  92. package/package.json +1 -1
  93. package/templates/.agents/README.en.md +20 -66
  94. package/templates/.agents/README.zh-CN.md +19 -65
  95. package/templates/.agents/rules/README.en.md +1 -1
  96. package/templates/.agents/rules/README.zh-CN.md +1 -1
  97. package/templates/.agents/rules/next-step-output.en.md +1 -1
  98. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  99. package/templates/.agents/rules/task-management.en.md +12 -0
  100. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  101. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  102. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
  103. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  106. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  108. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  109. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  110. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  111. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  112. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  113. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  114. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  115. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  116. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  117. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  118. package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
  119. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
  120. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  121. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  122. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  123. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  124. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  125. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  126. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
  127. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  128. package/lib/agent-clients/custom-tuis.ts +0 -149
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { parseTaskFrontmatter } from '../task/frontmatter.js';
5
5
  import { resolveTaskRef } from '../task/resolve-ref.js';
6
+ import { coordinatePlatformWrite } from './operation-coordinator.js';
6
7
  import { resolvePlatformProviderContext } from './context.js';
7
8
  import { platformResult } from './types.js';
8
9
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
@@ -29,6 +30,8 @@ const ARTIFACT_TITLES = {
29
30
  'review-plan': '技术方案审查',
30
31
  code: '实现报告',
31
32
  'review-code': '代码审查',
33
+ 'manual-validation': '人工验证',
34
+ 'validation-run': '验证运行证据',
32
35
  'pr-review': 'PR 审查报告'
33
36
  };
34
37
  function normalizeCommentContent(content) {
@@ -98,7 +101,7 @@ function taskCommentLanguage(repoRoot) {
98
101
  }
99
102
  function artifactIdentity(artifact) {
100
103
  const stem = path.basename(artifact, '.md');
101
- const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
104
+ const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
102
105
  if (!match)
103
106
  throw new Error(`unsupported artifact '${artifact}'`);
104
107
  const base = ARTIFACT_TITLES[match[1]];
@@ -430,7 +433,7 @@ function writeComment(client, repo, issue, cwd, body, id) {
430
433
  const endpoint = id ? `repos/${repo}/issues/comments/${id}` : `repos/${repo}/issues/${issue}/comments`;
431
434
  return client.json(['api', endpoint, '-X', id ? 'PATCH' : 'POST', '--input', '-'], { cwd, method: id ? 'PATCH' : 'POST', input: JSON.stringify({ body }) });
432
435
  }
433
- async function syncPlatformComment(taskRef, options) {
436
+ async function syncPlatformCommentImpl(taskRef, options) {
434
437
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
435
438
  if (!resolved.ok) {
436
439
  return platformResult('failed', {
@@ -452,7 +455,12 @@ async function syncPlatformComment(taskRef, options) {
452
455
  }
453
456
  let desired;
454
457
  try {
455
- desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, resolved.repoRoot, options);
458
+ const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
459
+ if (options.kind === 'task' && options.taskProjection
460
+ && createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
461
+ throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
462
+ }
463
+ desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
456
464
  }
457
465
  catch (error) {
458
466
  return platformResult('failed', {
@@ -644,6 +652,52 @@ async function syncPlatformComment(taskRef, options) {
644
652
  });
645
653
  return result;
646
654
  }
655
+ async function syncPlatformComment(taskRef, options) {
656
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
657
+ if (!resolved.ok)
658
+ return syncPlatformCommentImpl(taskRef, options);
659
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
660
+ if (!taskIssueIdentity(parseTaskFrontmatter(taskContent)))
661
+ return syncPlatformCommentImpl(taskRef, options);
662
+ let operation;
663
+ try {
664
+ const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
665
+ const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
666
+ const content = desired.map((chunk) => chunk.content).join('\0');
667
+ const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
668
+ : options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
669
+ const target = options.kind === 'artifact' ? options.artifact : options.kind;
670
+ operation = { kind, target, expectedDigest: createHash('sha256').update(content).digest('hex') };
671
+ }
672
+ catch {
673
+ return syncPlatformCommentImpl(taskRef, options);
674
+ }
675
+ const execute = () => syncPlatformCommentImpl(taskRef, options);
676
+ if (options.skipQueue)
677
+ return execute();
678
+ return coordinatePlatformWrite({
679
+ operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
680
+ agent: options.agent,
681
+ execute,
682
+ block: (error) => platformResult('blocked', { error }),
683
+ persistenceFailure: (error) => platformResult('failed', { error })
684
+ });
685
+ }
686
+ function inspectPlatformCommentOperation(taskRef, options) {
687
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
688
+ if (!resolved.ok)
689
+ return null;
690
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
691
+ if (!taskIssueIdentity(parseTaskFrontmatter(taskContent)))
692
+ return null;
693
+ const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
694
+ const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
695
+ const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
696
+ : options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
697
+ const target = options.kind === 'artifact' ? options.artifact : options.kind;
698
+ const expectedDigest = createHash('sha256').update(desired.map((chunk) => chunk.content).join('\0')).digest('hex');
699
+ return { kind, target, expectedDigest, id: createHash('sha256').update(`${kind}\0${target}\0${expectedDigest}`).digest('hex') };
700
+ }
647
701
  async function listPlatformComments(issue, cwd = process.cwd(), client) {
648
702
  const loaded = await resolvePlatformProviderContext({ cwd, client });
649
703
  const context = loaded.ok ? loaded.value.context : loaded.context;
@@ -708,5 +762,5 @@ async function checkPlatformCommentOwner(taskRef, options = {}) {
708
762
  }
709
763
  return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
710
764
  }
711
- export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
765
+ export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, inspectPlatformCommentOperation, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
712
766
  //# sourceMappingURL=issue-comments.js.map
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
3
4
  import { execFileSync } from 'node:child_process';
4
5
  import { buildDefaultBody } from '../task/commands/issue-body.js';
5
6
  import { extractTitle, parseTaskFrontmatter } from '../task/frontmatter.js';
@@ -10,10 +11,11 @@ import { writeTask } from '../task/write.js';
10
11
  import { resolvePlatformProviderContext } from './context.js';
11
12
  import { DEFAULT_REQUIREMENT_SECTION_ANCHORS, chooseMilestone, desiredIssueType, normalizeOption, planIssueMetadata } from './issue-metadata.js';
12
13
  import { platformResult } from './types.js';
13
- import { labelDelta, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
14
+ import { labelDelta, inLabelMappingDigest, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
14
15
  import { providerError, providerOperationContext, providerStatus, providerResourceIdentity, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
15
16
  import { resourceIdentityEquals, resourceIdentityNumber, resourceIdentityString, serializeResourceIdentity } from './resource-identity.js';
16
17
  import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
18
+ import { coordinatePlatformWrite } from './operation-coordinator.js';
17
19
  function result(status, taskId, issueNumber, overrides = {}) {
18
20
  return {
19
21
  ...platformResult(status),
@@ -247,7 +249,7 @@ async function createPlatformIssue(taskRef, options) {
247
249
  platform: base.context.platform, capabilities: base.context.capabilities,
248
250
  operations: [{ name: 'issue:create', status: 'planned', reasonCode: null }], error: null
249
251
  });
250
- {
252
+ const execute = async () => {
251
253
  const created = base.provider.issues?.create
252
254
  ? await base.provider.issues.create({
253
255
  context: providerOperationContext(base.loadedContext),
@@ -290,7 +292,7 @@ async function createPlatformIssue(taskRef, options) {
290
292
  return result('failed', base.resolved.taskId, null, {
291
293
  platform: base.context.platform, capabilities: base.context.capabilities,
292
294
  resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
293
- error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: false }
295
+ error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: true }
294
296
  });
295
297
  return result('applied', base.resolved.taskId, issueNumber, {
296
298
  changed: true,
@@ -301,7 +303,26 @@ async function createPlatformIssue(taskRef, options) {
301
303
  issue: normalizeProviderIssue(inspected.value, repository, issueNumber),
302
304
  error: null
303
305
  });
304
- }
306
+ };
307
+ const issueCreate = { title: payload.title, bodyDigest: createHash('sha256').update(payload.body).digest('hex') };
308
+ const operation = {
309
+ kind: 'issue-create',
310
+ target: `task:${base.resolved.taskId}`,
311
+ expectedDigest: createHash('sha256').update(JSON.stringify({
312
+ taskId: base.resolved.taskId,
313
+ title: payload.title,
314
+ body: createHash('sha256').update(payload.body).digest('hex'),
315
+ labels: payload.labels || [], assignees: payload.assignees,
316
+ milestone: typeof payload.milestone === 'string' ? payload.milestone : null
317
+ })).digest('hex')
318
+ };
319
+ return coordinatePlatformWrite({
320
+ operation: { taskRef: base.resolved.taskId, cwd: base.resolved.repoRoot, ...operation, issueCreate, dependency: 'required' },
321
+ agent: options.agent,
322
+ execute,
323
+ block: (error) => result('blocked', base.resolved.taskId, null, { platform: base.context.platform, capabilities: base.context.capabilities, error }),
324
+ persistenceFailure: (error) => result('failed', base.resolved.taskId, null, { error })
325
+ });
305
326
  }
306
327
  async function bindPlatformIssue(taskRef, options) {
307
328
  const base = await resolvedContext(taskRef, options);
@@ -435,7 +456,7 @@ function desiredFieldValues(frontmatter, fields) {
435
456
  }
436
457
  return desired;
437
458
  }
438
- async function syncPlatformIssue(taskRef, options) {
459
+ async function syncPlatformIssueImpl(taskRef, options) {
439
460
  const base = await resolvedContext(taskRef, options);
440
461
  if (!base.ok)
441
462
  return base.output;
@@ -582,5 +603,84 @@ async function syncPlatformIssue(taskRef, options) {
582
603
  operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
583
604
  });
584
605
  }
606
+ async function syncPlatformIssue(taskRef, options) {
607
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
608
+ if (!resolved.ok || options.dryRun)
609
+ return syncPlatformIssueImpl(taskRef, options);
610
+ let frontmatter;
611
+ let taskContent;
612
+ try {
613
+ taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
614
+ frontmatter = parseTaskFrontmatter(taskContent);
615
+ }
616
+ catch {
617
+ return syncPlatformIssueImpl(taskRef, options);
618
+ }
619
+ const identity = taskIssueIdentity(frontmatter);
620
+ if (!identity || ![
621
+ options.requirements, options.issueType, options.fields, options.status !== undefined,
622
+ options.assignees !== undefined, options.milestone !== undefined,
623
+ options.inLabels !== undefined, options.state !== undefined
624
+ ].some(Boolean))
625
+ return syncPlatformIssueImpl(taskRef, options);
626
+ let fromDiffFiles;
627
+ let mappingDigest;
628
+ if (options.inLabels === 'from-diff') {
629
+ const taskBase = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
630
+ if (taskBase && (!options.base || options.base === taskBase)) {
631
+ try {
632
+ fromDiffFiles = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
633
+ cwd: resolved.repoRoot, encoding: 'utf8'
634
+ }).trim().split(/\r?\n/).filter(Boolean).sort();
635
+ const config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8'));
636
+ const mapping = inLabelMappingDigest(config.labels?.in);
637
+ if (!mapping.ok)
638
+ return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
639
+ error: { code: mapping.error.code, message: mapping.error.message, retryable: false }
640
+ });
641
+ mappingDigest = mapping.digest;
642
+ }
643
+ catch {
644
+ return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
645
+ error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: 'Unable to derive the current changed-file set for Issue metadata recovery', retryable: false }
646
+ });
647
+ }
648
+ }
649
+ }
650
+ const issueMetadata = {
651
+ requirements: options.requirements === true,
652
+ issueType: options.issueType === true,
653
+ fields: options.fields === true,
654
+ ...(options.status !== undefined ? { status: options.status } : {}),
655
+ ...(options.assignees !== undefined ? { assignees: options.assignees } : {}),
656
+ ...(options.milestone !== undefined ? { milestone: options.milestone } : {}),
657
+ ...(options.inLabels ? { inLabels: options.inLabels } : {}),
658
+ ...(options.base ? { base: options.base } : {}),
659
+ ...(fromDiffFiles ? { fromDiffFiles } : {}),
660
+ ...(mappingDigest ? { inLabelMappingDigest: mappingDigest } : {}),
661
+ ...(options.state ? { state: options.state } : {}),
662
+ ...(options.closeReason ? { closeReason: options.closeReason } : {})
663
+ };
664
+ const operation = {
665
+ kind: 'issue-metadata',
666
+ target: JSON.stringify(identity),
667
+ expectedDigest: createHash('sha256').update(JSON.stringify({
668
+ ...issueMetadata,
669
+ task: resolved.taskId,
670
+ taskContent: createHash('sha256').update(taskContent).digest('hex')
671
+ })).digest('hex')
672
+ };
673
+ const dependency = options.dependency ?? (options.state === 'closed' && options.closeReason === 'not_planned' ? 'required' : 'deferred');
674
+ const execute = () => syncPlatformIssueImpl(taskRef, options);
675
+ if (options.skipQueue)
676
+ return execute();
677
+ return coordinatePlatformWrite({
678
+ operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, issueMetadata, dependency },
679
+ agent: options.agent,
680
+ execute,
681
+ block: (error) => result('blocked', resolved.taskId, resourceIdentityNumber(identity), { error }),
682
+ persistenceFailure: (error) => result('failed', resolved.taskId, resourceIdentityNumber(identity), { error })
683
+ });
684
+ }
585
685
  export { bindPlatformIssue, createPlatformIssue, inspectPlatformIssue, requirementSectionAnchors, syncPlatformIssue };
586
686
  //# sourceMappingURL=issues.js.map
@@ -0,0 +1,74 @@
1
+ import { recoverPlatformOperations } from '../task/platform-operation-recovery.js';
2
+ import { recordPlatformOperation } from '../task/platform-operation-journal.js';
3
+ async function coordinatePlatformWrite(input) {
4
+ let operationId;
5
+ try {
6
+ const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
7
+ operationId = queued.id;
8
+ }
9
+ catch (error) {
10
+ const value = error;
11
+ return input.persistenceFailure({
12
+ code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
13
+ message: value.message || 'Unable to queue platform operation',
14
+ retryable: true
15
+ });
16
+ }
17
+ const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
18
+ ? 'all' : input.operation.dependency;
19
+ const drained = input.drain
20
+ ? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
21
+ : await recoverPlatformOperations(input.operation.taskRef, selection, {
22
+ agent: input.agent,
23
+ client: input.client,
24
+ cwd: input.operation.cwd,
25
+ excludeId: operationId
26
+ });
27
+ if (drained.status !== 'applied' && drained.status !== 'no-op') {
28
+ return input.block(drained.error ?? {
29
+ code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
30
+ message: 'A previous platform operation is still unresolved; the current write remains queued',
31
+ retryable: true
32
+ });
33
+ }
34
+ try {
35
+ recordPlatformOperation({ ...input.operation, state: 'pending' });
36
+ }
37
+ catch (error) {
38
+ const value = error;
39
+ return input.persistenceFailure({
40
+ code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
41
+ message: value.message || 'Unable to mark platform operation as started',
42
+ retryable: true
43
+ });
44
+ }
45
+ let output;
46
+ try {
47
+ output = await input.execute();
48
+ }
49
+ catch (error) {
50
+ const value = error;
51
+ output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
52
+ }
53
+ const succeeded = (output.status === 'applied' || output.status === 'no-op')
54
+ && !output.error && !(output.warnings?.length);
55
+ const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
56
+ try {
57
+ recordPlatformOperation({
58
+ ...input.operation,
59
+ state,
60
+ lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
61
+ });
62
+ }
63
+ catch (error) {
64
+ const value = error;
65
+ return input.persistenceFailure({
66
+ code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
67
+ message: value.message || 'Unable to persist platform operation outcome',
68
+ retryable: true
69
+ });
70
+ }
71
+ return output;
72
+ }
73
+ export { coordinatePlatformWrite };
74
+ //# sourceMappingURL=operation-coordinator.js.map
@@ -1,7 +1,12 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { coordinatePlatformWrite } from './operation-coordinator.js';
1
5
  import { resolvePlatformProviderContext } from './context.js';
2
6
  import { platformResult } from './types.js';
3
7
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
4
- import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.js';
8
+ import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.js';
9
+ import { resolveTaskRef } from '../task/resolve-ref.js';
5
10
  const REVIEW_EVENTS = ['COMMENT', 'APPROVE', 'REQUEST_CHANGES'];
6
11
  // Marker-safe scope: a task id (`TASK-YYYYMMDD-HHMMSS`) or a bare PR number
7
12
  // (`pr{N}`). Any other value (e.g. one containing `\r\n` or `-->`) would break
@@ -19,6 +24,34 @@ export function reviewedCommitMarker(sha) {
19
24
  function normalizeBody(body) {
20
25
  return String(body || '').replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
21
26
  }
27
+ function canonicalReviewBody(body) {
28
+ return normalizeBody(body).replace(/\n$/u, '');
29
+ }
30
+ function reviewArtifactName(round, artifactFile) {
31
+ return path.basename(artifactFile) === artifactFile
32
+ && (artifactFile === 'pr-review.md' && round === 1 || artifactFile === `pr-review-r${round}.md`);
33
+ }
34
+ export function readReviewBodyFile(taskRef, round, artifactFile, bodyDigest, cwd) {
35
+ if (!/^TASK-\d{8}-\d{6}$/u.test(taskRef) || path.basename(artifactFile) !== artifactFile
36
+ || !reviewArtifactName(round, artifactFile)) {
37
+ throw Object.assign(new Error('Pull-request review artifact identity is invalid'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
38
+ }
39
+ const task = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
40
+ if (!task.ok)
41
+ throw Object.assign(new Error(task.message), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
42
+ const bodyFile = artifactFile.replace(/^pr-review/u, 'pr-review-body');
43
+ const report = fs.readFileSync(path.join(task.taskDir, artifactFile), 'utf8');
44
+ const bodyReferences = [...report.matchAll(/^\*\*(?:正文文件|Body file)\*\*(?::|:)\s*`([^`]+)`[^\r\n]*$/gmu)]
45
+ .map((match) => match[1]);
46
+ if (bodyReferences.length !== 1 || bodyReferences[0] !== bodyFile) {
47
+ throw Object.assign(new Error('Pull-request review artifact does not reference its canonical body file'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
48
+ }
49
+ const body = canonicalReviewBody(fs.readFileSync(path.join(task.taskDir, bodyFile), 'utf8'));
50
+ if (!body || createHash('sha256').update(body).digest('hex') !== bodyDigest) {
51
+ throw Object.assign(new Error('Canonical pull-request review body does not match its persisted digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
52
+ }
53
+ return body;
54
+ }
22
55
  function firstLine(body) {
23
56
  return normalizeBody(String(body || '')).split('\n', 1)[0] || '';
24
57
  }
@@ -74,6 +107,13 @@ export async function publishPrReview(options) {
74
107
  if (!hasUsableContext(context)) {
75
108
  return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
76
109
  }
110
+ const providerScopeId = loaded.ok ? loaded.value.snapshot.scope.id : null;
111
+ if (options.expectedProviderScopeId && providerScopeId !== options.expectedProviderScopeId) {
112
+ return platformResult('failed', {
113
+ platform: context.platform, capabilities: context.capabilities,
114
+ error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current provider scope differs from the persisted operation target', retryable: false }
115
+ });
116
+ }
77
117
  const identity = loaded.ok ? (() => { try {
78
118
  return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber));
79
119
  }
@@ -87,6 +127,13 @@ export async function publishPrReview(options) {
87
127
  });
88
128
  }
89
129
  const identityNumber = resourceIdentityNumber(identity);
130
+ if (options.expectedResource && !resourceIdentityEquals(identity, options.expectedResource)) {
131
+ return platformResult('failed', {
132
+ platform: context.platform, capabilities: context.capabilities,
133
+ resource: { kind: 'pull-request', number: identityNumber, identity },
134
+ error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current pull request identity differs from the persisted operation target', retryable: false }
135
+ });
136
+ }
90
137
  if (!REVIEW_EVENTS.includes(options.event)) {
91
138
  return platformResult('failed', {
92
139
  platform: context.platform, capabilities: context.capabilities,
@@ -101,6 +148,65 @@ export async function publishPrReview(options) {
101
148
  error: { code: 'REVIEW_IDENTITY_INVALID', message: 'review scope must be a task id or pr{N}; commitSha is required', retryable: false }
102
149
  });
103
150
  }
151
+ if (/^TASK-\d{8}-\d{6}$/u.test(options.identity.scope) && !options.skipQueue && !options.dryRun) {
152
+ if (!options.agent)
153
+ return platformResult('failed', {
154
+ platform: context.platform, capabilities: context.capabilities,
155
+ resource: { kind: 'pull-request', number: identityNumber, identity },
156
+ error: { code: 'PR_REVIEW_AGENT_REQUIRED', message: 'task-scoped review publication requires an agent token', retryable: false }
157
+ });
158
+ if (!options.recoveryArtifact || !reviewArtifactName(options.identity.round, options.recoveryArtifact))
159
+ return platformResult('failed', {
160
+ platform: context.platform, capabilities: context.capabilities,
161
+ resource: { kind: 'pull-request', number: identityNumber, identity },
162
+ error: { code: 'PR_REVIEW_ARTIFACT_REQUIRED', message: 'task-scoped review publication requires its canonical review artifact', retryable: false }
163
+ });
164
+ const body = canonicalReviewBody(options.body);
165
+ const bodyDigest = createHash('sha256').update(body).digest('hex');
166
+ try {
167
+ if (readReviewBodyFile(options.identity.scope, options.identity.round, options.recoveryArtifact, bodyDigest, options.cwd) !== body)
168
+ throw new Error('body mismatch');
169
+ }
170
+ catch {
171
+ return platformResult('failed', {
172
+ platform: context.platform, capabilities: context.capabilities,
173
+ resource: { kind: 'pull-request', number: identityNumber, identity },
174
+ error: { code: 'PR_REVIEW_ARTIFACT_MISMATCH', message: 'Review body must match the canonical review body file', retryable: false }
175
+ });
176
+ }
177
+ const pullRequestReview = {
178
+ prNumber: String(options.prNumber),
179
+ resource: identity,
180
+ providerScopeId: providerScopeId,
181
+ scope: options.identity.scope,
182
+ round: options.identity.round,
183
+ commitSha: options.identity.commitSha,
184
+ event: options.event,
185
+ artifactFile: options.recoveryArtifact,
186
+ bodyDigest
187
+ };
188
+ return coordinatePlatformWrite({
189
+ operation: {
190
+ taskRef: options.identity.scope,
191
+ cwd: options.cwd || process.cwd(),
192
+ kind: 'pull-request-review',
193
+ target: JSON.stringify(identity),
194
+ expectedDigest: createHash('sha256').update(JSON.stringify(pullRequestReview)).digest('hex'),
195
+ dependency: 'deferred',
196
+ pullRequestReview
197
+ },
198
+ agent: options.agent,
199
+ execute: () => publishPrReview({ ...options, expectedProviderScopeId: providerScopeId, skipQueue: true }),
200
+ block: (error) => platformResult('blocked', {
201
+ platform: context.platform, capabilities: context.capabilities,
202
+ resource: { kind: 'pull-request', number: identityNumber, identity }, error
203
+ }),
204
+ persistenceFailure: (error) => platformResult('failed', {
205
+ platform: context.platform, capabilities: context.capabilities,
206
+ resource: { kind: 'pull-request', number: identityNumber, identity }, error
207
+ })
208
+ });
209
+ }
104
210
  const marker = reviewMarker({ ...options.identity, resource: identity });
105
211
  const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
106
212
  if (listed.status === 'failed' || listed.status === 'blocked')