@fitlab-ai/agent-infra 0.11.6 → 0.11.8

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 (192) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/git/primary-worktree.js +58 -0
  6. package/dist/lib/internal/cli-route-inventory.js +3 -0
  7. package/dist/lib/internal/platform-pr-review.js +20 -3
  8. package/dist/lib/internal/task-operation-registry.js +3 -1
  9. package/dist/lib/internal/task-platform-recovery.js +56 -0
  10. package/dist/lib/merge.js +25 -5
  11. package/dist/lib/platform/completion-backfill.js +2 -1
  12. package/dist/lib/platform/in-label-sync.js +12 -1
  13. package/dist/lib/platform/issue-comments.js +58 -4
  14. package/dist/lib/platform/issues.js +105 -5
  15. package/dist/lib/platform/operation-coordinator.js +74 -0
  16. package/dist/lib/platform/pr-review.js +107 -1
  17. package/dist/lib/platform/pr-summary.js +79 -57
  18. package/dist/lib/platform/pull-requests.js +125 -7
  19. package/dist/lib/sandbox/commands/create.js +6 -3
  20. package/dist/lib/sandbox/commands/ls.js +4 -3
  21. package/dist/lib/sandbox/commands/rm.js +1 -1
  22. package/dist/lib/sandbox/config.js +22 -4
  23. package/dist/lib/sandbox/control/server.js +6 -2
  24. package/dist/lib/sandbox/git-metadata.js +50 -0
  25. package/dist/lib/sandbox/index.js +1 -1
  26. package/dist/lib/sandbox/mounts.js +12 -0
  27. package/dist/lib/sandbox/recovery.js +11 -0
  28. package/dist/lib/sandbox/removal.js +58 -20
  29. package/dist/lib/sandbox/tools.js +29 -7
  30. package/dist/lib/server/config.js +7 -27
  31. package/dist/lib/server/daemon.js +4 -3
  32. package/dist/lib/server/process-control.js +4 -2
  33. package/dist/lib/server/runner.js +4 -3
  34. package/dist/lib/task/activity-intent.js +4 -3
  35. package/dist/lib/task/artifact-lifecycle.js +42 -10
  36. package/dist/lib/task/artifact-receipts.js +62 -22
  37. package/dist/lib/task/capabilities.js +22 -4
  38. package/dist/lib/task/completion-facts.js +56 -0
  39. package/dist/lib/task/create-service.js +11 -5
  40. package/dist/lib/task/create.js +60 -79
  41. package/dist/lib/task/events.js +268 -195
  42. package/dist/lib/task/finalization-artifacts.js +4 -1
  43. package/dist/lib/task/finalization-handoff.js +2 -1
  44. package/dist/lib/task/finalization.js +157 -24
  45. package/dist/lib/task/invalidation-command.js +4 -2
  46. package/dist/lib/task/invalidation.js +38 -9
  47. package/dist/lib/task/lifecycle.js +38 -2
  48. package/dist/lib/task/local-artifact-finalization.js +17 -3
  49. package/dist/lib/task/platform-operation-journal.js +152 -0
  50. package/dist/lib/task/platform-operation-recovery.js +459 -0
  51. package/dist/lib/task/qualification-audit.js +23 -105
  52. package/dist/lib/task/review-finalization.js +7 -0
  53. package/dist/lib/task/review-identity.js +69 -0
  54. package/dist/lib/task/sections.js +6 -5
  55. package/dist/lib/task/task-execution-lock.js +6 -3
  56. package/dist/lib/task/verification-engine.js +14 -63
  57. package/dist/lib/task/verification.js +35 -38
  58. package/dist/lib/task/workflow-warning-intents.js +7 -1
  59. package/dist/lifecycle-build-manifest.json +3 -3
  60. package/lib/agent-clients/adapters/codex.ts +1 -1
  61. package/lib/agent-clients/config.ts +9 -3
  62. package/lib/defaults.json +6 -3
  63. package/lib/git/primary-worktree.ts +62 -0
  64. package/lib/internal/cli-route-inventory.ts +2 -0
  65. package/lib/internal/platform-pr-review.ts +14 -3
  66. package/lib/internal/task-operation-registry.ts +3 -1
  67. package/lib/internal/task-platform-recovery.ts +53 -0
  68. package/lib/merge.ts +26 -6
  69. package/lib/platform/completion-backfill.ts +2 -1
  70. package/lib/platform/in-label-sync.ts +12 -0
  71. package/lib/platform/issue-comments.ts +59 -3
  72. package/lib/platform/issues.ts +103 -5
  73. package/lib/platform/operation-coordinator.ts +96 -0
  74. package/lib/platform/pr-review.ts +113 -1
  75. package/lib/platform/pr-summary.ts +22 -1
  76. package/lib/platform/pull-requests.ts +132 -8
  77. package/lib/sandbox/commands/create.ts +7 -8
  78. package/lib/sandbox/commands/ls.ts +4 -3
  79. package/lib/sandbox/commands/rm.ts +1 -1
  80. package/lib/sandbox/config.ts +23 -5
  81. package/lib/sandbox/control/server.ts +5 -2
  82. package/lib/sandbox/git-metadata.ts +66 -0
  83. package/lib/sandbox/index.ts +1 -1
  84. package/lib/sandbox/mounts.ts +14 -0
  85. package/lib/sandbox/recovery.ts +13 -0
  86. package/lib/sandbox/removal.ts +57 -19
  87. package/lib/sandbox/tool-types.ts +1 -1
  88. package/lib/sandbox/tools.ts +36 -7
  89. package/lib/server/config.ts +7 -29
  90. package/lib/server/daemon.ts +5 -3
  91. package/lib/server/process-control.ts +4 -2
  92. package/lib/server/runner.ts +7 -4
  93. package/lib/server/server.schema.json +0 -4
  94. package/lib/task/activity-intent.ts +4 -3
  95. package/lib/task/artifact-lifecycle.ts +35 -11
  96. package/lib/task/artifact-receipts.ts +55 -15
  97. package/lib/task/capabilities.ts +17 -3
  98. package/lib/task/completion-facts.ts +66 -0
  99. package/lib/task/create-service.ts +12 -6
  100. package/lib/task/create.ts +54 -69
  101. package/lib/task/events.ts +248 -182
  102. package/lib/task/finalization-artifacts.ts +4 -1
  103. package/lib/task/finalization-handoff.ts +2 -1
  104. package/lib/task/finalization.ts +154 -21
  105. package/lib/task/invalidation-command.ts +4 -2
  106. package/lib/task/invalidation.ts +35 -10
  107. package/lib/task/lifecycle.ts +35 -3
  108. package/lib/task/local-artifact-finalization.ts +13 -3
  109. package/lib/task/platform-operation-journal.ts +230 -0
  110. package/lib/task/platform-operation-recovery.ts +435 -0
  111. package/lib/task/qualification-audit.ts +21 -114
  112. package/lib/task/review-finalization.ts +7 -0
  113. package/lib/task/review-identity.ts +85 -0
  114. package/lib/task/sections.ts +6 -6
  115. package/lib/task/task-execution-lock.ts +7 -3
  116. package/lib/task/verification-engine.ts +13 -86
  117. package/lib/task/verification.ts +34 -38
  118. package/lib/task/workflow-warning-intents.ts +9 -1
  119. package/package.json +1 -1
  120. package/templates/.agents/README.en.md +19 -17
  121. package/templates/.agents/README.zh-CN.md +18 -16
  122. package/templates/.agents/rules/README.en.md +2 -1
  123. package/templates/.agents/rules/README.zh-CN.md +2 -1
  124. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  125. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  126. package/templates/.agents/rules/task-management.en.md +12 -0
  127. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  128. package/templates/.agents/rules/validation-output.en.md +19 -0
  129. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  130. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  131. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  132. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  133. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  134. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
  135. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
  136. package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
  137. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  138. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  139. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  140. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  142. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  143. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  144. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  145. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  146. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  147. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  148. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  149. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  150. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  151. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  152. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  153. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  155. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  157. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  158. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  159. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  160. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  161. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  162. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  163. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  164. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  165. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  166. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  167. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  168. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  169. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  170. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  171. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  172. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  173. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  174. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  175. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  176. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  177. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  178. package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
  179. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
  180. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  181. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  182. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  183. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  184. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  191. package/templates/.agents/templates/task.en.md +2 -2
  192. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -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')
@@ -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 { enumerateArtifacts } from '../task/artifacts.js';
4
5
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
5
6
  import { resolveTaskRef } from '../task/resolve-ref.js';
@@ -9,6 +10,7 @@ import { resolvePlatformProviderContext } from './context.js';
9
10
  import { normalizeCommentContent } from './issue-comments.js';
10
11
  import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
11
12
  import { platformResult } from './types.js';
13
+ import { coordinatePlatformWrite } from './operation-coordinator.js';
12
14
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
13
15
  import { resourceIdentityEquals } from './resource-identity.js';
14
16
  import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
@@ -609,74 +611,94 @@ async function syncPullRequestSummary(taskRef, options) {
609
611
  operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
610
612
  result: null, warnings: [], ...info
611
613
  };
612
- const existingComment = reconciliation.commentId === null
613
- ? undefined
614
- : providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
615
- const written = loaded.value.provider.comments?.write
616
- ? await loaded.value.provider.comments.write({
617
- context: providerOperationContext(loaded.value),
618
- parent: prIdentity,
619
- body: desired,
620
- ...(existingComment ? { existingComment } : {}),
621
- mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
622
- })
623
- : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
624
- if (!written.ok)
625
- return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
626
- const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
627
- const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
628
- const compensateWrittenSummary = async () => reconciliation.action === 'create'
629
- ? written.value.remoteId && loaded.value.provider.comments?.delete
630
- ? await loaded.value.provider.comments.delete({
631
- context: providerOperationContext(loaded.value),
632
- parent: prIdentity,
633
- comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
634
- mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
635
- })
636
- : { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
637
- : reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
614
+ const publish = async () => {
615
+ const existingComment = reconciliation.commentId === null
616
+ ? undefined
617
+ : providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
618
+ const written = loaded.value.provider.comments?.write
638
619
  ? await loaded.value.provider.comments.write({
639
620
  context: providerOperationContext(loaded.value),
640
621
  parent: prIdentity,
641
- body: previousBody,
642
- existingComment: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId)),
643
- mutation: { idempotencyKey: `pr-summary-restore:${resolved.taskId}` }
622
+ body: desired,
623
+ ...(existingComment ? { existingComment } : {}),
624
+ mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
644
625
  })
645
- : { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Previous summary body is unavailable for restoration', retryable: true } };
646
- if (!after.ok) {
647
- const compensated = await compensateWrittenSummary();
648
- if (!compensated.ok)
626
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
627
+ if (!written.ok)
628
+ return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
629
+ const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
630
+ const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
631
+ const compensateWrittenSummary = async () => reconciliation.action === 'create'
632
+ ? written.value.remoteId && loaded.value.provider.comments?.delete
633
+ ? await loaded.value.provider.comments.delete({
634
+ context: providerOperationContext(loaded.value),
635
+ parent: prIdentity,
636
+ comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
637
+ mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
638
+ })
639
+ : { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
640
+ : reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
641
+ ? await loaded.value.provider.comments.write({
642
+ context: providerOperationContext(loaded.value),
643
+ parent: prIdentity,
644
+ body: previousBody,
645
+ existingComment: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId)),
646
+ mutation: { idempotencyKey: `pr-summary-restore:${resolved.taskId}` }
647
+ })
648
+ : { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Previous summary body is unavailable for restoration', retryable: true } };
649
+ if (!after.ok) {
650
+ const compensated = await compensateWrittenSummary();
651
+ if (!compensated.ok)
652
+ return fail('blocked', context, {
653
+ code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
654
+ message: `Summary post-write verification failed and compensation failed: ${compensated.error.message}`,
655
+ retryable: true
656
+ }, prNumber);
649
657
  return fail('blocked', context, {
650
- code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
651
- message: `Summary post-write verification failed and compensation failed: ${compensated.error.message}`,
658
+ code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
659
+ message: `Summary was written but pull-request verification failed: ${after.error?.message || 'unknown verification error'}; the previous summary was restored or the new comment was deleted`,
652
660
  retryable: true
653
661
  }, prNumber);
654
- return fail('blocked', context, {
655
- code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
656
- message: `Summary was written but pull-request verification failed: ${after.error?.message || 'unknown verification error'}; the previous summary was restored or the new comment was deleted`,
657
- retryable: true
658
- }, prNumber);
659
- }
660
- if (!sameIdentity(initial.value, after.value)) {
661
- const compensated = await compensateWrittenSummary();
662
- if (!compensated.ok)
662
+ }
663
+ if (!sameIdentity(initial.value, after.value)) {
664
+ const compensated = await compensateWrittenSummary();
665
+ if (!compensated.ok)
666
+ return fail('blocked', context, {
667
+ code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
668
+ message: `Pull-request head changed and summary compensation failed: ${compensated.error.message}`,
669
+ retryable: true
670
+ }, prNumber);
663
671
  return fail('blocked', context, {
664
- code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
665
- message: `Pull-request head changed and summary compensation failed: ${compensated.error.message}`,
672
+ code: 'PR_SUMMARY_HEAD_RACE',
673
+ message: 'Pull-request head changed while publishing; the previous summary was restored or the new comment was deleted; rerun report and summary sync',
666
674
  retryable: true
667
675
  }, prNumber);
668
- return fail('blocked', context, {
669
- code: 'PR_SUMMARY_HEAD_RACE',
670
- message: 'Pull-request head changed while publishing; the previous summary was restored or the new comment was deleted; rerun report and summary sync',
671
- retryable: true
672
- }, prNumber);
673
- }
674
- return {
675
- ...basePlatformResult('applied', context, resolved.taskId, prNumber),
676
- comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 },
677
- operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
678
- result: null, warnings: [], ...info
676
+ }
677
+ return {
678
+ ...basePlatformResult('applied', context, resolved.taskId, prNumber),
679
+ comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 },
680
+ operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
681
+ result: null, warnings: [], ...info
682
+ };
683
+ };
684
+ const operation = {
685
+ taskRef: resolved.taskId,
686
+ cwd: resolved.repoRoot,
687
+ kind: 'pull-request-summary',
688
+ target: JSON.stringify(prIdentity),
689
+ expectedDigest: createHash('sha256').update(desired).digest('hex'),
690
+ dependency: 'deferred',
691
+ pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile }
679
692
  };
693
+ if (options.skipQueue)
694
+ return publish();
695
+ return coordinatePlatformWrite({
696
+ operation,
697
+ agent: options.agent,
698
+ execute: publish,
699
+ block: (error) => fail('blocked', context, error, prNumber),
700
+ persistenceFailure: (error) => fail('failed', context, error, prNumber)
701
+ });
680
702
  };
681
703
  return await (options.lockAlreadyHeld
682
704
  ? execute()