@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.
- package/dist/lib/internal/manual-validation.js +11 -17
- package/dist/lib/internal/platform-comment.js +56 -7
- package/dist/lib/platform/issue-comments.js +373 -267
- package/dist/lib/platform/issue-summary-content.js +112 -0
- package/dist/lib/platform/pr-change-report.js +175 -26
- package/dist/lib/platform/pr-summary-content.js +54 -0
- package/dist/lib/platform/pr-summary.js +318 -284
- package/dist/lib/platform/summary-content-pipeline.js +18 -0
- package/dist/lib/sandbox/removal.js +59 -2
- package/dist/lib/task/finalization.js +16 -7
- package/dist/lib/task/platform-operation-recovery.js +87 -87
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/manual-validation.ts +12 -18
- package/lib/internal/platform-comment.ts +46 -4
- package/lib/platform/issue-comments.ts +478 -280
- package/lib/platform/issue-summary-content.ts +132 -0
- package/lib/platform/pr-change-report.ts +165 -20
- package/lib/platform/pr-summary-content.ts +60 -0
- package/lib/platform/pr-summary.ts +364 -257
- package/lib/platform/summary-content-pipeline.ts +23 -0
- package/lib/sandbox/removal.ts +57 -1
- package/lib/task/finalization.ts +11 -6
- package/lib/task/platform-operation-recovery.ts +70 -81
- package/package.json +1 -1
- package/templates/.agents/rules/pr-sync.en.md +4 -2
- package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
- package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
- 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 {
|
|
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,
|
|
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:
|
|
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
|
-
:
|
|
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
|
|
546
|
-
if (!
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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 };
|