@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,23 @@
1
+ export type SummaryContentOperations<T> = Readonly<{
2
+ validate: (value: unknown) => T;
3
+ correct: (value: unknown) => unknown;
4
+ render: (value: T) => string;
5
+ }>;
6
+
7
+ function parse<T>(value: unknown, parser: (value: unknown) => T): T {
8
+ try {
9
+ return parser(value);
10
+ } catch (error) {
11
+ throw error instanceof Error ? error : new Error(String(error));
12
+ }
13
+ }
14
+
15
+ export function runSummaryContentPipeline<T>(
16
+ value: unknown,
17
+ operations: SummaryContentOperations<T>
18
+ ): Readonly<{ content: T; rendered: string }> {
19
+ try { parse(value, operations.validate); } catch { /* Source correction gets a chance to repair invalid structure. */ }
20
+ const corrected = operations.correct(value);
21
+ const content = parse(corrected, operations.validate);
22
+ return { content, rendered: operations.render(content) };
23
+ }
@@ -73,6 +73,41 @@ function isMissingPathError(error: unknown): boolean {
73
73
  && (error as { code?: unknown }).code === 'ENOENT');
74
74
  }
75
75
 
76
+ function removalErrorCode(error: unknown): string | null {
77
+ return error && typeof error === 'object' && 'code' in error
78
+ && typeof (error as { code?: unknown }).code === 'string'
79
+ ? (error as { code: string }).code
80
+ : null;
81
+ }
82
+
83
+ function removalErrorMessage(error: unknown): string {
84
+ const code = removalErrorCode(error);
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ return code ? `${code}: ${message}` : message;
87
+ }
88
+
89
+ function addOwnerWriteToDirectories(root: string): void {
90
+ const pending = [root];
91
+ while (pending.length > 0) {
92
+ const current = pending.pop()!;
93
+ let stat: fs.Stats;
94
+ try {
95
+ stat = fs.lstatSync(current);
96
+ } catch (error) {
97
+ if (isMissingPathError(error)) continue;
98
+ throw error;
99
+ }
100
+ if (!stat.isDirectory()) continue;
101
+
102
+ fs.chmodSync(current, stat.mode | 0o200);
103
+ for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
104
+ }
105
+ }
106
+
107
+ function isPermissionRemovalError(error: unknown): boolean {
108
+ return ['EACCES', 'EPERM', 'ENOTEMPTY'].includes(removalErrorCode(error) ?? '');
109
+ }
110
+
76
111
  function canonicalPath(input: string): string {
77
112
  try { return fs.realpathSync.native(input); }
78
113
  catch { return path.resolve(input); }
@@ -1011,7 +1046,25 @@ async function removeUncheckedSandbox(
1011
1046
  fs.rmSync(root, { recursive: true, force: true });
1012
1047
  removeEmptyManagedParent(path.join(config.workspaceViewBase, config.project), root);
1013
1048
  }
1014
- for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) fs.rmSync(candidate, { recursive: true, force: true });
1049
+ const toolRemovalFailures: string[] = [];
1050
+ for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) {
1051
+ try {
1052
+ fs.rmSync(candidate, { recursive: true, force: true });
1053
+ continue;
1054
+ } catch (initialError) {
1055
+ if (isPermissionRemovalError(initialError)) {
1056
+ try {
1057
+ addOwnerWriteToDirectories(candidate);
1058
+ fs.rmSync(candidate, { recursive: true, force: true });
1059
+ continue;
1060
+ } catch (retryError) {
1061
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}; retry failed: ${removalErrorMessage(retryError)}`);
1062
+ }
1063
+ } else {
1064
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}`);
1065
+ }
1066
+ }
1067
+ }
1015
1068
  for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
1016
1069
  if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
1017
1070
  if (shouldRemoveWorktree) {
@@ -1025,6 +1078,9 @@ async function removeUncheckedSandbox(
1025
1078
  assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
1026
1079
  runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
1027
1080
  }
1081
+ if (toolRemovalFailures.length > 0) {
1082
+ throw new Error(`SANDBOX_TOOL_CLEANUP_INCOMPLETE:\n${toolRemovalFailures.map((failure) => ` - ${failure}`).join('\n')}`);
1083
+ }
1028
1084
  releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
1029
1085
 
1030
1086
  if (!options.quiet) p.outro('Sandbox removed');
@@ -33,6 +33,7 @@ import {
33
33
  } from './operation-outcome.ts';
34
34
  import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
35
35
  import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.ts';
36
+ import { parseDeliverySummaryContent, renderDeliverySummaryContent } from '../platform/issue-summary-content.ts';
36
37
 
37
38
  const RECEIPT_VERSION = 4 as const;
38
39
  const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
@@ -730,16 +731,20 @@ function readDeliverySummary(taskDir: string, taskId: string): { body: string; s
730
731
  } catch {
731
732
  throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
732
733
  }
733
- if (value.taskId !== taskId || typeof value.body !== 'string' || typeof value.sha256 !== 'string') {
734
+ if (value.taskId !== taskId || !value.content || typeof value.contentSha256 !== 'string') {
734
735
  throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
735
736
  }
736
- const canonical = canonicalizeSummaryBody(value.body);
737
- if (!canonical.ok || canonical.value !== value.body) {
738
- throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
737
+ let content;
738
+ try { content = parseDeliverySummaryContent(value.content); }
739
+ catch { throw Object.assign(new Error('delivery summary staging payload is invalid'), { code: 'SUMMARY_STAGING_INVALID' }); }
740
+ if (content.taskId !== taskId || createHash('sha256').update(JSON.stringify(content)).digest('hex') !== value.contentSha256) {
741
+ throw Object.assign(new Error('delivery summary staging digest does not match payload'), { code: 'SUMMARY_STAGING_INVALID' });
739
742
  }
743
+ const body = renderDeliverySummaryContent(content);
744
+ const canonical = canonicalizeSummaryBody(body);
745
+ if (!canonical.ok) throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
740
746
  const digest = createHash('sha256').update(canonical.value).digest('hex');
741
- if (digest !== value.sha256) throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
742
- return { body: canonical.value, sha256: value.sha256 };
747
+ return { body: canonical.value, sha256: digest };
743
748
  }
744
749
 
745
750
  async function syncPendingSummary(input: {
@@ -3,13 +3,15 @@ import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { execFileSync } from 'node:child_process';
5
5
 
6
- import { inspectPlatformCommentOperation, normalizeCommentContent, syncPlatformComment } from '../platform/issue-comments.ts';
6
+ import { inspectPlatformCommentOperation, syncPlatformComment } from '../platform/issue-comments.ts';
7
+ import { parseDeliverySummaryContent, renderDeliverySummaryContent, parseCancelSummaryContent, renderCancelSummaryContent } from '../platform/issue-summary-content.ts';
8
+ import { enumerateArtifacts } from './artifacts.ts';
7
9
  import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
8
10
  import { resolvePlatformProviderContext } from '../platform/context.ts';
9
11
  import { providerOperationContext, providerError, unsupportedProviderOperation } from '../platform/provider-bridge.ts';
10
12
  import { inLabelMappingDigest } from '../platform/in-label-sync.ts';
11
- import { bindPlatformPullRequest, inspectPlatformPullRequest, recoverCreatedPullRequest, syncPlatformPullRequest } from '../platform/pull-requests.ts';
12
- import { syncPullRequestSummary } from '../platform/pr-summary.ts';
13
+ import { bindPlatformPullRequest, recoverCreatedPullRequest, syncPlatformPullRequest } from '../platform/pull-requests.ts';
14
+ import { replayPullRequestSummary } from '../platform/pr-summary.ts';
13
15
  import { publishPrReview, readReviewBodyFile } from '../platform/pr-review.ts';
14
16
  import type { PlatformClient } from '../platform/context.ts';
15
17
  import { parseTaskFrontmatter } from './frontmatter.ts';
@@ -53,33 +55,57 @@ function result(status: RecoveryResult['status'], recovered: string[], pending:
53
55
  function summaryPayload(taskDir: string, taskId: string): { body: string; sha256: string } | null {
54
56
  const file = path.join(taskDir, '.delivery-summary.json');
55
57
  if (!fs.existsSync(file)) return null;
56
- const data = JSON.parse(fs.readFileSync(file, 'utf8')) as { taskId?: unknown; body?: unknown; sha256?: unknown };
57
- const canonical = typeof data.body === 'string' ? canonicalizeSummaryBody(data.body) : null;
58
- if (data.taskId !== taskId || typeof data.body !== 'string' || typeof data.sha256 !== 'string'
59
- || !canonical?.ok || canonical.value !== data.body
60
- || createHash('sha256').update(data.body).digest('hex') !== data.sha256) {
58
+ const data = JSON.parse(fs.readFileSync(file, 'utf8')) as { taskId?: unknown; content?: unknown; contentSha256?: unknown };
59
+ let content;
60
+ try { content = parseDeliverySummaryContent(data.content); }
61
+ catch { content = null; }
62
+ if (data.taskId !== taskId || !content || content.taskId !== taskId || typeof data.contentSha256 !== 'string'
63
+ || createHash('sha256').update(JSON.stringify(content)).digest('hex') !== data.contentSha256) {
61
64
  throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
62
65
  }
63
- return { body: data.body, sha256: data.sha256 };
66
+ const canonical = canonicalizeSummaryBody(renderDeliverySummaryContent(content));
67
+ if (!canonical?.ok) throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
68
+ return { body: canonical.value, sha256: createHash('sha256').update(canonical.value).digest('hex') };
69
+ }
70
+
71
+ function cancellationPayload(taskDir: string, taskId: string): string | null {
72
+ const taskPath = path.join(taskDir, 'task.md');
73
+ if (!fs.existsSync(taskPath)) return null;
74
+ const taskContent = fs.readFileSync(taskPath, 'utf8');
75
+ const frontmatter = parseTaskFrontmatter(taskContent);
76
+ const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim() || taskId;
77
+ const artifacts = enumerateArtifacts(taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name);
78
+ const content = parseCancelSummaryContent({
79
+ taskId,
80
+ title,
81
+ outcome: 'cancelled',
82
+ reason: frontmatter.cancel_reason,
83
+ cancelledAt: frontmatter.cancelled_at,
84
+ lastCompletedArtifact: artifacts.at(-1) ?? null
85
+ });
86
+ return renderCancelSummaryContent(content);
64
87
  }
65
88
 
66
89
  async function replayComment(taskId: string, taskDir: string, operation: ReturnType<typeof readPlatformOperationJournal>['operations'][number], agent: string, cwd: string, client?: PlatformClient): Promise<PlatformResult> {
67
90
  if (operation.kind === 'task-comment') {
68
- return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency, skipQueue: true });
91
+ return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
69
92
  }
70
93
  if (operation.kind === 'artifact-comment') {
71
- return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency, skipQueue: true });
94
+ return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
72
95
  }
73
96
  if (operation.kind === 'summary-comment') {
74
- const summary = summaryPayload(taskDir, taskId);
75
- if (!summary) return platformResult('failed', { error: { code: 'SUMMARY_STAGING_MISSING', message: 'delivery summary staging record is unavailable', retryable: false } });
76
- return syncPlatformComment(taskId, {
77
- kind: 'summary', body: summary.body, agent, cwd,
78
- summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, client, skipQueue: true
79
- });
97
+ let summary;
98
+ try { summary = summaryPayload(taskDir, taskId); }
99
+ catch (error) { return platformResult('failed', { error: { code: 'SUMMARY_STAGING_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } }); }
100
+ if (!summary) return platformResult('blocked', { error: { code: 'SUMMARY_STAGING_MISSING', message: 'delivery summary staging record is unavailable', retryable: true } });
101
+ return syncPlatformComment(taskId, { kind: 'summary', body: summary.body, agent, cwd, client, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, queuedOperation: operation });
80
102
  }
81
103
  if (operation.kind === 'cancel-comment') {
82
- return syncPlatformComment(taskId, { kind: 'cancel', agent, cwd, client, dependency: operation.dependency, skipQueue: true });
104
+ let body: string | null;
105
+ try { body = cancellationPayload(taskDir, taskId); }
106
+ catch (error) { return platformResult('failed', { error: { code: 'CANCEL_SUMMARY_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } }); }
107
+ if (!body) return platformResult('blocked', { error: { code: 'CANCEL_SUMMARY_INVALID', message: 'Cancellation facts are unavailable for recovery', retryable: true } });
108
+ return syncPlatformComment(taskId, { kind: 'cancel', body, agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
83
109
  }
84
110
  return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_UNSUPPORTED', message: `operation kind '${operation.kind}' has no registered recovery handler`, retryable: false } });
85
111
  }
@@ -183,55 +209,6 @@ async function verifyIssueCreate(taskId: string, operation: ReturnType<typeof re
183
209
  return platformResult('no-op');
184
210
  }
185
211
 
186
- async function replayPullRequestSummary(taskId: string, operation: ReturnType<typeof readPlatformOperationJournal>['operations'][number], agent: string, cwd: string, client?: PlatformClient): Promise<PlatformResult> {
187
- let identity;
188
- try { identity = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request summary identity'); }
189
- catch { return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } }); }
190
- const summary = operation.pullRequestSummary;
191
- if (!summary) return platformResult('blocked', { error: {
192
- code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
193
- } });
194
- const manualValidationHeading = /^###\s+(?:⏳\s+(?:Manual Validation Pending|人工验证待收尾)|✅\s+(?:Manual Validation Passed|人工验证已通过))\s*$/mu.test(summary.body);
195
- if (manualValidationHeading && !summary.manualValidation) return platformResult('blocked', { error: {
196
- code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'Queued manual-validation summary has no persisted transaction context', retryable: false
197
- } });
198
- const resolved = await resolvePlatformProviderContext({ cwd, client });
199
- if (!resolved.ok) return resolved.context;
200
- const listed = resolved.value.provider.comments?.list
201
- ? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
202
- : unsupportedProviderOperation(resolved.value.provider, 'comments.list');
203
- if (!listed.ok) return platformResult(providerErrorRetryable(listed.error.code) ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
204
- const marker = `<!-- sync-pr:${taskId}:summary -->`;
205
- const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
206
- if (summaries.length > 1) return platformResult('blocked', { error: {
207
- code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple pull-request comments contain the queued summary marker', retryable: false
208
- } });
209
- if (!summary.manualValidation && summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0]!.body)).digest('hex') === operation.expectedDigest) {
210
- return platformResult('no-op');
211
- }
212
- const bound = await inspectPlatformPullRequest(taskId, { cwd, client });
213
- if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, identity)) return platformResult('blocked', { error: bound.error ?? {
214
- code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request differs from the queued summary target', retryable: false
215
- } });
216
- const replayed = await syncPullRequestSummary(taskId, {
217
- agent,
218
- cwd,
219
- client,
220
- body: summary.body,
221
- changeReportFile: summary.changeReportFile,
222
- ...(summary.manualValidation ? { manualValidation: summary.manualValidation } : {}),
223
- primaryResult: 'no_op',
224
- strict: true,
225
- skipQueue: true
226
- });
227
- if ((replayed.status === 'applied' || replayed.status === 'no-op') && !replayed.error && replayed.warnings.length === 0) {
228
- return platformResult(replayed.status, { changed: replayed.changed });
229
- }
230
- return platformResult('blocked', { error: replayed.error ?? {
231
- code: 'PR_SUMMARY_RECOVERY_UNCONFIRMED', message: 'Pull-request summary replay did not confirm the requested remote state', retryable: true
232
- } });
233
- }
234
-
235
212
  async function replayPullRequestReview(operation: ReturnType<typeof readPlatformOperationJournal>['operations'][number], agent: string, cwd: string, client?: PlatformClient): Promise<PlatformResult> {
236
213
  const intent = operation.pullRequestReview;
237
214
  if (!intent) return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request review recovery parameters are missing', retryable: false } });
@@ -280,6 +257,14 @@ async function replayPullRequest(taskId: string, operation: ReturnType<typeof re
280
257
  return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
281
258
  }
282
259
 
260
+ function isRecoveryEligible(operation: ReturnType<typeof readPlatformOperationJournal>['operations'][number], excludeId?: string): boolean {
261
+ if (operation.id === excludeId) return false;
262
+ if (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown') return true;
263
+ return operation.state === 'failed'
264
+ && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'
265
+ ;
266
+ }
267
+
283
268
  async function recoverPlatformOperationsOnce(
284
269
  taskRef: string,
285
270
  selection: 'required' | 'deferred' | 'all',
@@ -293,9 +278,7 @@ async function recoverPlatformOperationsOnce(
293
278
  const value = error as { code?: string; message?: string };
294
279
  return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
295
280
  }
296
- const candidates = journal.operations.filter((operation) => operation.id !== options.excludeId
297
- && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
298
- || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')));
281
+ const candidates = journal.operations.filter((operation) => isRecoveryEligible(operation, options.excludeId));
299
282
  if (!candidates.length) return result('no-op', [], []);
300
283
  const head = candidates[0]!;
301
284
  if (selection !== 'all' && head.dependency !== selection) {
@@ -357,11 +340,18 @@ async function recoverPlatformOperationsOnce(
357
340
  const commentOptions = operation.kind === 'task-comment'
358
341
  ? { kind: 'task' as const, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
359
342
  : operation.kind === 'artifact-comment'
360
- ? { kind: 'artifact' as const, artifact: operation.target, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
361
- : operation.kind === 'summary-comment' ? (() => {
362
- const summary = summaryPayload(resolved.taskDir, resolved.taskId);
363
- return summary ? { kind: 'summary' as const, body: summary.body, agent: options.agent, cwd: resolved.repoRoot, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency } : null;
364
- })() : { kind: 'cancel' as const, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency };
343
+ ? { kind: 'artifact' as const, artifact: operation.target, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
344
+ : operation.kind === 'summary-comment' ? (() => {
345
+ try {
346
+ const summary = summaryPayload(resolved.taskDir, resolved.taskId);
347
+ return summary ? { kind: 'summary' as const, body: summary.body, agent: options.agent, cwd: resolved.repoRoot, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency } : null;
348
+ } catch { return null; }
349
+ })() : (() => {
350
+ try {
351
+ const body = cancellationPayload(resolved.taskDir, resolved.taskId);
352
+ return body ? { kind: 'cancel' as const, body, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency } : null;
353
+ } catch { return null; }
354
+ })();
365
355
  const current = commentOptions ? inspectPlatformCommentOperation(resolved.taskId, commentOptions) : null;
366
356
  if (!current) {
367
357
  pending.push(operation.id);
@@ -383,20 +373,21 @@ async function recoverPlatformOperationsOnce(
383
373
  remote = operation.kind === 'issue-create'
384
374
  ? await verifyIssueCreate(resolved.taskId, operation, resolved.repoRoot)
385
375
  : operation.kind === 'pull-request-summary'
386
- ? await replayPullRequestSummary(resolved.taskId, operation, options.agent, resolved.repoRoot, options.client)
376
+ ? await replayPullRequestSummary(resolved.taskId, targetOperation, { agent: options.agent, cwd: resolved.repoRoot, client: options.client })
387
377
  : operation.kind === 'pull-request-review'
388
378
  ? await replayPullRequestReview(operation, options.agent, resolved.repoRoot, options.client)
389
379
  : operation.kind === 'issue-metadata'
390
380
  ? await replayIssueMetadata(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
391
381
  : operation.kind === 'pull-request'
392
382
  ? await replayPullRequest(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
393
- : await replayComment(resolved.taskId, resolved.taskDir, operation, options.agent, resolved.repoRoot);
383
+ : await replayComment(resolved.taskId, resolved.taskDir, targetOperation, options.agent, resolved.repoRoot, options.client);
394
384
  }
395
385
  catch (error) {
396
386
  const value = error as { code?: string; message?: string };
397
387
  remote = platformResult('blocked', { error: { code: value.code || 'PLATFORM_OPERATION_RECOVERY_FAILED', message: value.message || String(error), retryable: true } });
398
388
  }
399
389
  const succeeded = (remote.status === 'applied' || remote.status === 'no-op') && !remote.error;
390
+ const latestOperation = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot).operations.find((item) => item.id === operation.id);
400
391
  try {
401
392
  recordPlatformOperation({
402
393
  taskRef: resolved.taskId,
@@ -411,7 +402,7 @@ async function recoverPlatformOperationsOnce(
411
402
  ...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
412
403
  dependency: operation.dependency,
413
404
  state: succeeded ? 'succeeded' : 'unknown',
414
- lastCode: remote.error?.code ?? null
405
+ lastCode: remote.error?.code ?? latestOperation?.lastCode ?? null
415
406
  });
416
407
  } catch (error) {
417
408
  const value = error as { code?: string; message?: string };
@@ -441,9 +432,7 @@ async function recoverPlatformOperations(
441
432
  const value = error as { code?: string; message?: string };
442
433
  return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
443
434
  }
444
- const eligible = (operation: typeof initial.operations[number]) => operation.id !== options.excludeId
445
- && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
446
- || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'));
435
+ const eligible = (operation: typeof initial.operations[number]) => isRecoveryEligible(operation, options.excludeId);
447
436
  const first = initial.operations.filter(eligible);
448
437
  if (!first.length) return result('no-op', [], []);
449
438
  const operationLimit = options.limit === undefined ? first.length : Math.max(1, options.limit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@ agent-infra-internal platform-pr summary-sync {task-id} \
25
25
  --result {primary-result}
26
26
  ```
27
27
 
28
- Use only canonical inputs returned by the context command. The precheck must contain file evidence for six fixed checks; `formalReview` is always false and `needs-review` routes to `review-code`. Write the plain body with exactly one `<!-- canonical-pr-change-report -->` placeholder, without a report heading/JSON, marker, or SHA. The core validates the task-intent digest, bound PR identity, complete patch SHA, and mechanical totals before rendering the report, then owns `<!-- sync-pr:{task-id}:summary -->`, authoritative `<!-- last-commit: ... -->`, create/update/no-op behavior, and duplicate-marker conflicts. Missing, stale, invalid, or bypassed report input never publishes; summary failure never rolls back a PR or commit.
28
+ Use only canonical inputs returned by the context command. The precheck must contain file evidence for six fixed checks; `formalReview` is always false and `needs-review` routes to `review-code`. Generate a typed JSON payload with `overview:string`, `tests:{state,detail}`, `review:{state,detail}`, `manualValidation:{state,detail}`, `humanDecisions:{open,resolved,detail}`, and `risks:string`. Detail fields are non-empty, counts are non-negative integers, and structure checks do not assess semantic quality. Use `not-started`/`pending` for tests that have not started, `none`/`not-observed`/`not-published` for absent or unobserved remote Reviews, and `pending` for manual validation that has not run. Human-decision counts come from the task ledger. The renderer supplies fixed sections and the canonical report placeholder. The core validates the task-intent digest, bound PR identity, complete patch SHA, and mechanical totals before rendering the report, then owns markers, HEAD metadata, and create/update/no-op behavior.
29
29
 
30
30
  The canonical comment envelope produced by the core is:
31
31
 
@@ -40,7 +40,9 @@ The canonical comment envelope produced by the core is:
40
40
  ### PR Code Changes
41
41
  ```
42
42
 
43
- Canonical inputs include `manual-validation.md`. The manual-validation section is one of `### ✅ Manual Validation Passed`, `### ⚠️ Manual Verification Required`, or `### ✅ No Manual Verification Needed`; `complete-manual-validation` refreshes the same summary in place. The PR code-change section uses authoritative base/head facts from `platform-pr inspect` and the complete `git diff --find-renames --numstat base...head`, reconciles runtime, test, skill/rule, template, documentation, and other categories, and explains renames, mechanical mirrors, and potentially unnecessary changes. The caller supplies the placeholder; the core renderer supplies the canonical `### PR Code Changes` section.
43
+ Canonical inputs include `manual-validation.md`. The manual-validation `state` can be `required`, `pending`, `passed`, `none`/`not-required`, or `not-observed`. English headings such as `### ⚠️ Manual Verification Required`, `### ✅ Manual Validation Passed`, and `### ✅ No Manual Verification Needed` are documentation labels; the current renderer emits Chinese headings. `complete-manual-validation` refreshes the same summary in place. The PR code-change section uses authoritative base/head facts from `platform-pr inspect` and the complete `git diff --find-renames --numstat base...head`, reconciles runtime, test, skill/rule, template, documentation, and other categories, and explains renames, mechanical mirrors, and potentially unnecessary changes. The core renderer supplies the canonical `### PR Code Changes` section. After publishing, the core reads comments back and requires one summary marker with a body that exactly matches the canonical rendered body; mismatch or read failure uses the existing coordinator/recovery path.
44
+
45
+ Examples include `required`, `pending`, `passed`, `none`/`not-required`, and `not-observed`; this list is illustrative, not exhaustive. The renderer also accepts `unavailable` and other non-empty states through its existing fallback. English headings below are documentation labels; the current renderer emits the stated Chinese headings. `required` retains each item, location, and reason under `### ⚠️ 需人工校验`. `pending` means validation has not started or remains incomplete and renders as `### ⏳ 人工验证待收尾`. `passed` (`### ✅ Manual Validation Passed` in this documentation; `### ✅ 人工验证已通过` from the renderer) is publishable only through the final `complete-manual-validation` coordinator after it writes a valid receipt. Preserve the receipt marker bound to the transaction, receipt, evidence, and current PR head; never infer passed status from summary text or another state. `none`/`not-required` renders as `### ✅ 无需人工校验` only when no manual item is required and carries no passed receipt. `not-observed` means the state has not been read and renders as `### ℹ️ 尚未读取人工校验状态`.
44
46
 
45
47
  ```bash
46
48
  agent-infra-internal task-warning {task-id} add --step {step} --severity WARNING \
@@ -18,17 +18,14 @@ PR 摘要的语义聚合由模型完成;任务意图 digest、完整三点 dif
18
18
  agent-infra-internal platform-pr summary-context {task-id}
19
19
  ```
20
20
 
21
- 只使用返回的 canonical latest `plan*`、`review-plan*`、`code*`、`review-code*`、`manual-validation*`(包括 `manual-validation.md`)。模型据此生成 reviewer 摘要正文,至少包含:
21
+ 只使用返回的 canonical latest `plan*`、`review-plan*`、`code*`、`review-code*`、`manual-validation*`(包括 `manual-validation.md`)。模型生成 PR 专属 JSON payload:`overview:string`、`tests:{state,detail}`、`review:{state,detail}`、`manualValidation:{state,detail}`、`humanDecisions:{open,resolved,detail}`、`risks:string`。说明字段为非空字符串,计数为非负整数;结构校验不评价内容语义。
22
22
 
23
- - 变更摘要与实现范围
24
- - 测试结果
25
- - 审查历程与当前结论
26
- - 人工校验三态:`### ⚠️ 需人工校验`、`### ✅ 人工验证已通过` 或 `### ✅ 无需人工校验`
27
- - 变更摘要、实现范围、测试结果、审查历程和人工校验三态
28
- - 一个 `<!-- canonical-pr-change-report -->` 占位符;`### PR 代码增减` 由 core renderer 基于权威 diff 生成,调用方不自行写标题或统计
23
+ 测试尚未启动用 `not-started`/`pending`,暂无远端 Review 用 `none`/`not-observed`/`not-published`,待人工校验用 `pending`,不得渲染为通过或零值。决策计数来自当前任务账本。renderer 固定生成完整章节并加入一个 canonical 报告占位符;代码增减仍由现有 report renderer 输出。
29
24
 
30
25
  保留人工校验项时,每条写明校验内容、定位和只能人工完成的原因。
31
26
 
27
+ 例如可见状态包括 `required`、`pending`、`passed`、`none`/`not-required` 和 `not-observed`;这些是示例而非穷举。renderer 也接受 `unavailable` 及其他非空状态,并按现有 fallback 渲染。英文标题是文档标签;当前 renderer 输出下列中文标题:`required` 使用 `### ⚠️ 需人工校验` 并保留每项内容、定位和原因;`pending` 使用 `### ⏳ 人工验证待收尾`;`passed` 使用 `### ✅ 人工验证已通过`,但仅允许 `complete-manual-validation` 的 final coordinator 写入有效 receipt 后发布,并保留绑定 transaction、receipt、evidence 和当前 PR head 的凭据标记;不得根据摘要文字或其他状态推断通过。`none`/`not-required` 仅在无需人工校验时使用 `### ✅ 无需人工校验`,不附加通过凭据;`not-observed` 使用 `### ℹ️ 尚未读取人工校验状态`。
28
+
32
29
  core 最终包装出的 canonical 评论结构为:
33
30
 
34
31
  ```markdown
@@ -54,7 +51,7 @@ precheck 必须覆盖六项固定检查并提供文件证据;`formalReview`
54
51
 
55
52
  ## 发布
56
53
 
57
- 把聚合后的纯正文写入 `{summary-body-file}`,不要自行添加 marker 或 commit SHA:
54
+ 把聚合后的 JSON payload 写入 `{summary-body-file}`,不要自行添加 marker 或 commit SHA:
58
55
 
59
56
  ```bash
60
57
  agent-infra-internal platform-pr summary-sync {task-id} \
@@ -63,7 +60,7 @@ agent-infra-internal platform-pr summary-sync {task-id} \
63
60
  --result {primary-result}
64
61
  ```
65
62
 
66
- 正文必须恰好包含一次 `<!-- canonical-pr-change-report -->`,不得自行拼接报告标题/JSON、marker 或 last-commit。core 会重新校验任务意图 digest、绑定 PR identity、完整 patch SHA 和机械统计,由 renderer 生成报告段,再包装唯一 `<!-- sync-pr:{task-id}:summary -->` 和权威 `<!-- last-commit: ... -->`,分页查找 PR 普通评论:不存在则创建、正文变化则原地更新、无差异则 no-op、重复 marker 或报告失配则稳定失败。正文通过文件输入,调用方不得拼 shell/heredoc。
63
+ core 校验结构后插入报告占位符,重新校验任务意图 digest、PR identity、完整 patch SHA 和机械统计,生成报告段,再包装 marker 与权威 HEAD。发布后使用 `comments.list` 回读并要求唯一 marker 和正文完全匹配;不符或读取失败沿用现有 coordinator/recovery。payload 通过文件输入,调用方不得拼 shell/heredoc。
67
64
 
68
65
  ## 结果回传
69
66
 
@@ -83,7 +83,7 @@ If a valid `platform_issue_identity` exists, require `applied` / `no-op` without
83
83
  - Write the cancellation body to a temporary file and run `agent-infra-internal platform-comment sync {task-id} --kind cancel --body-file {path} --agent {standard-agent-token}`
84
84
  - Run `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`
85
85
 
86
- The cancellation comment must include at least:
86
+ The cancellation comment JSON fields are `taskId`, `title`, `outcome:"cancelled"`, `reason`, `cancelledAt`, and `lastCompletedArtifact`, sourced from task identity, task.md, the recorded cancellation reason/time, and the current artifact registry (`null` when no completed artifact exists). It must include at least:
87
87
  - the cancellation reason
88
88
  - the selected `status:` label
89
89
 
@@ -82,7 +82,7 @@ ls <task-workspace>/completed/{task-id}/task.md
82
82
  - 将取消正文写入临时文件,调用 `agent-infra-internal platform-comment sync {task-id} --kind cancel --body-file {path} --agent {standard-agent-token}`
83
83
  - 调用 `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`
84
84
 
85
- 取消评论至少包含:
85
+ 取消评论 JSON 字段为 `taskId`、`title`、`outcome:"cancelled"`、`reason`、`cancelledAt`、`lastCompletedArtifact`,值来自任务身份、task.md、已记录取消原因与时间和当前 artifact registry;无已完成产物时用 `null`。取消评论至少包含:
86
86
  - 取消原因
87
87
  - 选定的 `status:` label
88
88
 
@@ -3,7 +3,7 @@
3
3
  Run only when `{task-id}` is valid and task.md has a `bound/verified` `pr_delivery_fact`.
4
4
 
5
5
  1. Run `agent-infra-internal platform-pr summary-context {task-id}` to obtain canonical inputs and report status; rebuild a stale/missing sidecar through the `change-report` flow for the current head.
6
- 2. Generate the plain summary body per `.agents/rules/pr-sync.md`, with exactly one `<!-- canonical-pr-change-report -->` placeholder, and write it to a temporary file.
6
+ 2. Generate the complete PR summary JSON payload per `.agents/rules/pr-sync.md` and write it to a temporary file.
7
7
  3. Run `agent-infra-internal platform-pr summary-sync {task-id} --agent {standard-agent-token} --body-file {summary-body-file} --change-report-file <task-workspace>/active/{task-id}/pr-change-report.json --result no_op`. The commit path only synchronizes an existing PR summary and neither creates nor reuses a PR, so `no_op` explicitly represents the unchanged PR identity.
8
8
 
9
9
  Failure records a warning but never blocks or rolls back the completed commit. `no-op` maps to `summary skipped (no diff)`.
@@ -3,7 +3,7 @@
3
3
  仅当 `{task-id}` 有效且 `task.md` 存在 `bound/verified` 的 `pr_delivery_fact` 时执行;否则跳过。
4
4
 
5
5
  1. 调用 `agent-infra-internal platform-pr summary-context {task-id}` 获取 canonical 聚合输入和报告状态;sidecar stale/missing 时先按 `change-report` 流程重建当前 head 报告。
6
- 2. 按 `.agents/rules/pr-sync.md` 生成只含一次 `<!-- canonical-pr-change-report -->` 的纯摘要正文并写入临时文件。
6
+ 2. 按 `.agents/rules/pr-sync.md` 生成完整 PR 摘要 JSON payload 并写入临时文件;字段和零值状态由内容合同规定。
7
7
  3. 调用 `agent-infra-internal platform-pr summary-sync {task-id} --agent {standard-agent-token} --body-file {summary-body-file} --change-report-file <task-workspace>/active/{task-id}/pr-change-report.json --result no_op`。commit 路径只同步已有 PR 摘要,不创建或复用 PR,因此固定使用 `no_op` 表示 PR 身份未变化。
8
8
 
9
9
  同步失败只记录 warning,不阻塞或回滚已经完成的 commit。`no-op` 回传 `summary skipped (no diff)`。
@@ -4,7 +4,7 @@ Run only for a valid `{task-id}` whose task.md binds a verified `pr_delivery_fac
4
4
 
5
5
  Follow `.agents/rules/pr-sync.md` for the shared summary structure and failure semantics.
6
6
 
7
- 1. Run `agent-infra-internal platform-pr summary-context {task-id}` to obtain the canonical summary body; the maintainer's PR validation comment and validation summary are the human-validation authority.
7
+ 1. Run `agent-infra-internal platform-pr summary-context {task-id}` to obtain canonical summary facts. `--summary-file` uses the complete PR summary JSON payload; the maintainer's PR validation comment and validation summary are the human-validation authority.
8
8
  2. Before creating the canonical `manual-validation*` artifact, call the transaction coordinator in prepare mode to record `started` before writing the prepared transaction:
9
9
 
10
10
  ```bash
@@ -13,7 +13,7 @@ agent-infra-internal manual-validation transaction {task-id} --prepare \
13
13
  --summary-file {summary-body-file} --agent {standard-agent-token}
14
14
  ```
15
15
 
16
- 3. After prepare succeeds, write the canonical `manual-validation*` artifact, write the pending plain body with exactly one `<!-- canonical-pr-change-report -->` placeholder to a temporary file, and run the transaction coordinator:
16
+ 3. After prepare succeeds, write the canonical `manual-validation*` artifact, set the JSON payload's `manualValidation.state` to `pending` with a non-empty explanation, and run the transaction coordinator:
17
17
 
18
18
  ```bash
19
19
  agent-infra-internal manual-validation transaction {task-id} \
@@ -4,7 +4,7 @@
4
4
 
5
5
  摘要结构与失败语义统一遵循 `.agents/rules/pr-sync.md`。
6
6
 
7
- 1. 调用 `agent-infra-internal platform-pr summary-context {task-id}`,取得 canonical 摘要正文;维护者提供的 PR 人工验证留言和验证说明作为完成人工校验依据。
7
+ 1. 调用 `agent-infra-internal platform-pr summary-context {task-id}`,取得 canonical 摘要事实;`--summary-file` 使用完整 PR 摘要 JSON payload,维护者提供的 PR 人工验证留言和验证说明作为完成人工校验依据。
8
8
  2. 在创建 canonical `manual-validation*` artifact 前,调用 transaction coordinator 的 prepare 模式,先登记 started,再写 prepared transaction:
9
9
 
10
10
  ```bash
@@ -13,7 +13,7 @@ agent-infra-internal manual-validation transaction {task-id} --prepare \
13
13
  --summary-file {summary-body-file} --agent {standard-agent-token}
14
14
  ```
15
15
 
16
- 3. prepare 成功后写入 canonical `manual-validation*` artifact,并把只含一次 `<!-- canonical-pr-change-report -->` 的 pending 正文写入临时文件,调用 transaction coordinator:
16
+ 3. prepare 成功后写入 canonical `manual-validation*` artifact,并将 JSON payload 中 `manualValidation` 设置为 `pending`、提供非空说明后写入临时文件,调用 transaction coordinator:
17
17
 
18
18
  ```bash
19
19
  agent-infra-internal manual-validation transaction {task-id} \
@@ -18,8 +18,7 @@ Host finalization uses receipt v4 with an immutable `receiptId`, monotonic `revi
18
18
 
19
19
  Before generating a completion report or synchronized content, read `.agents/rules/evidence-reporting.md`. Successful checks record the command, scope, status or structured result, actual result, and uncovered parts; failures, blocking conditions, or disputes retain a reproducible entry point, exact location, and decisive excerpt.
20
20
 
21
- - This command updates task metadata AND physically moves the task directory
22
- - Do not move a task that has incomplete workflow steps unless forced
21
+ - Host finalization updates task metadata and atomically moves the task directory
23
22
  - The entry point accepts optional `--external-pr <N>` only to select among ambiguous external-delivery candidates; it never bypasses identity or platform gates
24
23
 
25
24
  Version stamp rule: when creating or updating `task.md` frontmatter, read `.agents/rules/version-stamp.md` first and write or refresh `agent_infra_version`.
@@ -142,7 +141,7 @@ Check whether task.md has a valid `platform_issue_identity`. If it does not, ski
142
141
  When a valid `platform_issue_identity` exists, execute in this exact order:
143
142
 
144
143
  1. Run `agent-infra-internal platform-issue sync {task-id} --agent {standard-agent-token} --requirements --fields`.
145
- 2. Write the business summary to a temporary file and run `agent-infra-internal platform-comment stage-summary {task-id} --body-file {path}`. This stores the body and SHA-256 in the task's durable staging record; it must not publish the summary comment directly.
144
+ 2. Write the completion summary as JSON fields `taskId`, `title`, `summary`, `artifacts`, and `verification:{state,detail}`, then run `agent-infra-internal platform-comment stage-summary {task-id} --body-file {path}`. Use the current artifact registry (`[]` when empty). If final verification has not run, use `state:"not-run"` with a non-empty explanation. The command persists the normalized payload; do not publish the summary comment in this step.
146
145
 
147
146
  The summary body records only delivery results, the associated PR and actual merge facts, verification commands with their scope/results/uncovered areas, and known todo items supported by current task evidence. Include the related commit when it helps explain verification scope. Do not infer unsupported facts; list unfinished work as a todo. This is an Issue closeout summary, not a PR comment.
148
147
 
@@ -225,32 +224,18 @@ ai sandbox rm {task-id} # or use the task's retained short id
225
224
 
226
225
  ## Completion Checklist
227
226
 
228
- - [ ] Verified all workflow steps are complete
229
- - [ ] Updated task.md with completed status and timestamp
230
- - [ ] Moved task directory to `<task-workspace>/completed/`
231
- - [ ] Verified move succeeded
232
- - [ ] Informed user of completion
227
+ - [ ] Hard prerequisites are satisfied and the pre-completion gate passes
228
+ - [ ] Host finalization returns `result=completed` or `result=completed_with_warnings`
229
+ - [ ] Completion verification passes
230
+ - [ ] The structured completion result and applicable warnings are reported to the user
233
231
 
234
232
  ## Notes
235
233
 
236
- 1. **Premature completion**: Do not move a task that has incomplete steps. Examples of incomplete situations:
237
- - Code is written but not committed
238
- - Code is committed but not reviewed
239
- - Review found blockers that haven't been fixed
240
- - PR is created but not merged
241
- - Manual validation items are incomplete
242
-
243
- 2. **Rollback**: If a task was incorrectly moved:
244
- ```bash
245
- mv <task-workspace>/completed/{task-id} <task-workspace>/active/{task-id}
246
- ```
247
- Then update task.md status back to `active`.
248
-
249
- 3. **Multiple contributors**: If multiple AI agents worked on the task, ensure all contributions are committed before completing.
234
+ - When multiple AI agents contributed, ensure all contributions are included in the local checkpoint.
250
235
 
251
236
  ## Error Handling
252
237
 
253
238
  - Task not found: Prompt "Task {task-id} not found in active directory"
254
- - Already completed: Prompt "Task {task-id} is already in completed directory"
255
- - Task is blocked: Prompt "Task {task-id} is blocked. Unblock it first by moving to active/"
256
- - Move failed: Prompt error and suggest manual move
239
+ - Already completed: Enter `finalization-retry` only when a matching Complete Task Activity Log exists; otherwise report incomplete terminal identity and stop
240
+ - Task is blocked: Satisfy the recorded unblock condition, then reactivate it through `task-lifecycle activate` before retrying
241
+ - Finalization failed or blocked: Preserve source data and the receipt, then recover through the same finalization entry point using its structured error; do not manually rewrite terminal state or move the task directory