@fitlab-ai/agent-infra 0.12.0 → 0.12.1

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 (46) hide show
  1. package/dist/lib/internal/manual-validation.js +11 -17
  2. package/dist/lib/internal/platform-comment.js +56 -7
  3. package/dist/lib/platform/issue-comments.js +373 -267
  4. package/dist/lib/platform/issue-summary-content.js +112 -0
  5. package/dist/lib/platform/pr-change-report.js +175 -26
  6. package/dist/lib/platform/pr-summary-content.js +54 -0
  7. package/dist/lib/platform/pr-summary.js +318 -284
  8. package/dist/lib/platform/summary-content-pipeline.js +18 -0
  9. package/dist/lib/sandbox/removal.js +59 -2
  10. package/dist/lib/task/finalization.js +16 -7
  11. package/dist/lib/task/platform-operation-recovery.js +87 -87
  12. package/dist/lifecycle-build-manifest.json +3 -3
  13. package/lib/internal/manual-validation.ts +12 -18
  14. package/lib/internal/platform-comment.ts +46 -4
  15. package/lib/platform/issue-comments.ts +478 -280
  16. package/lib/platform/issue-summary-content.ts +132 -0
  17. package/lib/platform/pr-change-report.ts +165 -20
  18. package/lib/platform/pr-summary-content.ts +60 -0
  19. package/lib/platform/pr-summary.ts +364 -257
  20. package/lib/platform/summary-content-pipeline.ts +23 -0
  21. package/lib/sandbox/removal.ts +57 -1
  22. package/lib/task/finalization.ts +11 -6
  23. package/lib/task/platform-operation-recovery.ts +70 -81
  24. package/package.json +1 -1
  25. package/templates/.agents/rules/pr-sync.en.md +4 -2
  26. package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
  27. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  28. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  29. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  30. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  31. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
  32. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
  33. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
  34. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
  35. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  36. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  37. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
  38. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
  39. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  40. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  41. package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
  42. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
  43. package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
  44. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  45. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
  46. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
@@ -5,18 +5,21 @@ import { createHash } from 'node:crypto';
5
5
  import { enumerateArtifacts } from '../task/artifacts.ts';
6
6
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
7
7
  import { resolveTaskRef } from '../task/resolve-ref.ts';
8
+ import type { ResolveRefResult } from '../task/resolve-ref.ts';
8
9
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
9
10
  import type { PrDeliveryFact } from '../task/pr-delivery-fact.ts';
10
11
  import { resolvePlatformProviderContext } from './context.ts';
12
+ import { inspectPlatformPullRequest } from './pull-requests.ts';
11
13
  import type { LoadedContext, PlatformClient } from './context.ts';
12
- import { normalizeCommentContent } from './issue-comments.ts';
14
+ import { normalizeCommentContent, syncCommentPlan } from './issue-comments.ts';
15
+ import type { CommentPlanStrategy, RenderedChunk } from './issue-comments.ts';
13
16
  import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
14
17
  import { platformResult } from './types.ts';
15
18
  import type { PlatformResult } from './types.ts';
16
- import { coordinatePlatformWrite } from './operation-coordinator.ts';
19
+ import type { PlatformOperation as JournalOperation, RecordOperationInput } from '../task/platform-operation-journal.ts';
17
20
  import type { OperationWarning } from '../task/operation-outcome.ts';
18
- import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
19
- import { resourceIdentityEquals } from './resource-identity.ts';
21
+ import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
22
+ import { parseResourceIdentity, resourceIdentityEquals } from './resource-identity.ts';
20
23
  import type { ResourceIdentity } from './resource-identity.ts';
21
24
  import type { PlatformChangeRequestSnapshot } from './snapshots.ts';
22
25
  import type { ChangeRequestSnapshot } from './provider-contract.ts';
@@ -38,6 +41,9 @@ import type {
38
41
  import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
39
42
  import { readManualValidationCompletion } from '../task/manual-validation-completion.ts';
40
43
  import { readManualValidationTransaction } from '../task/manual-validation-transaction.ts';
44
+ import { parsePrSummaryContent, renderPrSummaryContent } from './pr-summary-content.ts';
45
+ import { runSummaryContentPipeline } from './summary-content-pipeline.ts';
46
+ import { parseLedgerDocument } from '../task/ledger.ts';
41
47
 
42
48
  type SummaryComment = { id: number | string; body: string };
43
49
  type ChangeReportState = 'ready' | 'missing' | 'stale' | 'invalid';
@@ -62,6 +68,20 @@ type ManualValidationSummaryOptions = {
62
68
  prHeadSha?: string;
63
69
  authority?: 'coordinator';
64
70
  };
71
+ type PullRequestSummarySyncOptions = {
72
+ agent: string;
73
+ body: string;
74
+ changeReportFile?: string;
75
+ cwd?: string;
76
+ client?: PlatformClient;
77
+ dryRun?: boolean;
78
+ strict?: boolean;
79
+ primaryResult: PullRequestPrimaryResult;
80
+ runtimeVersion?: string;
81
+ manualValidation?: ManualValidationSummaryOptions;
82
+ queuedOperation?: JournalOperation;
83
+ };
84
+ type ResolvedTaskRef = Extract<ResolveRefResult, { ok: true }>;
65
85
  type ReportWriteResult = PlatformResult & {
66
86
  report: { path: string; status: 'written' | 'no-op' | 'planned'; precheckVerdict: 'clear' | 'needs-review'; nextAction: 'watch-pr' | 'review-code' } | null;
67
87
  };
@@ -122,17 +142,6 @@ function buildPullRequestSummary(taskId: string, body: string, headSha: string):
122
142
  return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
123
143
  }
124
144
 
125
- function reconcileSummaryComment(comments: SummaryComment[], taskId: string, desired: string):
126
- { action: 'create' | 'update' | 'no-op' | 'conflict'; commentId: number | string | null } {
127
- const marker = summaryMarker(taskId);
128
- const matches = comments.filter((comment) => comment.body.includes(marker));
129
- if (matches.length > 1) return { action: 'conflict', commentId: null };
130
- if (matches.length === 0) return { action: 'create', commentId: null };
131
- return normalizeCommentContent(matches[0]!.body) === normalizeCommentContent(desired)
132
- ? { action: 'no-op', commentId: matches[0]!.id }
133
- : { action: 'update', commentId: matches[0]!.id };
134
- }
135
-
136
145
  function canonicalArtifacts(taskDir: string) {
137
146
  const allowed = /^(plan|review-plan|code|review-code|manual-validation)(?:-r\d+)?\.md$/;
138
147
  const byFamily = new Map<string, { family: string; name: string; path: string; round: number }>();
@@ -174,6 +183,281 @@ function inspectionError(error: { code: string; message: string; retryable: bool
174
183
  return error.retryable ? 'blocked' : 'failed';
175
184
  }
176
185
 
186
+ function preparationError(code: string, message: string): Error {
187
+ return Object.assign(new Error(message), { code });
188
+ }
189
+
190
+ function validateManualSummaryPhase(manual: ManualValidationSummaryOptions | undefined, rendered: string): void {
191
+ const hasFinal = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(rendered);
192
+ if (hasFinal && (!manual || manual.phase !== 'final')) throw preparationError('MANUAL_VALIDATION_TRANSACTION_REQUIRED', 'final manual-validation summary requires the transaction coordinator');
193
+ if (manual && (manual.phase === 'pending' ? hasFinal : !hasFinal)) throw preparationError('MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', 'manual-validation summary phase does not match the requested writer phase');
194
+ }
195
+
196
+ function readPendingManualEvidence(resolved: ResolvedTaskRef, manual: ManualValidationSummaryOptions, current: PlatformChangeRequestSnapshot, prNumber: number): null {
197
+ const transaction = readManualValidationTransaction(resolved.taskDir, { transactionId: manual.transactionId!, taskId: resolved.taskId, prNumber, prHeadSha: current.head.sha });
198
+ if (!transaction.ok) throw preparationError(transaction.error.code, transaction.error.message);
199
+ return null;
200
+ }
201
+
202
+ function readFinalManualEvidence(resolved: ResolvedTaskRef, manual: ManualValidationSummaryOptions, rendered: string, current: PlatformChangeRequestSnapshot, prNumber: number): string {
203
+ if (!manual.receiptDigest || manual.prHeadSha !== current.head.sha || manual.authority !== 'coordinator') throw preparationError('MANUAL_VALIDATION_TRANSACTION_REQUIRED', 'final manual-validation summary requires transaction, receipt, and current head identity');
204
+ const completion = readManualValidationCompletion(resolved.taskDir, { transactionId: manual.transactionId!, taskId: resolved.taskId, prNumber, prHeadSha: current.head.sha, receiptDigest: manual.receiptDigest });
205
+ if (!completion.ok) throw preparationError(completion.error.code, completion.error.message);
206
+ if (completion.value.transaction.phase !== 'final-promotion-in-progress' || !completion.value.transaction.eventAppended
207
+ || !manualValidationFinalSummaryProjectionMatches(rendered, completion.value.receipt)) throw preparationError('MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', 'final summary does not match the receipt-backed promotion intent');
208
+ return completion.value.receipt.evidenceDigest;
209
+ }
210
+
211
+ async function manualValidationEvidence(
212
+ resolved: ResolvedTaskRef,
213
+ manual: ManualValidationSummaryOptions | undefined,
214
+ rendered: string,
215
+ current: PlatformChangeRequestSnapshot
216
+ ): Promise<string | null> {
217
+ validateManualSummaryPhase(manual, rendered);
218
+ if (!manual) return null;
219
+ const prNumber = current.number;
220
+ if (!Number.isSafeInteger(prNumber) || prNumber <= 0) throw preparationError('MANUAL_VALIDATION_PR_NUMBER_REQUIRED', 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot');
221
+ if (!manual.transactionId) throw preparationError('MANUAL_VALIDATION_TRANSACTION_REQUIRED', 'manual-validation summary requires a transaction ID');
222
+ return manual.phase === 'pending'
223
+ ? readPendingManualEvidence(resolved, manual, current, prNumber!)
224
+ : readFinalManualEvidence(resolved, manual, rendered, current, prNumber!);
225
+ }
226
+
227
+ function validatedSummaryReport(
228
+ resolved: ResolvedTaskRef,
229
+ options: PullRequestSummarySyncOptions,
230
+ current: PlatformChangeRequestSnapshot,
231
+ taskContent: string
232
+ ): PrChangeReport {
233
+ if (!options.changeReportFile) throw preparationError('PR_CHANGE_REPORT_MISSING', 'summary-sync requires --change-report-file');
234
+ const reportPath = taskReportPath(resolved.taskDir);
235
+ if (path.resolve(resolved.repoRoot, options.changeReportFile) !== reportPath) throw preparationError('PR_CHANGE_REPORT_PATH_INVALID', 'summary-sync must consume the task-bound pr-change-report.json');
236
+ const report = readPrChangeReport(reportPath);
237
+ if (!report.ok) throw preparationError(report.error.code, report.error.message);
238
+ const checked = currentReportCheck(report.value, current, taskContent, resolved.repoRoot);
239
+ if (!checked.ok) throw preparationError(checked.error.code, checked.error.message);
240
+ return report.value;
241
+ }
242
+
243
+ function renderedSummaryBody(taskId: string, rendered: string, report: PrChangeReport, manual: ManualValidationSummaryOptions | undefined, evidenceDigest: string | null, headSha: string): string {
244
+ const split = splitDocumentPlaceholder(rendered, CANONICAL_REPORT_PLACEHOLDER);
245
+ if (!split.ok) throw preparationError('PR_SUMMARY_BODY_CONTRACT_INVALID', split.error.message);
246
+ const prefix = sanitizeMarkdownDocument(split.value.prefix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
247
+ const suffix = sanitizeMarkdownDocument(split.value.suffix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
248
+ if (!prefix.ok || !suffix.ok) throw preparationError('PR_SUMMARY_BODY_CONTRACT_INVALID', 'PR summary body contract is invalid');
249
+ const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${CANONICAL_REPORT_PLACEHOLDER}${suffix.value}`, report);
250
+ if (!replaced.ok) throw preparationError(replaced.error.code, replaced.error.message);
251
+ const withReceipt = manual?.phase === 'final' ? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
252
+ `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${evidenceDigest}; head=${manual.prHeadSha} -->`) : replaced.value;
253
+ const body = buildPullRequestSummary(taskId, withReceipt, headSha);
254
+ if (!isSafeSummaryEnvelope(body, taskId)) throw preparationError('PR_SUMMARY_RENDER_INVALID', 'Summary contains an invalid or duplicated reserved control marker');
255
+ return body;
256
+ }
257
+
258
+ type PreparedBoundSummary = Readonly<{
259
+ parent: ResourceIdentity;
260
+ item: RenderedChunk;
261
+ snapshot: PlatformChangeRequestSnapshot;
262
+ rendered: string;
263
+ manual: ManualValidationSummaryOptions | undefined;
264
+ prNumber: number;
265
+ precheckVerdict: 'clear' | 'needs-review';
266
+ nextAction: 'watch-pr' | 'review-code';
267
+ }>;
268
+
269
+ type BoundSummaryState = {
270
+ prepared: PreparedBoundSummary | null;
271
+ };
272
+
273
+ function prepareBoundSummaryContent(body: string, taskContent: string) {
274
+ try { return prepareAndRenderPrSummaryContent(JSON.parse(body), taskContent); }
275
+ catch (error) {
276
+ throw preparationError('PR_SUMMARY_CONTENT_INVALID', error instanceof Error ? error.message : String(error));
277
+ }
278
+ }
279
+
280
+ async function prepareBoundPullRequestSummary(
281
+ resolved: ResolvedTaskRef,
282
+ options: PullRequestSummarySyncOptions,
283
+ context: PlatformResult,
284
+ loaded: LoadedContext,
285
+ expectedIdentity: ResourceIdentity
286
+ ): Promise<PreparedBoundSummary> {
287
+ const taskSnapshot = readBoundTaskSnapshot(resolved.taskMdPath);
288
+ if (taskSnapshot.factError) throw preparationError(taskSnapshot.factError.code, taskSnapshot.factError.message);
289
+ if (!taskSnapshot.boundFact || !resourceIdentityEquals(taskSnapshot.boundFact.identity.resource, expectedIdentity)) throw preparationError('PLATFORM_OPERATION_IDENTITY_MISMATCH', 'Bound pull request identity changed while the summary was queued');
290
+ const preparedContent = prepareBoundSummaryContent(options.body, taskSnapshot.taskContent);
291
+ const current = await inspectBoundPullRequest(context, resolved.repoRoot, expectedIdentity, loaded);
292
+ if (!current.ok) throw preparationError(current.error?.code || 'PR_SNAPSHOT_UNAVAILABLE', current.error?.message || 'Pull-request snapshot is unavailable');
293
+ const evidenceDigest = await manualValidationEvidence(resolved, options.manualValidation, preparedContent.rendered, current.value);
294
+ const report = validatedSummaryReport(resolved, options, current.value, taskSnapshot.taskContent);
295
+ const body = renderedSummaryBody(resolved.taskId, preparedContent.rendered, report, options.manualValidation, evidenceDigest, current.value.head.sha);
296
+ return {
297
+ parent: expectedIdentity,
298
+ item: { marker: summaryMarker(resolved.taskId), body, content: options.body, part: 1, total: 1 },
299
+ snapshot: current.value,
300
+ rendered: body,
301
+ manual: options.manualValidation,
302
+ prNumber: current.value.number,
303
+ precheckVerdict: report.precheck.verdict,
304
+ nextAction: report.precheck.route
305
+ };
306
+ }
307
+
308
+ async function validateBoundPullRequestSummary(
309
+ resolved: ResolvedTaskRef,
310
+ options: PullRequestSummarySyncOptions,
311
+ context: PlatformResult,
312
+ loaded: LoadedContext,
313
+ prepared: PreparedBoundSummary
314
+ ) {
315
+ const latest = await inspectBoundPullRequest(context, resolved.repoRoot, prepared.parent, loaded);
316
+ if (!latest.ok) return latest.error;
317
+ if (!sameIdentity(prepared.snapshot, latest.value)) return { code: 'PR_SUMMARY_HEAD_RACE', message: 'Pull-request identity or base/head snapshot changed during summary publication', retryable: true };
318
+ const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
319
+ const bound = readPrDeliveryFact(frontmatter, options.runtimeVersion);
320
+ if (bound.status !== 'valid' || bound.fact.state !== 'bound' || !resourceIdentityEquals(bound.fact.identity.resource, prepared.parent)) return { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request identity changed during summary publication', retryable: true };
321
+ try { await manualValidationEvidence(resolved, prepared.manual, prepared.rendered, latest.value); }
322
+ catch (error) {
323
+ const value = error as { code?: string; message?: string };
324
+ return { code: value.code || 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: value.message || String(error), retryable: false };
325
+ }
326
+ return null;
327
+ }
328
+
329
+ async function syncBoundPullRequestSummary(
330
+ resolved: ResolvedTaskRef,
331
+ options: PullRequestSummarySyncOptions,
332
+ context: PlatformResult,
333
+ loaded: LoadedContext,
334
+ expectedIdentity: ResourceIdentity,
335
+ preserveFailure: (output: PlatformResult, knownPrNumber?: number | null) => PullRequestSummaryResult
336
+ ): Promise<PullRequestSummaryResult> {
337
+ const state: BoundSummaryState = { prepared: null };
338
+ const expectedDigest = createHash('sha256').update(options.body).digest('hex');
339
+ const operation: Omit<RecordOperationInput, 'state' | 'lastCode'> = {
340
+ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: 'pull-request-summary',
341
+ target: JSON.stringify(expectedIdentity), expectedDigest, dependency: 'deferred',
342
+ pullRequestSummary: {
343
+ body: options.body, changeReportFile: options.changeReportFile ?? taskReportPath(resolved.taskDir),
344
+ ...(options.manualValidation?.transactionId ? { manualValidation: { ...options.manualValidation, transactionId: options.manualValidation.transactionId } } : {})
345
+ }
346
+ };
347
+ const strategy: CommentPlanStrategy = {
348
+ taskId: resolved.taskId, cwd: resolved.repoRoot, agent: options.agent, client: options.client, resolved: { loaded, context },
349
+ parentKind: 'pull-request', kind: 'summary', markerPrefix: summaryMarker(resolved.taskId).replace(/ -->$/u, ''),
350
+ dryRun: options.dryRun, verifyReadback: true,
351
+ prepare: async () => {
352
+ const prepared = await prepareBoundPullRequestSummary(resolved, options, context, loaded, expectedIdentity);
353
+ state.prepared = prepared;
354
+ return { parent: prepared.parent, items: [prepared.item] };
355
+ },
356
+ validate: async () => state.prepared ? validateBoundPullRequestSummary(resolved, options, context, loaded, state.prepared) : null
357
+ };
358
+ const output = await syncCommentPlan(strategy, operation, options.queuedOperation);
359
+ if (output.error || output.status === 'failed' || output.status === 'blocked') return preserveFailure({
360
+ ...output, resource: { kind: 'pull-request', number: state.prepared?.prNumber ?? resourceIdentityNumber(expectedIdentity), identity: expectedIdentity }
361
+ }, state.prepared?.prNumber ?? resourceIdentityNumber(expectedIdentity));
362
+ const info = state.prepared
363
+ ? { precheckVerdict: state.prepared.precheckVerdict, nextAction: state.prepared.nextAction }
364
+ : { precheckVerdict: 'needs-review' as const, nextAction: 'review-code' as const };
365
+ return {
366
+ ...output,
367
+ resource: { kind: 'pull-request', number: state.prepared?.prNumber ?? resourceIdentityNumber(expectedIdentity), identity: expectedIdentity },
368
+ comment: output.comment ? { ...output.comment, kind: 'summary' } : null,
369
+ result: null, warnings: [], ...info
370
+ };
371
+ }
372
+
373
+ type PreparedSummarySync = Readonly<{
374
+ ok: true;
375
+ resolved: ResolvedTaskRef;
376
+ fact: ReturnType<typeof readPrDeliveryFact>;
377
+ knownPrNumber: number | null;
378
+ loaded: LoadedContext;
379
+ context: PlatformResult;
380
+ }> | Readonly<{
381
+ ok: false;
382
+ knownPrNumber: number | null;
383
+ error: PlatformResult;
384
+ }>;
385
+
386
+ function validateUnboundPrSummaryInput(options: PullRequestSummarySyncOptions, taskContent: string, knownPrNumber: number | null): PlatformResult | null {
387
+ try { prepareAndRenderPrSummaryContent(JSON.parse(options.body), taskContent); return null; }
388
+ catch (error) {
389
+ return platformResult('failed', {
390
+ resource: { kind: 'pull-request', number: knownPrNumber },
391
+ error: { code: 'PR_SUMMARY_CONTENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
392
+ });
393
+ }
394
+ }
395
+
396
+ function boundSummaryPhase(raw: unknown): { passed: boolean; known: boolean } | null {
397
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
398
+ const input = raw as Record<string, unknown>;
399
+ const manual = input.manualValidation && typeof input.manualValidation === 'object' && !Array.isArray(input.manualValidation)
400
+ ? input.manualValidation as Record<string, unknown> : {};
401
+ const state = typeof manual.state === 'string' ? manual.state : null;
402
+ const overview = typeof input.overview === 'string' ? input.overview : '';
403
+ return {
404
+ passed: state === 'passed' || /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(overview),
405
+ known: state !== null || overview.length > 0
406
+ };
407
+ }
408
+
409
+ function manualSummaryPhaseFailure(options: PullRequestSummarySyncOptions, phase: { passed: boolean; known: boolean }): { code: string; message: string } | null {
410
+ const manual = options.manualValidation;
411
+ if (phase.passed && (manual?.phase !== 'final' || manual.authority !== 'coordinator')) return {
412
+ code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator'
413
+ };
414
+ if (!manual || !phase.known) return null;
415
+ if ((manual.phase === 'pending' && phase.passed) || (manual.phase === 'final' && !phase.passed)) return {
416
+ code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase'
417
+ };
418
+ return null;
419
+ }
420
+
421
+ function boundSummaryPhaseError(options: PullRequestSummarySyncOptions, raw: unknown, knownPrNumber: number | null): PlatformResult | null {
422
+ const phase = boundSummaryPhase(raw);
423
+ if (!phase) return null;
424
+ const error = manualSummaryPhaseFailure(options, phase);
425
+ return error ? platformResult('failed', { resource: { kind: 'pull-request', number: knownPrNumber }, error: { ...error, retryable: false } }) : null;
426
+ }
427
+
428
+ function validateBoundPrSummaryInput(options: PullRequestSummarySyncOptions, knownPrNumber: number | null): PlatformResult | null {
429
+ try {
430
+ return boundSummaryPhaseError(options, JSON.parse(options.body), knownPrNumber);
431
+ }
432
+ catch (error) {
433
+ return platformResult('failed', {
434
+ resource: { kind: 'pull-request', number: knownPrNumber },
435
+ error: { code: 'PR_SUMMARY_CONTENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
436
+ });
437
+ }
438
+ }
439
+
440
+ async function prepareSummarySync(taskRef: string, options: PullRequestSummarySyncOptions): Promise<PreparedSummarySync> {
441
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
442
+ if (!resolved.ok) return { ok: false, knownPrNumber: null, error: platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }) };
443
+ const knownPrNumber = boundPrNumberForWarning(resolved.taskMdPath);
444
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
445
+ const fact = readPrDeliveryFact(parseTypedTaskFrontmatter(taskContent), options.runtimeVersion);
446
+ if (fact.status === 'invalid') return { ok: false, knownPrNumber, error: platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }) };
447
+ const isBound = fact.status === 'valid' && fact.fact.state === 'bound';
448
+ const contentError = isBound
449
+ ? validateBoundPrSummaryInput(options, knownPrNumber)
450
+ : validateUnboundPrSummaryInput(options, taskContent, knownPrNumber);
451
+ if (contentError) return { ok: false, knownPrNumber, error: contentError };
452
+ if (!options.changeReportFile && (options.dryRun || fact.status !== 'valid' || fact.fact.state !== 'bound')) return {
453
+ ok: false, knownPrNumber, error: platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_MISSING', message: 'summary-sync requires --change-report-file', retryable: false } })
454
+ };
455
+ const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
456
+ const context = loaded.ok ? loaded.value.context : loaded.context;
457
+ if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status)) return { ok: false, knownPrNumber, error: context };
458
+ return { ok: true, resolved, fact, knownPrNumber, loaded: loaded.value, context };
459
+ }
460
+
177
461
  async function inspectBoundPullRequest(
178
462
  context: PlatformResult,
179
463
  repoRoot: string,
@@ -509,14 +793,14 @@ async function reportWrite(taskRef: string, options: ReportWriteOptions): Promis
509
793
 
510
794
  async function syncPullRequestSummary(
511
795
  taskRef: string,
512
- options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; skipQueue?: boolean }
796
+ options: PullRequestSummarySyncOptions
513
797
  ): Promise<PullRequestSummaryResult> {
514
798
  const warningResult = warningResultForPrimary(options.primaryResult);
515
799
  let knownPrNumber: number | null = null;
516
- const softenFailure = (output: PlatformResult): PullRequestSummaryResult => {
800
+ const softenFailure = (output: PlatformResult, fallbackNumber = knownPrNumber): PullRequestSummaryResult => {
517
801
  const prNumber = output.resource.kind === 'pull-request' && output.resource.number
518
802
  ? output.resource.number
519
- : knownPrNumber;
803
+ : fallbackNumber;
520
804
  const warning = output.error && prNumber && output.error.code !== 'PR_NOT_LINKED'
521
805
  ? {
522
806
  code: output.error.code,
@@ -539,257 +823,80 @@ async function syncPullRequestSummary(
539
823
  }
540
824
  : { ...output, result: null, warnings: [] };
541
825
  };
542
- const preserveFailure = (output: PlatformResult): PullRequestSummaryResult => options.strict
826
+ const preserveFailure = (output: PlatformResult, fallbackNumber = knownPrNumber): PullRequestSummaryResult => options.strict
827
+ || isHardSummaryFailure(output.error)
543
828
  ? { ...output, result: null, warnings: [] }
544
- : softenFailure(output);
545
- const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
546
- if (!resolved.ok) return softenFailure(platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }));
547
- const fail = (status: PlatformResult['status'], context: PlatformResult, error: PlatformResult['error'], prNumber = knownPrNumber): PullRequestSummaryResult => {
548
- const output = basePlatformResult(status, context, resolved.taskId, prNumber, error);
549
- return options.strict || isHardSummaryFailure(error)
550
- ? { ...output, result: null, warnings: [] }
551
- : { ...softenFailure(output) };
552
- };
553
- const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
554
- const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
555
- if (fact.status === 'invalid') return preserveFailure(platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }));
556
- if (!options.changeReportFile) return preserveFailure(platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_MISSING', message: 'summary-sync requires --change-report-file', retryable: false } }));
557
- const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
558
- const context = loaded.ok ? loaded.value.context : loaded.context;
559
- if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status)) {
560
- // Preserve the known PR target for diagnostics when provider context resolution fails.
561
- knownPrNumber = boundPrNumberForWarning(resolved.taskMdPath);
562
- return preserveFailure(context);
829
+ : softenFailure(output, fallbackNumber);
830
+ const prepared = await prepareSummarySync(taskRef, options);
831
+ if (!prepared.ok) return preserveFailure(prepared.error, prepared.knownPrNumber);
832
+ const { resolved, fact, loaded, context } = prepared;
833
+ knownPrNumber = prepared.knownPrNumber;
834
+ if (fact.status === 'valid' && fact.fact.state === 'bound') {
835
+ return syncBoundPullRequestSummary(resolved, options, context, loaded, fact.fact.identity.resource, preserveFailure);
563
836
  }
564
- try {
565
- const execute = async (): Promise<PullRequestSummaryResult> => {
566
- const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
567
- if (factError) return {
568
- ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: factError.code, message: factError.message, retryable: false } }),
569
- result: null, warnings: []
570
- };
571
- if (!boundFact) return {
572
- ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
573
- result: null, warnings: []
574
- };
575
- const prIdentity = boundFact.identity.resource;
576
- const boundPrNumber = resourceIdentityNumber(prIdentity);
577
- knownPrNumber = boundPrNumber;
578
- let prNumber = boundPrNumber;
579
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
580
- if (!initial.ok) return fail(initial.status, context, initial.error);
581
- const manual = options.manualValidation;
582
- let manualEvidenceDigest: string | null = null;
583
- const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
584
- if (hasFinalManualValidation && (!manual || manual.phase !== 'final')) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
585
- if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
586
- if (manual) {
587
- if (!Number.isSafeInteger(initial.value.number) || initial.value.number <= 0) return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot', retryable: false }, prNumber);
588
- prNumber = initial.value.number;
589
- knownPrNumber = prNumber;
590
- if (!manual.transactionId) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'manual-validation summary requires a transaction ID', retryable: false }, prNumber);
591
- if (manual.phase === 'pending') {
592
- const transaction = readManualValidationTransaction(resolved.taskDir, {
593
- transactionId: manual.transactionId,
594
- taskId: resolved.taskId,
595
- prNumber,
596
- prHeadSha: initial.value.head.sha
597
- });
598
- if (!transaction.ok) return fail('failed', context, platformError(transaction.error), prNumber);
599
- }
600
- if (manual.phase === 'final' && (!manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha)) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
601
- if (manual.phase === 'final') {
602
- if (manual.authority !== 'coordinator') return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires coordinator authority', retryable: false }, prNumber);
603
- const completion = readManualValidationCompletion(resolved.taskDir, {
604
- transactionId: manual.transactionId,
605
- taskId: resolved.taskId,
606
- prNumber,
607
- prHeadSha: initial.value.head.sha,
608
- receiptDigest: manual.receiptDigest
609
- });
610
- if (!completion.ok) return fail('failed', context, platformError(completion.error), prNumber);
611
- const { receipt, transaction } = completion.value;
612
- manualEvidenceDigest = receipt.evidenceDigest;
613
- if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
614
- if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', message: 'final summary body does not match the canonical receipt projection', retryable: false }, prNumber);
615
- }
616
- }
617
- const expectedReportPath = taskReportPath(resolved.taskDir);
618
- const suppliedReportPath = path.resolve(resolved.repoRoot, options.changeReportFile!);
619
- if (suppliedReportPath !== expectedReportPath) return fail('failed', context, { code: 'PR_CHANGE_REPORT_PATH_INVALID', message: 'summary-sync must consume the task-bound pr-change-report.json', retryable: false });
620
- const report = readPrChangeReport(suppliedReportPath);
621
- if (!report.ok) return fail('failed', context, platformError(report.error));
622
- const checked = currentReportCheck(report.value, initial.value, taskContent, resolved.repoRoot);
623
- if (!checked.ok) return fail('failed', context, checked.error);
624
- const placeholder = CANONICAL_REPORT_PLACEHOLDER;
625
- const split = splitDocumentPlaceholder(options.body, placeholder);
626
- if (!split.ok) return fail('failed', context, {
627
- code: 'PR_SUMMARY_BODY_CONTRACT_INVALID',
628
- message: split.error.message,
629
- retryable: false
630
- });
631
- const prefix = sanitizeMarkdownDocument(split.value.prefix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
632
- const suffix = sanitizeMarkdownDocument(split.value.suffix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
633
- if (!prefix.ok) {
634
- return fail('failed', context, {
635
- code: 'PR_SUMMARY_BODY_CONTRACT_INVALID',
636
- message: `${prefix.error.message} at offset ${prefix.error.offset}`,
637
- retryable: false
638
- });
639
- }
640
- if (!suffix.ok) {
641
- return fail('failed', context, {
642
- code: 'PR_SUMMARY_BODY_CONTRACT_INVALID',
643
- message: `${suffix.error.message} at offset ${suffix.error.offset}`,
644
- retryable: false
645
- });
646
- }
647
- const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
648
- if (!replaced.ok) return fail('failed', context, platformError(replaced.error));
649
- const renderedBody = manual?.phase === 'final'
650
- ? replaced.value.replace(
651
- MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
652
- `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
653
- )
654
- : replaced.value;
655
- const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
656
- if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
657
- code: 'PR_SUMMARY_RENDER_INVALID',
658
- message: 'Summary contains an invalid or duplicated reserved control marker',
659
- retryable: false
660
- });
661
- const listed = loaded.value.provider.comments?.list
662
- ? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: prIdentity }).then((response) => response.ok
663
- ? { ok: true as const, value: response.value.map((comment) => ({ id: /^\d+$/.test(comment.id) ? Number(comment.id) : comment.id, body: comment.body })) }
664
- : response)
665
- : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
666
- if (!listed.ok) return fail(providerStatus(listed.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'));
667
- const reconciliation = reconcileSummaryComment(listed.value, resolved.taskId, desired);
668
- if (reconciliation.action === 'conflict') return fail('failed', context, { code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple PR comments contain the summary marker', retryable: false });
669
- const previousBody = reconciliation.action === 'update'
670
- ? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
671
- : null;
672
- const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route } as const;
673
- const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
674
- if (!beforeWrite.ok) return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
675
- if (!sameIdentity(initial.value, beforeWrite.value)) return fail('blocked', context, {
676
- code: 'PR_SUMMARY_HEAD_RACE',
677
- message: 'Pull request head changed before publishing the summary',
678
- retryable: true
679
- }, prNumber);
680
- if (reconciliation.action === 'no-op') return {
681
- ...basePlatformResult('no-op', context, resolved.taskId, prNumber),
682
- comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: [reconciliation.commentId!], parts: 1 },
683
- result: null, warnings: [], ...info
684
- };
685
- if (options.dryRun) return {
686
- ...basePlatformResult('planned', context, resolved.taskId, prNumber),
687
- operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
688
- result: null, warnings: [], ...info
689
- };
690
- const publish = async (): Promise<PullRequestSummaryResult> => {
691
- const existingComment = reconciliation.commentId === null
692
- ? undefined
693
- : providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
694
- const written = loaded.value.provider.comments?.write
695
- ? await loaded.value.provider.comments.write({
696
- context: providerOperationContext(loaded.value),
697
- parent: prIdentity,
698
- body: desired,
699
- ...(existingComment ? { existingComment } : {}),
700
- mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
701
- })
702
- : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
703
- if (!written.ok) return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
704
- const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
705
- const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
706
- const compensateWrittenSummary = async () => reconciliation.action === 'create'
707
- ? written.value.remoteId && loaded.value.provider.comments?.delete
708
- ? await loaded.value.provider.comments.delete({
709
- context: providerOperationContext(loaded.value),
710
- parent: prIdentity,
711
- comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
712
- mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
713
- })
714
- : { ok: false as const, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
715
- : reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
716
- ? await loaded.value.provider.comments.write({
717
- context: providerOperationContext(loaded.value),
718
- parent: prIdentity,
719
- body: previousBody,
720
- existingComment: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId)),
721
- mutation: { idempotencyKey: `pr-summary-restore:${resolved.taskId}` }
722
- })
723
- : { ok: false as const, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Previous summary body is unavailable for restoration', retryable: true } };
724
- if (!after.ok) {
725
- const compensated = await compensateWrittenSummary();
726
- if (!compensated.ok) return fail('blocked', context, {
727
- code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
728
- message: `Summary post-write verification failed and compensation failed: ${compensated.error.message}`,
729
- retryable: true
730
- }, prNumber);
731
- return fail('blocked', context, {
732
- code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
733
- 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`,
734
- retryable: true
735
- }, prNumber);
736
- }
737
- if (!sameIdentity(initial.value, after.value)) {
738
- const compensated = await compensateWrittenSummary();
739
- if (!compensated.ok) return fail('blocked', context, {
740
- code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
741
- message: `Pull-request head changed and summary compensation failed: ${compensated.error.message}`,
742
- retryable: true
743
- }, prNumber);
744
- return fail('blocked', context, {
745
- code: 'PR_SUMMARY_HEAD_RACE',
746
- message: 'Pull-request head changed while publishing; the previous summary was restored or the new comment was deleted; rerun report and summary sync',
747
- retryable: true
748
- }, prNumber);
749
- }
750
- return {
751
- ...basePlatformResult('applied', context, resolved.taskId, prNumber),
752
- comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 },
753
- operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
754
- result: null, warnings: [], ...info
755
- };
756
- };
757
- const operation = {
758
- taskRef: resolved.taskId,
759
- cwd: resolved.repoRoot,
760
- kind: 'pull-request-summary' as const,
761
- target: JSON.stringify(prIdentity),
762
- expectedDigest: createHash('sha256').update(desired).digest('hex'),
763
- dependency: 'deferred' as const,
764
- pullRequestSummary: {
765
- body: options.body,
766
- changeReportFile: options.changeReportFile!,
767
- ...(manual?.transactionId ? { manualValidation: { ...manual, transactionId: manual.transactionId } } : {})
768
- }
769
- };
770
- if (options.skipQueue) return publish();
771
- return coordinatePlatformWrite({
772
- operation,
773
- agent: options.agent,
774
- execute: publish,
775
- block: (error) => fail('blocked', context, error, prNumber),
776
- persistenceFailure: (error) => fail('failed', context, error, prNumber)
777
- });
778
- };
779
- return await execute();
780
- } catch (error) {
781
- return fail('failed', context, { code: 'PR_SUMMARY_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false });
837
+ return preserveFailure(platformResult(options.dryRun ? 'blocked' : 'failed', { error: {
838
+ code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false
839
+ } }));
840
+ }
841
+
842
+ async function replayPullRequestSummary(taskId: string, operation: JournalOperation, options: { agent: string; cwd: string; client?: PlatformClient }): Promise<PlatformResult> {
843
+ const intent = operation.pullRequestSummary;
844
+ if (!intent) return platformResult('blocked', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary content is missing', retryable: false } });
845
+ let target: ResourceIdentity;
846
+ try { target = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request summary identity'); }
847
+ catch { return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } }); }
848
+ const bound = await inspectPlatformPullRequest(taskId, { cwd: options.cwd, client: options.client });
849
+ if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, target)) return platformResult('blocked', {
850
+ resource: bound.pullRequest ? { kind: 'pull-request', number: bound.pullRequest.number, identity: bound.pullRequest.identity } : undefined,
851
+ error: bound.error ?? { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request differs from the queued summary target', retryable: false }
852
+ });
853
+ const replayed = await syncPullRequestSummary(taskId, {
854
+ agent: options.agent, cwd: options.cwd, client: options.client, body: intent.body,
855
+ changeReportFile: intent.changeReportFile, ...(intent.manualValidation ? { manualValidation: intent.manualValidation } : {}),
856
+ primaryResult: 'no_op', strict: true, queuedOperation: operation
857
+ });
858
+ return (replayed.status === 'applied' || replayed.status === 'no-op') && !replayed.error && replayed.warnings.length === 0
859
+ ? replayed
860
+ : platformResult('blocked', { resource: replayed.resource, error: replayed.error ?? {
861
+ code: 'PR_SUMMARY_RECOVERY_UNCONFIRMED', message: 'Pull-request summary preparation or recovery did not confirm the frozen operation', retryable: true
862
+ } });
863
+ }
864
+
865
+ function correctPrSummaryContent(value: unknown, taskContent: string): unknown {
866
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return parsePrSummaryContent(value);
867
+ const input = { ...(value as Record<string, unknown>) };
868
+ const ledger = parseLedgerDocument(taskContent);
869
+ if (ledger.present) {
870
+ const decisions = ledger.rows.filter((row) => row.id.startsWith('HD-'));
871
+ const open = decisions.filter((row) => row.status === 'needs-human-decision').length;
872
+ const resolved = decisions.filter((row) => row.status === 'human-decided').length;
873
+ const current = input.humanDecisions && typeof input.humanDecisions === 'object' && !Array.isArray(input.humanDecisions)
874
+ ? { ...(input.humanDecisions as Record<string, unknown>) }
875
+ : {};
876
+ if (!Number.isSafeInteger(current.open) || Number(current.open) < 0) current.open = open;
877
+ if (!Number.isSafeInteger(current.resolved) || Number(current.resolved) < 0) current.resolved = resolved;
878
+ if (typeof current.detail !== 'string' || !current.detail.trim()) current.detail = `Task decision ledger: ${open} open, ${resolved} resolved.`;
879
+ input.humanDecisions = current;
782
880
  }
881
+ return input;
882
+ }
883
+
884
+ export function prepareAndRenderPrSummaryContent(value: unknown, taskContent: string) {
885
+ return runSummaryContentPipeline(value, {
886
+ validate: parsePrSummaryContent,
887
+ correct: (input) => correctPrSummaryContent(input, taskContent),
888
+ render: renderPrSummaryContent
889
+ });
783
890
  }
784
891
 
785
892
  export {
786
893
  buildPullRequestSummary,
787
- reconcileSummaryComment,
788
894
  reportWrite,
789
895
  summaryCommentState,
790
896
  summaryContext,
791
897
  summaryMarker,
792
898
  syncPullRequestSummary,
899
+ replayPullRequestSummary,
793
900
  warningResultForPrimary
794
901
  };
795
902
  export type { ManualValidationSummaryOptions, PullRequestSummaryResult, ReportWriteOptions, ReportWriteResult, SummaryCommentStateResult, SummaryContextResult };