@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
@@ -0,0 +1,18 @@
1
+ function parse(value, parser) {
2
+ try {
3
+ return parser(value);
4
+ }
5
+ catch (error) {
6
+ throw error instanceof Error ? error : new Error(String(error));
7
+ }
8
+ }
9
+ export function runSummaryContentPipeline(value, operations) {
10
+ try {
11
+ parse(value, operations.validate);
12
+ }
13
+ catch { /* Source correction gets a chance to repair invalid structure. */ }
14
+ const corrected = operations.correct(value);
15
+ const content = parse(corrected, operations.validate);
16
+ return { content, rendered: operations.render(content) };
17
+ }
18
+ //# sourceMappingURL=summary-content-pipeline.js.map
@@ -27,6 +27,40 @@ function isMissingPathError(error) {
27
27
  return Boolean(error && typeof error === 'object' && 'code' in error
28
28
  && error.code === 'ENOENT');
29
29
  }
30
+ function removalErrorCode(error) {
31
+ return error && typeof error === 'object' && 'code' in error
32
+ && typeof error.code === 'string'
33
+ ? error.code
34
+ : null;
35
+ }
36
+ function removalErrorMessage(error) {
37
+ const code = removalErrorCode(error);
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return code ? `${code}: ${message}` : message;
40
+ }
41
+ function addOwnerWriteToDirectories(root) {
42
+ const pending = [root];
43
+ while (pending.length > 0) {
44
+ const current = pending.pop();
45
+ let stat;
46
+ try {
47
+ stat = fs.lstatSync(current);
48
+ }
49
+ catch (error) {
50
+ if (isMissingPathError(error))
51
+ continue;
52
+ throw error;
53
+ }
54
+ if (!stat.isDirectory())
55
+ continue;
56
+ fs.chmodSync(current, stat.mode | 0o200);
57
+ for (const entry of fs.readdirSync(current))
58
+ pending.push(path.join(current, entry));
59
+ }
60
+ }
61
+ function isPermissionRemovalError(error) {
62
+ return ['EACCES', 'EPERM', 'ENOTEMPTY'].includes(removalErrorCode(error) ?? '');
63
+ }
30
64
  function canonicalPath(input) {
31
65
  try {
32
66
  return fs.realpathSync.native(input);
@@ -718,8 +752,28 @@ async function removeUncheckedSandbox(config, target, options) {
718
752
  fs.rmSync(root, { recursive: true, force: true });
719
753
  removeEmptyManagedParent(path.join(config.workspaceViewBase, config.project), root);
720
754
  }
721
- for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates))
722
- fs.rmSync(candidate, { recursive: true, force: true });
755
+ const toolRemovalFailures = [];
756
+ for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) {
757
+ try {
758
+ fs.rmSync(candidate, { recursive: true, force: true });
759
+ continue;
760
+ }
761
+ catch (initialError) {
762
+ if (isPermissionRemovalError(initialError)) {
763
+ try {
764
+ addOwnerWriteToDirectories(candidate);
765
+ fs.rmSync(candidate, { recursive: true, force: true });
766
+ continue;
767
+ }
768
+ catch (retryError) {
769
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}; retry failed: ${removalErrorMessage(retryError)}`);
770
+ }
771
+ }
772
+ else {
773
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}`);
774
+ }
775
+ }
776
+ }
723
777
  for (const shell of shellConfigDirCandidates(config, effectiveBranch))
724
778
  fs.rmSync(shell, { recursive: true, force: true });
725
779
  if (shouldRemoveShare)
@@ -736,6 +790,9 @@ async function removeUncheckedSandbox(config, target, options) {
736
790
  assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
737
791
  runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
738
792
  }
793
+ if (toolRemovalFailures.length > 0) {
794
+ throw new Error(`SANDBOX_TOOL_CLEANUP_INCOMPLETE:\n${toolRemovalFailures.map((failure) => ` - ${failure}`).join('\n')}`);
795
+ }
739
796
  releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
740
797
  if (!options.quiet)
741
798
  p.outro('Sandbox removed');
@@ -17,6 +17,7 @@ import { projectFinalizationWarning } from './workflow-warning-intents.js';
17
17
  import { mergeOperationWarnings } from './operation-outcome.js';
18
18
  import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
19
19
  import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
20
+ import { parseDeliverySummaryContent, renderDeliverySummaryContent } from '../platform/issue-summary-content.js';
20
21
  const RECEIPT_VERSION = 4;
21
22
  const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
22
23
  export function resolveTaskFinalizationDirectory(repoRoot, taskId) {
@@ -569,17 +570,25 @@ function readDeliverySummary(taskDir, taskId) {
569
570
  catch {
570
571
  throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
571
572
  }
572
- if (value.taskId !== taskId || typeof value.body !== 'string' || typeof value.sha256 !== 'string') {
573
+ if (value.taskId !== taskId || !value.content || typeof value.contentSha256 !== 'string') {
573
574
  throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
574
575
  }
575
- const canonical = canonicalizeSummaryBody(value.body);
576
- if (!canonical.ok || canonical.value !== value.body) {
577
- throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
576
+ let content;
577
+ try {
578
+ content = parseDeliverySummaryContent(value.content);
579
+ }
580
+ catch {
581
+ throw Object.assign(new Error('delivery summary staging payload is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
582
+ }
583
+ if (content.taskId !== taskId || createHash('sha256').update(JSON.stringify(content)).digest('hex') !== value.contentSha256) {
584
+ throw Object.assign(new Error('delivery summary staging digest does not match payload'), { code: 'SUMMARY_STAGING_INVALID' });
578
585
  }
586
+ const body = renderDeliverySummaryContent(content);
587
+ const canonical = canonicalizeSummaryBody(body);
588
+ if (!canonical.ok)
589
+ throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
579
590
  const digest = createHash('sha256').update(canonical.value).digest('hex');
580
- if (digest !== value.sha256)
581
- throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
582
- return { body: canonical.value, sha256: value.sha256 };
591
+ return { body: canonical.value, sha256: digest };
583
592
  }
584
593
  async function syncPendingSummary(input) {
585
594
  let { receipt } = input;
@@ -2,13 +2,15 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { execFileSync } from 'node:child_process';
5
- import { inspectPlatformCommentOperation, normalizeCommentContent, syncPlatformComment } from '../platform/issue-comments.js';
5
+ import { inspectPlatformCommentOperation, syncPlatformComment } from '../platform/issue-comments.js';
6
+ import { parseDeliverySummaryContent, renderDeliverySummaryContent, parseCancelSummaryContent, renderCancelSummaryContent } from '../platform/issue-summary-content.js';
7
+ import { enumerateArtifacts } from './artifacts.js';
6
8
  import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
7
9
  import { resolvePlatformProviderContext } from '../platform/context.js';
8
10
  import { providerOperationContext, providerError, unsupportedProviderOperation } from '../platform/provider-bridge.js';
9
11
  import { inLabelMappingDigest } from '../platform/in-label-sync.js';
10
- import { bindPlatformPullRequest, inspectPlatformPullRequest, recoverCreatedPullRequest, syncPlatformPullRequest } from '../platform/pull-requests.js';
11
- import { syncPullRequestSummary } from '../platform/pr-summary.js';
12
+ import { bindPlatformPullRequest, recoverCreatedPullRequest, syncPlatformPullRequest } from '../platform/pull-requests.js';
13
+ import { replayPullRequestSummary } from '../platform/pr-summary.js';
12
14
  import { publishPrReview, readReviewBodyFile } from '../platform/pr-review.js';
13
15
  import { parseTaskFrontmatter } from './frontmatter.js';
14
16
  import { taskIssueIdentity } from '../platform/task-identities.js';
@@ -35,32 +37,70 @@ function summaryPayload(taskDir, taskId) {
35
37
  if (!fs.existsSync(file))
36
38
  return null;
37
39
  const data = JSON.parse(fs.readFileSync(file, 'utf8'));
38
- const canonical = typeof data.body === 'string' ? canonicalizeSummaryBody(data.body) : null;
39
- if (data.taskId !== taskId || typeof data.body !== 'string' || typeof data.sha256 !== 'string'
40
- || !canonical?.ok || canonical.value !== data.body
41
- || createHash('sha256').update(data.body).digest('hex') !== data.sha256) {
40
+ let content;
41
+ try {
42
+ content = parseDeliverySummaryContent(data.content);
43
+ }
44
+ catch {
45
+ content = null;
46
+ }
47
+ if (data.taskId !== taskId || !content || content.taskId !== taskId || typeof data.contentSha256 !== 'string'
48
+ || createHash('sha256').update(JSON.stringify(content)).digest('hex') !== data.contentSha256) {
42
49
  throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
43
50
  }
44
- return { body: data.body, sha256: data.sha256 };
51
+ const canonical = canonicalizeSummaryBody(renderDeliverySummaryContent(content));
52
+ if (!canonical?.ok)
53
+ throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
54
+ return { body: canonical.value, sha256: createHash('sha256').update(canonical.value).digest('hex') };
55
+ }
56
+ function cancellationPayload(taskDir, taskId) {
57
+ const taskPath = path.join(taskDir, 'task.md');
58
+ if (!fs.existsSync(taskPath))
59
+ return null;
60
+ const taskContent = fs.readFileSync(taskPath, 'utf8');
61
+ const frontmatter = parseTaskFrontmatter(taskContent);
62
+ const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim() || taskId;
63
+ const artifacts = enumerateArtifacts(taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name);
64
+ const content = parseCancelSummaryContent({
65
+ taskId,
66
+ title,
67
+ outcome: 'cancelled',
68
+ reason: frontmatter.cancel_reason,
69
+ cancelledAt: frontmatter.cancelled_at,
70
+ lastCompletedArtifact: artifacts.at(-1) ?? null
71
+ });
72
+ return renderCancelSummaryContent(content);
45
73
  }
46
74
  async function replayComment(taskId, taskDir, operation, agent, cwd, client) {
47
75
  if (operation.kind === 'task-comment') {
48
- return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency, skipQueue: true });
76
+ return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
49
77
  }
50
78
  if (operation.kind === 'artifact-comment') {
51
- return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency, skipQueue: true });
79
+ return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
52
80
  }
53
81
  if (operation.kind === 'summary-comment') {
54
- const summary = summaryPayload(taskDir, taskId);
82
+ let summary;
83
+ try {
84
+ summary = summaryPayload(taskDir, taskId);
85
+ }
86
+ catch (error) {
87
+ return platformResult('failed', { error: { code: 'SUMMARY_STAGING_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
88
+ }
55
89
  if (!summary)
56
- return platformResult('failed', { error: { code: 'SUMMARY_STAGING_MISSING', message: 'delivery summary staging record is unavailable', retryable: false } });
57
- return syncPlatformComment(taskId, {
58
- kind: 'summary', body: summary.body, agent, cwd,
59
- summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, client, skipQueue: true
60
- });
90
+ return platformResult('blocked', { error: { code: 'SUMMARY_STAGING_MISSING', message: 'delivery summary staging record is unavailable', retryable: true } });
91
+ return syncPlatformComment(taskId, { kind: 'summary', body: summary.body, agent, cwd, client, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, queuedOperation: operation });
61
92
  }
62
93
  if (operation.kind === 'cancel-comment') {
63
- return syncPlatformComment(taskId, { kind: 'cancel', agent, cwd, client, dependency: operation.dependency, skipQueue: true });
94
+ let body;
95
+ try {
96
+ body = cancellationPayload(taskDir, taskId);
97
+ }
98
+ catch (error) {
99
+ return platformResult('failed', { error: { code: 'CANCEL_SUMMARY_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
100
+ }
101
+ if (!body)
102
+ return platformResult('blocked', { error: { code: 'CANCEL_SUMMARY_INVALID', message: 'Cancellation facts are unavailable for recovery', retryable: true } });
103
+ return syncPlatformComment(taskId, { kind: 'cancel', body, agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
64
104
  }
65
105
  return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_UNSUPPORTED', message: `operation kind '${operation.kind}' has no registered recovery handler`, retryable: false } });
66
106
  }
@@ -185,64 +225,6 @@ async function verifyIssueCreate(taskId, operation, cwd, client) {
185
225
  } });
186
226
  return platformResult('no-op');
187
227
  }
188
- async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
189
- let identity;
190
- try {
191
- identity = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request summary identity');
192
- }
193
- catch {
194
- return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } });
195
- }
196
- const summary = operation.pullRequestSummary;
197
- if (!summary)
198
- return platformResult('blocked', { error: {
199
- code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
200
- } });
201
- const manualValidationHeading = /^###\s+(?:⏳\s+(?:Manual Validation Pending|人工验证待收尾)|✅\s+(?:Manual Validation Passed|人工验证已通过))\s*$/mu.test(summary.body);
202
- if (manualValidationHeading && !summary.manualValidation)
203
- return platformResult('blocked', { error: {
204
- code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'Queued manual-validation summary has no persisted transaction context', retryable: false
205
- } });
206
- const resolved = await resolvePlatformProviderContext({ cwd, client });
207
- if (!resolved.ok)
208
- return resolved.context;
209
- const listed = resolved.value.provider.comments?.list
210
- ? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
211
- : unsupportedProviderOperation(resolved.value.provider, 'comments.list');
212
- if (!listed.ok)
213
- return platformResult(providerErrorRetryable(listed.error.code) ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
214
- const marker = `<!-- sync-pr:${taskId}:summary -->`;
215
- const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
216
- if (summaries.length > 1)
217
- return platformResult('blocked', { error: {
218
- code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple pull-request comments contain the queued summary marker', retryable: false
219
- } });
220
- if (!summary.manualValidation && summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
221
- return platformResult('no-op');
222
- }
223
- const bound = await inspectPlatformPullRequest(taskId, { cwd, client });
224
- if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, identity))
225
- return platformResult('blocked', { error: bound.error ?? {
226
- code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request differs from the queued summary target', retryable: false
227
- } });
228
- const replayed = await syncPullRequestSummary(taskId, {
229
- agent,
230
- cwd,
231
- client,
232
- body: summary.body,
233
- changeReportFile: summary.changeReportFile,
234
- ...(summary.manualValidation ? { manualValidation: summary.manualValidation } : {}),
235
- primaryResult: 'no_op',
236
- strict: true,
237
- skipQueue: true
238
- });
239
- if ((replayed.status === 'applied' || replayed.status === 'no-op') && !replayed.error && replayed.warnings.length === 0) {
240
- return platformResult(replayed.status, { changed: replayed.changed });
241
- }
242
- return platformResult('blocked', { error: replayed.error ?? {
243
- code: 'PR_SUMMARY_RECOVERY_UNCONFIRMED', message: 'Pull-request summary replay did not confirm the requested remote state', retryable: true
244
- } });
245
- }
246
228
  async function replayPullRequestReview(operation, agent, cwd, client) {
247
229
  const intent = operation.pullRequestReview;
248
230
  if (!intent)
@@ -301,6 +283,14 @@ async function replayPullRequest(taskId, operation, agent, cwd) {
301
283
  }
302
284
  return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
303
285
  }
286
+ function isRecoveryEligible(operation, excludeId) {
287
+ if (operation.id === excludeId)
288
+ return false;
289
+ if (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown')
290
+ return true;
291
+ return operation.state === 'failed'
292
+ && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED';
293
+ }
304
294
  async function recoverPlatformOperationsOnce(taskRef, selection, options) {
305
295
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
306
296
  if (!resolved.ok)
@@ -313,9 +303,7 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
313
303
  const value = error;
314
304
  return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
315
305
  }
316
- const candidates = journal.operations.filter((operation) => operation.id !== options.excludeId
317
- && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
318
- || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')));
306
+ const candidates = journal.operations.filter((operation) => isRecoveryEligible(operation, options.excludeId));
319
307
  if (!candidates.length)
320
308
  return result('no-op', [], []);
321
309
  const head = candidates[0];
@@ -382,9 +370,22 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
382
370
  : operation.kind === 'artifact-comment'
383
371
  ? { kind: 'artifact', artifact: operation.target, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
384
372
  : operation.kind === 'summary-comment' ? (() => {
385
- const summary = summaryPayload(resolved.taskDir, resolved.taskId);
386
- return summary ? { kind: 'summary', body: summary.body, agent: options.agent, cwd: resolved.repoRoot, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency } : null;
387
- })() : { kind: 'cancel', agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency };
373
+ try {
374
+ const summary = summaryPayload(resolved.taskDir, resolved.taskId);
375
+ return summary ? { kind: 'summary', body: summary.body, agent: options.agent, cwd: resolved.repoRoot, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency } : null;
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ })() : (() => {
381
+ try {
382
+ const body = cancellationPayload(resolved.taskDir, resolved.taskId);
383
+ return body ? { kind: 'cancel', body, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency } : null;
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ })();
388
389
  const current = commentOptions ? inspectPlatformCommentOperation(resolved.taskId, commentOptions) : null;
389
390
  if (!current) {
390
391
  pending.push(operation.id);
@@ -407,20 +408,21 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
407
408
  remote = operation.kind === 'issue-create'
408
409
  ? await verifyIssueCreate(resolved.taskId, operation, resolved.repoRoot)
409
410
  : operation.kind === 'pull-request-summary'
410
- ? await replayPullRequestSummary(resolved.taskId, operation, options.agent, resolved.repoRoot, options.client)
411
+ ? await replayPullRequestSummary(resolved.taskId, targetOperation, { agent: options.agent, cwd: resolved.repoRoot, client: options.client })
411
412
  : operation.kind === 'pull-request-review'
412
413
  ? await replayPullRequestReview(operation, options.agent, resolved.repoRoot, options.client)
413
414
  : operation.kind === 'issue-metadata'
414
415
  ? await replayIssueMetadata(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
415
416
  : operation.kind === 'pull-request'
416
417
  ? await replayPullRequest(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
417
- : await replayComment(resolved.taskId, resolved.taskDir, operation, options.agent, resolved.repoRoot);
418
+ : await replayComment(resolved.taskId, resolved.taskDir, targetOperation, options.agent, resolved.repoRoot, options.client);
418
419
  }
419
420
  catch (error) {
420
421
  const value = error;
421
422
  remote = platformResult('blocked', { error: { code: value.code || 'PLATFORM_OPERATION_RECOVERY_FAILED', message: value.message || String(error), retryable: true } });
422
423
  }
423
424
  const succeeded = (remote.status === 'applied' || remote.status === 'no-op') && !remote.error;
425
+ const latestOperation = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot).operations.find((item) => item.id === operation.id);
424
426
  try {
425
427
  recordPlatformOperation({
426
428
  taskRef: resolved.taskId,
@@ -435,7 +437,7 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
435
437
  ...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
436
438
  dependency: operation.dependency,
437
439
  state: succeeded ? 'succeeded' : 'unknown',
438
- lastCode: remote.error?.code ?? null
440
+ lastCode: remote.error?.code ?? latestOperation?.lastCode ?? null
439
441
  });
440
442
  }
441
443
  catch (error) {
@@ -464,9 +466,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
464
466
  const value = error;
465
467
  return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
466
468
  }
467
- const eligible = (operation) => operation.id !== options.excludeId
468
- && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
469
- || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'));
469
+ const eligible = (operation) => isRecoveryEligible(operation, options.excludeId);
470
470
  const first = initial.operations.filter(eligible);
471
471
  if (!first.length)
472
472
  return result('no-op', [], []);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.12.0",
4
- "sourceInputHash": "c0a3da802ab0d992591d4ddd191550627992e7f76fea444bab99f342bd59ec18",
5
- "internalExecutableBuildHash": "536ccdede573b4204982cb184b9b0f5ad8c3f943f3d1e749b42ed05bbcca9ead"
3
+ "packageVersion": "0.12.1",
4
+ "sourceInputHash": "b998fe9d348d3f7f55e1258d2d09793f09240d1d70173d6bce49a9ce6c89429e",
5
+ "internalExecutableBuildHash": "d75b6e06c5ed88b0cd03663d98e2001c3f5c661d9637b8e6b8993580f69a3f16"
6
6
  }
@@ -3,9 +3,10 @@ import path from 'node:path';
3
3
 
4
4
  import { locateActivityLog, pairEntries } from '../task/activity-log.ts';
5
5
  import { resolveTaskRef } from '../task/resolve-ref.ts';
6
+ import { prepareAndRenderPrSummaryContent } from '../platform/pr-summary.ts';
6
7
  import { applyTaskEvent } from '../task/events.ts';
7
8
  import { normalizeAgentToken } from '../agent-clients/tokens.ts';
8
- import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
9
+ import { createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
9
10
  import { archiveManualValidationGeneration, createManualValidationTransaction, readManualValidationTransaction, retryManualValidationTransaction, summaryPreimageDigest, transitionManualValidationTransaction, validateManualValidationGenerationArchive, writeManualValidationTransactionAtomic } from '../task/manual-validation-transaction.ts';
10
11
  import type { ManualValidationTransaction } from '../task/manual-validation-transaction.ts';
11
12
  import type { ManualValidationReceipt } from '../task/manual-validation-receipt.ts';
@@ -42,23 +43,15 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
42
43
  process.exitCode = 1;
43
44
  }
44
45
 
45
- const MANUAL_VALIDATION_STATUS_HEADING = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$/mu;
46
-
47
46
  function manualSummaryBody(body: string, phase: 'pending' | 'final'): string {
48
- const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
49
- const heading = phase === 'pending'
50
- ? chinese ? '### ⏳ 人工验证待收尾' : '### ⏳ Manual Validation Pending'
51
- : chinese ? '### ✅ 人工验证已通过' : '### ✅ Manual Validation Passed';
52
- const marker = phase === 'final'
53
- ? `\n\n${MANUAL_VALIDATION_RECEIPT_PLACEHOLDER}`
54
- : '';
55
- if (MANUAL_VALIDATION_STATUS_HEADING.test(body)) {
56
- return `${body.replace(MANUAL_VALIDATION_STATUS_HEADING, `${heading}${marker}`).replace(/\s+$/u, '')}\n`;
57
- }
58
- const fallback = phase === 'pending'
59
- ? chinese ? '人工验证正在等待最终完成。' : 'Manual validation is awaiting final promotion.'
60
- : chinese ? '人工验证已通过。' : 'Manual validation passed.';
61
- return `${body.replace(/\s+$/u, '')}\n\n${heading}${marker}\n\n${fallback}\n`;
47
+ const content = JSON.parse(body) as Record<string, unknown>;
48
+ const manual = content.manualValidation as Record<string, unknown>;
49
+ manual.state = phase === 'pending' ? 'pending' : 'passed';
50
+ return JSON.stringify(content);
51
+ }
52
+
53
+ function renderedManualSummaryBody(body: string, phase: 'pending' | 'final', taskContent: string): string {
54
+ return prepareAndRenderPrSummaryContent(JSON.parse(manualSummaryBody(body, phase)), taskContent).rendered;
62
55
  }
63
56
 
64
57
  function openManualValidationStarted(taskMdPath: string): { present: boolean; transactionId: string | null } {
@@ -112,6 +105,7 @@ async function executeManualValidationTransactionAtResolvedTask(
112
105
  const preimage = { commentId: currentState.comment?.id ?? null, body: preimageBody, digest: summaryPreimageDigest(preimageBody) };
113
106
  const summaryPath = path.resolve(cwd, values.summaryFile!);
114
107
  const finalBodyWithoutReceipt = fs.readFileSync(summaryPath, 'utf8');
108
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
115
109
  const evidenceDigest = sha256File(summaryPath);
116
110
  const archiveGeneration = options.archiveGeneration ?? archiveManualValidationGeneration;
117
111
  let transactionResult = readManualValidationTransaction(resolved.taskDir, { taskId: resolved.taskId, prNumber: pullRequest.number, prHeadSha: pullRequest.head.sha, evidenceDigest, artifact: values.artifact! });
@@ -146,7 +140,7 @@ async function executeManualValidationTransactionAtResolvedTask(
146
140
  evidenceDigest,
147
141
  summaryPreimage: preimage,
148
142
  pendingSummaryDigest: summaryPreimageDigest(pendingBody),
149
- finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final')),
143
+ finalSummaryDigest: manualValidationFinalSummaryDigest(renderedManualSummaryBody(finalBodyWithoutReceipt, 'final', taskContent)),
150
144
  artifact: values.artifact!,
151
145
  attempt: 1,
152
146
  createdAt: new Date().toISOString(),
@@ -17,6 +17,9 @@ import { recoverTaskFromComments } from '../platform/task-recovery.ts';
17
17
  import { backfillCompletionComments } from '../platform/completion-backfill.ts';
18
18
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
19
19
  import { resolveTaskRef } from '../task/resolve-ref.ts';
20
+ import { enumerateArtifacts } from '../task/artifacts.ts';
21
+ import { parseTaskFrontmatter } from '../task/frontmatter.ts';
22
+ import { prepareAndRenderCancelSummaryContent, prepareAndRenderDeliverySummaryContent } from '../platform/issue-summary-content.ts';
20
23
 
21
24
  const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
22
25
  agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
@@ -158,13 +161,26 @@ async function platformComment(args: string[] = []): Promise<void> {
158
161
  let body: string;
159
162
  try { body = readBodyFile(parsed.values.bodyFile, cwd); }
160
163
  catch (error) { fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`); return; }
161
- const canonical = canonicalizeSummaryBody(body);
162
- if (!canonical.ok) { fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`); return; }
163
- body = canonical.value;
164
164
  const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
165
165
  if (!resolved.ok || resolved.state !== 'active') { fail('stage-summary requires an active task'); return; }
166
166
  const target = path.join(resolved.taskDir, '.delivery-summary.json');
167
- const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
167
+ let content;
168
+ try {
169
+ const supplied = JSON.parse(body) as Record<string, unknown>;
170
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
171
+ const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim();
172
+ const prepared = prepareAndRenderDeliverySummaryContent(supplied, {
173
+ taskId: resolved.taskId,
174
+ title: title || resolved.taskId,
175
+ artifacts: enumerateArtifacts(resolved.taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name),
176
+ verificationFallback: { state: 'not-run', detail: 'Final verification has not run at summary staging.' }
177
+ });
178
+ content = prepared.content;
179
+ const canonical = canonicalizeSummaryBody(prepared.rendered);
180
+ if (!canonical.ok) throw new Error(`${canonical.error.code}: ${canonical.error.message}`);
181
+ body = canonical.value;
182
+ } catch (error) { fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`); return; }
183
+ const value = { taskId: resolved.taskId, content, contentSha256: createHash('sha256').update(JSON.stringify(content)).digest('hex') };
168
184
  const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
169
185
  const next = `${JSON.stringify(value)}\n`;
170
186
  if (previous === next) {
@@ -199,6 +215,32 @@ async function platformComment(args: string[] = []): Promise<void> {
199
215
  fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
200
216
  return;
201
217
  }
218
+ if (kind === 'summary' || kind === 'cancel') {
219
+ try {
220
+ const input = JSON.parse(body!) as Record<string, unknown>;
221
+ const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
222
+ if (!resolved.ok) throw new Error(resolved.message);
223
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
224
+ const taskFrontmatter = parseTaskFrontmatter(taskContent);
225
+ const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim();
226
+ const artifacts = enumerateArtifacts(resolved.taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name);
227
+ const rendered = kind === 'summary'
228
+ ? prepareAndRenderDeliverySummaryContent(input, {
229
+ taskId: resolved.taskId,
230
+ title: title || resolved.taskId,
231
+ artifacts,
232
+ ...(resolved.state === 'active' ? { verificationFallback: { state: 'not-run', detail: 'Final verification has not run at summary staging.' } } : {})
233
+ }).rendered
234
+ : prepareAndRenderCancelSummaryContent(input, {
235
+ taskId: resolved.taskId,
236
+ title: title || resolved.taskId,
237
+ reason: typeof taskFrontmatter.cancel_reason === 'string' ? taskFrontmatter.cancel_reason : '',
238
+ cancelledAt: typeof taskFrontmatter.cancelled_at === 'string' ? taskFrontmatter.cancelled_at : '',
239
+ lastCompletedArtifact: artifacts.at(-1) ?? null
240
+ }).rendered;
241
+ body = rendered;
242
+ } catch (error) { fail(`invalid ${kind} summary structure: ${error instanceof Error ? error.message : String(error)}`); return; }
243
+ }
202
244
  finish(await syncPlatformComment(taskRef!, {
203
245
  kind: kind as CommentKind,
204
246
  agent: normalizedAgent,