@fitlab-ai/agent-infra 0.11.1 → 0.11.3

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 (131) hide show
  1. package/dist/bin/internal-cli.js +7 -2
  2. package/dist/lib/agent-clients/adapter.js +3 -0
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  6. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  7. package/dist/lib/internal/cli-route-inventory.js +1 -1
  8. package/dist/lib/internal/manual-validation.js +22 -27
  9. package/dist/lib/internal/platform-comment.js +59 -0
  10. package/dist/lib/internal/sandbox-control.js +2 -171
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +7 -2
  14. package/dist/lib/internal/task-review.js +5 -5
  15. package/dist/lib/internal/task-validate.js +0 -6
  16. package/dist/lib/platform/comment-safety.js +7 -1
  17. package/dist/lib/platform/github-provider.js +13 -3
  18. package/dist/lib/platform/issue-comments.js +93 -15
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/platform/provider-contract.d.ts +2 -0
  21. package/dist/lib/platform/provider-validation.js +7 -2
  22. package/dist/lib/platform/verification-sync.js +19 -2
  23. package/dist/lib/sandbox/control/client.js +3 -44
  24. package/dist/lib/sandbox/control/executor.js +14 -117
  25. package/dist/lib/sandbox/control/finalization-response.js +8 -2
  26. package/dist/lib/sandbox/control/protocol.js +5 -64
  27. package/dist/lib/sandbox/control/server.js +29 -206
  28. package/dist/lib/sandbox/control/state.js +1 -7
  29. package/dist/lib/sandbox/control/task-view.js +2 -1
  30. package/dist/lib/task/artifact-command.js +6 -6
  31. package/dist/lib/task/codex-orchestration.js +10 -208
  32. package/dist/lib/task/commit-operation.js +3 -18
  33. package/dist/lib/task/control-authority.js +35 -44
  34. package/dist/lib/task/control-recovery.js +3 -42
  35. package/dist/lib/task/delegation-receipts.js +16 -78
  36. package/dist/lib/task/events.js +89 -237
  37. package/dist/lib/task/finalization-artifacts.js +1 -3
  38. package/dist/lib/task/finalization-state.js +2 -1
  39. package/dist/lib/task/finalization.js +208 -57
  40. package/dist/lib/task/lifecycle-execution.js +2 -11
  41. package/dist/lib/task/lifecycle.js +1 -1
  42. package/dist/lib/task/local-artifact-finalization.js +25 -242
  43. package/dist/lib/task/manual-validation-receipt.js +8 -3
  44. package/dist/lib/task/orchestration.js +12 -24
  45. package/dist/lib/task/review-command.js +0 -1
  46. package/dist/lib/task/review-finalization.js +46 -211
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapter.ts +46 -0
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  51. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  52. package/lib/agent-clients/adapters/codex.ts +10 -0
  53. package/lib/internal/cli-route-inventory.ts +1 -1
  54. package/lib/internal/manual-validation.ts +22 -26
  55. package/lib/internal/platform-comment.ts +38 -0
  56. package/lib/internal/sandbox-control.ts +1 -185
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +2 -2
  59. package/lib/internal/task-operation-registry.ts +8 -4
  60. package/lib/internal/task-review.ts +5 -5
  61. package/lib/internal/task-validate.ts +0 -5
  62. package/lib/platform/comment-safety.ts +8 -0
  63. package/lib/platform/github-provider.ts +11 -2
  64. package/lib/platform/issue-comments.ts +102 -17
  65. package/lib/platform/pr-summary.ts +10 -2
  66. package/lib/platform/provider-contract.ts +2 -0
  67. package/lib/platform/provider-validation.ts +6 -2
  68. package/lib/platform/verification-sync.ts +19 -2
  69. package/lib/sandbox/control/client.ts +5 -68
  70. package/lib/sandbox/control/executor.ts +14 -125
  71. package/lib/sandbox/control/finalization-response.ts +8 -2
  72. package/lib/sandbox/control/protocol.ts +7 -83
  73. package/lib/sandbox/control/server.ts +30 -220
  74. package/lib/sandbox/control/state.ts +1 -9
  75. package/lib/sandbox/control/task-view.ts +2 -1
  76. package/lib/task/artifact-command.ts +6 -7
  77. package/lib/task/codex-orchestration.ts +8 -241
  78. package/lib/task/commit-operation.ts +3 -23
  79. package/lib/task/control-authority.ts +39 -55
  80. package/lib/task/control-recovery.ts +3 -43
  81. package/lib/task/delegation-receipts.ts +14 -81
  82. package/lib/task/events.ts +106 -255
  83. package/lib/task/finalization-artifacts.ts +1 -3
  84. package/lib/task/finalization-state.ts +2 -1
  85. package/lib/task/finalization.ts +221 -65
  86. package/lib/task/lifecycle-execution.ts +10 -21
  87. package/lib/task/lifecycle.ts +2 -2
  88. package/lib/task/local-artifact-finalization.ts +23 -281
  89. package/lib/task/manual-validation-receipt.ts +8 -2
  90. package/lib/task/orchestration.ts +30 -53
  91. package/lib/task/review-command.ts +0 -1
  92. package/lib/task/review-finalization.ts +45 -193
  93. package/package.json +2 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
  95. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
  96. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  97. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  101. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  102. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  105. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  106. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  109. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  110. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  111. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  112. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  113. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  114. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  115. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  116. package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
  117. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
  118. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  119. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  120. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  121. package/dist/lib/sandbox/control/workflow-executor.js +0 -157
  122. package/dist/lib/task/lifecycle-recovery.js +0 -549
  123. package/dist/lib/task/recovery-warning.js +0 -19
  124. package/dist/lib/task/workflow-command.js +0 -18
  125. package/dist/lib/task/workflow-dispatch.js +0 -161
  126. package/lib/sandbox/control/task-workflow.ts +0 -95
  127. package/lib/sandbox/control/workflow-executor.ts +0 -183
  128. package/lib/task/lifecycle-recovery.ts +0 -691
  129. package/lib/task/recovery-warning.ts +0 -21
  130. package/lib/task/workflow-command.ts +0 -19
  131. package/lib/task/workflow-dispatch.ts +0 -171
@@ -1,9 +1,13 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
4
 
5
5
  import { syncPlatformComment } from '../platform/issue-comments.ts';
6
+ import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
7
+ import { backfillCompletionComments } from '../platform/completion-backfill.ts';
6
8
  import type { PlatformResult } from '../platform/types.ts';
9
+ import { taskIssueIdentity } from '../platform/task-identities.ts';
10
+ import { parseTaskFrontmatter } from './frontmatter.ts';
7
11
  import {
8
12
  applyTaskLifecycle,
9
13
  type TaskLifecycleOptions,
@@ -22,8 +26,8 @@ import {
22
26
  type OperationWarningSeverity
23
27
  } from './operation-outcome.ts';
24
28
 
25
- const RECEIPT_VERSION = 2 as const;
26
- const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification'] as const;
29
+ const RECEIPT_VERSION = 4 as const;
30
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
27
31
  type FinalizationStep = typeof FINALIZATION_STEPS[number];
28
32
  type FinalizationStepState = 'pending' | 'done' | 'skipped';
29
33
  type FinalizationError = { code: string; message: string; retryable: boolean };
@@ -58,6 +62,7 @@ type TaskFinalizationOptions = Readonly<{
58
62
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
59
63
  metadataProvider?: TaskLifecycleOptions['metadataProvider'];
60
64
  lifecycle?: typeof applyTaskLifecycle;
65
+ backfill?: typeof backfillCompletionComments;
61
66
  commentSync?: typeof syncPlatformComment;
62
67
  verify?: typeof verifyTaskEvent;
63
68
  preflight?: typeof verifyTaskEvent;
@@ -72,6 +77,7 @@ type TaskFinalizationReceipt = Readonly<{
72
77
  lifecycle: FinalizationStepState;
73
78
  taskComment: FinalizationStepState;
74
79
  verification: FinalizationStepState;
80
+ summary: FinalizationStepState;
75
81
  warningProjection: WarningProjectionState;
76
82
  warnings: readonly FinalizationWarning[];
77
83
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
@@ -98,9 +104,11 @@ type TaskFinalizationResult = Readonly<{
98
104
  status: 'completed' | 'failed' | 'blocked';
99
105
  changed: boolean;
100
106
  taskId: string | null;
107
+ backfill: TaskFinalizationStep | null;
101
108
  lifecycle: TaskFinalizationStep | null;
102
109
  taskComment: TaskFinalizationStep | null;
103
110
  verification: TaskFinalizationStep | null;
111
+ summary: TaskFinalizationStep | null;
104
112
  completedSteps: readonly FinalizationStep[];
105
113
  pendingSteps: readonly FinalizationStep[];
106
114
  result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
@@ -136,9 +144,11 @@ function failed(
136
144
  status: error.retryable ? 'blocked' : 'failed',
137
145
  changed: false,
138
146
  taskId,
147
+ backfill: null,
139
148
  lifecycle: null,
140
149
  taskComment: null,
141
150
  verification: null,
151
+ summary: null,
142
152
  completedSteps: [],
143
153
  pendingSteps: [...FINALIZATION_STEPS],
144
154
  result: error.retryable ? 'blocked' : 'failed',
@@ -158,6 +168,7 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
158
168
  lifecycle: 'pending',
159
169
  taskComment: 'pending',
160
170
  verification: 'pending',
171
+ summary: 'pending',
161
172
  warningProjection: 'done',
162
173
  warnings: [],
163
174
  ...(controlBinding ? { controlBinding } : {}),
@@ -194,6 +205,7 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
194
205
  || !states.includes(String(receipt.lifecycle))
195
206
  || !states.includes(String(receipt.taskComment))
196
207
  || !states.includes(String(receipt.verification))
208
+ || !states.includes(String(receipt.summary))
197
209
  || !['pending', 'done'].includes(String(receipt.warningProjection))
198
210
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
199
211
  || typeof receipt.updatedAt !== 'string'
@@ -217,14 +229,6 @@ function readTaskFinalizationReceipt(repoRoot: string, taskId: string): TaskFina
217
229
  return readReceipt(path.resolve(repoRoot), taskId);
218
230
  }
219
231
 
220
- function controlBindingMatches(
221
- receipt: TaskFinalizationReceipt,
222
- binding: Readonly<{ generation: string; requestId: string }>
223
- ): boolean {
224
- return receipt.controlBinding?.generation === binding.generation
225
- && receipt.controlBinding.requestId === binding.requestId;
226
- }
227
-
228
232
  function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void {
229
233
  const directory = finalizationRoot(repoRoot);
230
234
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -242,25 +246,29 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
242
246
  function updateReceipt(
243
247
  repoRoot: string,
244
248
  receipt: TaskFinalizationReceipt,
245
- patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'warningProjection' | 'warnings' | 'lastError'>>
249
+ patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'warningProjection' | 'warnings' | 'controlBinding' | 'lastError'>>
246
250
  ): TaskFinalizationReceipt {
247
251
  const current = readReceipt(repoRoot, receipt.taskId);
248
- if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
249
- const error = new Error('finalization receipt revision is stale');
250
- Object.assign(error, { code: 'FINALIZATION_CAPABILITY_STALE' });
251
- throw error;
252
- }
252
+ if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
253
+ 'FINALIZATION_SCOPE_INVALID',
254
+ 'finalization receipt does not belong to the current task'
255
+ );
253
256
  const next = { ...current, ...patch, revision: current.revision + 1, updatedAt: now() };
254
257
  writeReceipt(repoRoot, next);
255
258
  return next;
256
259
  }
257
260
 
258
261
  function completedSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
259
- return FINALIZATION_STEPS.filter((step) => receipt[step === 'task-comment' ? 'taskComment' : step] !== 'pending');
262
+ return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) !== 'pending');
260
263
  }
261
264
 
262
265
  function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
263
- return FINALIZATION_STEPS.filter((step) => receipt[step === 'task-comment' ? 'taskComment' : step] === 'pending');
266
+ return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) === 'pending');
267
+ }
268
+
269
+ function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
270
+ if (step === 'task-comment') return receipt.taskComment;
271
+ return receipt[step];
264
272
  }
265
273
 
266
274
  function lifecycleStep(result: TaskLifecycleResult): TaskFinalizationStep {
@@ -376,9 +384,10 @@ function validateCapabilityMutation(
376
384
  mutation: FinalizationMutation,
377
385
  consumed: Set<string>
378
386
  ): void {
379
- if (capability.receiptId !== current.receiptId || capability.baseRevision !== current.revision || consumed.has(capability.nonce)) {
380
- throw capabilityError('FINALIZATION_CAPABILITY_STALE', 'finalization capability is stale or outside its scope');
381
- }
387
+ if (capability.receiptId !== current.receiptId) throw capabilityError(
388
+ 'FINALIZATION_SCOPE_INVALID',
389
+ 'finalization mutation belongs to a different task receipt'
390
+ );
382
391
  if (capability.scope !== mutation.scope) {
383
392
  throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation scope does not match its capability');
384
393
  }
@@ -556,15 +565,89 @@ async function syncPendingTaskComment(input: {
556
565
  }
557
566
  }
558
567
 
568
+ function readDeliverySummary(taskDir: string, taskId: string): { body: string; sha256: string } {
569
+ const file = path.join(taskDir, '.delivery-summary.json');
570
+ let value: Record<string, unknown>;
571
+ try {
572
+ value = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
573
+ } catch {
574
+ throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
575
+ }
576
+ if (value.taskId !== taskId || typeof value.body !== 'string' || typeof value.sha256 !== 'string') {
577
+ throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
578
+ }
579
+ const canonical = canonicalizeSummaryBody(value.body);
580
+ if (!canonical.ok || canonical.value !== value.body) {
581
+ throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
582
+ }
583
+ const digest = createHash('sha256').update(canonical.value).digest('hex');
584
+ if (digest !== value.sha256) throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
585
+ return { body: canonical.value, sha256: value.sha256 };
586
+ }
587
+
588
+ async function syncPendingSummary(input: {
589
+ repoRoot: string;
590
+ taskId: string;
591
+ agent: string;
592
+ receipt: TaskFinalizationReceipt;
593
+ commentSync: typeof syncPlatformComment;
594
+ }): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
595
+ let { receipt } = input;
596
+ try {
597
+ const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
598
+ if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
599
+ if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
600
+ receipt = updateReceipt(input.repoRoot, receipt, {
601
+ summary: 'skipped', lastError: null
602
+ });
603
+ return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
604
+ }
605
+ const summary = readDeliverySummary(resolved.taskDir, input.taskId);
606
+ const result = await input.commentSync(input.taskId, {
607
+ kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
608
+ summaryAuthorization: { sha256: summary.sha256 }
609
+ });
610
+ const step = commentStep(result);
611
+ if (result.status === 'applied' || result.status === 'no-op') {
612
+ const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
613
+ const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
614
+ const warnings = resolveStepWarnings(receipt, 'summary');
615
+ receipt = updateReceipt(input.repoRoot, receipt, {
616
+ summary: skipped ? 'skipped' : 'done',
617
+ taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
618
+ warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
619
+ warnings,
620
+ lastError: null
621
+ });
622
+ return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
623
+ }
624
+ const error = step.error ?? { code: 'SUMMARY_SYNC_FAILED', message: 'delivery summary synchronization failed', retryable: true };
625
+ const warning = warningFromError('summary', error);
626
+ const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
627
+ const warnings = replaceWarning(receipt, warning, 'open');
628
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
629
+ return { receipt, step, changed: result.changed, error };
630
+ } catch (cause) {
631
+ const error = errorOf(cause, 'SUMMARY_SYNC_FAILED', true);
632
+ try {
633
+ const warning = warningFromError('summary', error);
634
+ const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
635
+ const warnings = replaceWarning(receipt, warning, 'open');
636
+ receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
637
+ } catch { /* preserve primary error */ }
638
+ return { receipt, step: { status: 'blocked', changed: false, error }, changed: false, error };
639
+ }
640
+ }
641
+
559
642
  function terminalResult(
560
643
  taskId: string,
561
644
  receipt: TaskFinalizationReceipt,
562
- steps: Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification'>,
645
+ steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
563
646
  changed: boolean,
564
647
  error: FinalizationError | null = null
565
648
  ): TaskFinalizationResult {
566
649
  const pending = pendingSteps(receipt);
567
- const blocked = [steps.lifecycle, steps.taskComment, steps.verification].some((step) => step?.status === 'blocked');
650
+ const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
568
651
  const warnings = openWarnings(receipt);
569
652
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
570
653
  const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
@@ -572,14 +655,18 @@ function terminalResult(
572
655
  status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
573
656
  changed,
574
657
  taskId,
575
- ...steps,
658
+ backfill: steps.backfill ?? null,
659
+ lifecycle: steps.lifecycle ?? null,
660
+ taskComment: steps.taskComment ?? null,
661
+ verification: steps.verification ?? null,
662
+ summary: steps.summary ?? null,
576
663
  completedSteps: completedSteps(receipt),
577
664
  pendingSteps: pending,
578
- result: postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
579
- ? 'completed_with_warnings'
580
- : hardError
581
- ? (error?.retryable ? 'blocked' : 'failed')
582
- : pending.length === 0 || postLifecyclePending
665
+ result: hardError
666
+ ? (error?.retryable ? 'blocked' : 'failed')
667
+ : postLifecyclePending
668
+ ? 'completed_with_warnings'
669
+ : pending.length === 0
583
670
  ? 'completed'
584
671
  : blocked
585
672
  ? 'blocked'
@@ -596,6 +683,7 @@ async function applyUnderLock(
596
683
  ): Promise<TaskFinalizationResult> {
597
684
  const repoRoot = path.resolve(options.repoRoot);
598
685
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
686
+ const backfill = options.backfill ?? backfillCompletionComments;
599
687
  const commentSync = options.commentSync ?? syncPlatformComment;
600
688
  const verify = options.verify ?? verifyTaskEvent;
601
689
  const consumedCapabilities = new Set<string>();
@@ -608,17 +696,10 @@ async function applyUnderLock(
608
696
  } catch (error) {
609
697
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
610
698
  }
611
- if (options.controlBinding && !controlBindingMatches(receipt, options.controlBinding)) {
612
- const error: FinalizationError = {
613
- code: 'TASK_FINALIZATION_CONTROL_BINDING_CONFLICT',
614
- message: 'finalization receipt belongs to a different sandbox request or generation',
615
- retryable: false
616
- };
617
- return failed(taskId, error, {
618
- completedSteps: completedSteps(receipt),
619
- pendingSteps: pendingSteps(receipt)
620
- });
621
- }
699
+ if (options.controlBinding && (
700
+ receipt.controlBinding?.generation !== options.controlBinding.generation
701
+ || receipt.controlBinding.requestId !== options.controlBinding.requestId
702
+ )) receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
622
703
 
623
704
  const preflightState = resolveTaskRef(taskId, { repoRoot });
624
705
  if (options.preflight && preflightState.ok && preflightState.state === 'active') {
@@ -642,8 +723,36 @@ async function applyUnderLock(
642
723
  }
643
724
  }
644
725
 
645
- let lifecycleResult: TaskFinalizationStep | null = null;
646
726
  let changed = false;
727
+ let backfillResult: TaskFinalizationStep | null = null;
728
+ try {
729
+ const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
730
+ backfillResult = commentStep(result);
731
+ const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
732
+ if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
733
+ const detail = backfillResult.error ?? {
734
+ code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
735
+ };
736
+ return failed(taskId, detail, {
737
+ backfill: backfillResult,
738
+ completedSteps: completedSteps(receipt),
739
+ pendingSteps: pendingSteps(receipt)
740
+ });
741
+ }
742
+ changed = result.changed;
743
+ if (result.changed && receipt.taskComment !== 'pending') {
744
+ receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
745
+ }
746
+ } catch (error) {
747
+ const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
748
+ return failed(taskId, detail, {
749
+ backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
750
+ completedSteps: completedSteps(receipt),
751
+ pendingSteps: pendingSteps(receipt)
752
+ });
753
+ }
754
+
755
+ let lifecycleResult: TaskFinalizationStep | null = null;
647
756
  try {
648
757
  const resolved = resolveTaskRef(taskId, { repoRoot });
649
758
  const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
@@ -655,6 +764,7 @@ async function applyUnderLock(
655
764
  };
656
765
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
657
766
  return terminalResult(taskId, receipt, {
767
+ backfill: backfillResult,
658
768
  lifecycle: { status: 'failed', changed: false, error: detail },
659
769
  taskComment: null,
660
770
  verification: null
@@ -669,6 +779,7 @@ async function applyUnderLock(
669
779
  };
670
780
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
671
781
  return terminalResult(taskId, receipt, {
782
+ backfill: backfillResult,
672
783
  lifecycle: { status: 'failed', changed: false, error: detail },
673
784
  taskComment: null,
674
785
  verification: null
@@ -686,15 +797,15 @@ async function applyUnderLock(
686
797
  lifecycleResult = lifecycleStep(result);
687
798
  if (result.status !== 'applied' && result.status !== 'no-op') {
688
799
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
689
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
800
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
690
801
  }
691
- changed = result.changed;
802
+ changed = changed || result.changed;
692
803
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
693
804
  }
694
805
  } catch (error) {
695
806
  const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
696
807
  try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
697
- return failed(taskId, detail, { lifecycle: lifecycleResult });
808
+ return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
698
809
  }
699
810
 
700
811
  let taskComment: TaskFinalizationStep | null = null;
@@ -706,21 +817,20 @@ async function applyUnderLock(
706
817
  taskComment = initialComment.step;
707
818
  changed = changed || initialComment.changed;
708
819
  if (initialComment.error) {
709
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
820
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
710
821
  }
711
822
 
712
823
  let verification: TaskFinalizationStep | null = null;
713
824
  try {
714
825
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
715
- if (receipt.verification !== 'pending') {
716
- verification = { status: 'no-op', changed: false, error: null };
717
- } else {
718
- const result = await verify(
719
- { taskRef: taskId, event: 'complete-task.completed' },
720
- { repoRoot }
721
- );
722
- verification = verificationStep(result);
723
- if (result.status === 'pass') {
826
+ const verificationPending = receipt.verification === 'pending';
827
+ const result = await verify(
828
+ { taskRef: taskId, event: 'complete-task.completed' },
829
+ { repoRoot }
830
+ );
831
+ verification = verificationStep(result);
832
+ if (result.status === 'pass') {
833
+ if (verificationPending) {
724
834
  const capability = issueCapability(receipt, 'verification');
725
835
  receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
726
836
  scope: 'verification', operation: 'succeeded'
@@ -733,31 +843,76 @@ async function applyUnderLock(
733
843
  if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
734
844
  changed = changed || finalComment.changed;
735
845
  if (finalComment.error) {
736
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
846
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
737
847
  }
738
848
  } else {
739
- const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
849
+ receipt = updateReceipt(repoRoot, receipt, { lastError: null });
850
+ }
851
+ } else {
852
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
853
+ if (verificationPending) {
740
854
  const capability = issueCapability(receipt, 'verification');
741
855
  receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
742
856
  scope: 'verification', operation: 'failed', error: detail
743
857
  }, consumedCapabilities);
744
858
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
745
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, verification.error);
859
+ } else {
860
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
861
+ receipt = updateReceipt(repoRoot, receipt, {
862
+ verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
863
+ });
864
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
865
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
866
+ receipt = warningComment.receipt;
867
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
868
+ changed = changed || warningComment.changed;
746
869
  }
870
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
747
871
  }
748
872
  } catch (error) {
749
873
  const detail = errorOf(error, 'VERIFY_FAILED', true);
750
874
  try {
751
- const capability = issueCapability(receipt, 'verification');
752
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
753
- scope: 'verification', operation: 'failed', error: detail
754
- }, consumedCapabilities);
875
+ if (receipt.verification === 'pending') {
876
+ const capability = issueCapability(receipt, 'verification');
877
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
878
+ scope: 'verification', operation: 'failed', error: detail
879
+ }, consumedCapabilities);
880
+ } else {
881
+ const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
882
+ receipt = updateReceipt(repoRoot, receipt, {
883
+ verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
884
+ });
885
+ }
755
886
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
887
+ } catch (writeError) {
888
+ const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
889
+ const hardError: FinalizationError = {
890
+ code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
891
+ message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
892
+ retryable: true
893
+ };
894
+ return terminalResult(taskId, receipt, {
895
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
896
+ }, changed, hardError);
897
+ }
898
+ try {
899
+ const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
900
+ receipt = warningComment.receipt;
901
+ if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
902
+ changed = changed || warningComment.changed;
756
903
  } catch { /* preserve the primary error */ }
757
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
904
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
905
+ }
906
+
907
+ let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
908
+ receipt = summary.receipt;
909
+ changed = changed || summary.changed;
910
+ if (summary.error) {
911
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
912
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
758
913
  }
759
914
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
760
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed);
915
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
761
916
  }
762
917
 
763
918
  async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
@@ -781,7 +936,8 @@ export {
781
936
  applyFinalizationReceiptMutation,
782
937
  applyTaskFinalization,
783
938
  issueCapability as createFinalizationCapability,
784
- readTaskFinalizationReceipt
939
+ readTaskFinalizationReceipt,
940
+ terminalResult
785
941
  };
786
942
  export type {
787
943
  FinalizationError,
@@ -1,8 +1,7 @@
1
1
  import {
2
2
  inspectOrchestrationStage,
3
3
  pauseOrchestration,
4
- planOrchestrationStageCompletion,
5
- readRun
4
+ planOrchestrationStageCompletion
6
5
  } from './orchestration.ts';
7
6
  import type {
8
7
  OrchestrationStageCompletion,
@@ -31,28 +30,15 @@ function failure(mode: LifecycleExecutionMode, code: string, message: string): L
31
30
  function inspectLifecycleExecution(
32
31
  taskRef: string,
33
32
  request: LifecycleExecutionRequest,
34
- options: Readonly<{ repoRoot?: string }> = {}
33
+ options: Readonly<{
34
+ repoRoot?: string;
35
+ now?: () => string;
36
+ }> = {}
35
37
  ): LifecycleExecutionResult {
36
38
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
37
39
  if (!resolved.ok) return failure(request.mode, resolved.code, resolved.message);
38
40
  if (request.mode === 'standalone') {
39
- try {
40
- const run = readRun(resolved.taskDir);
41
- if (run?.pendingDelegation) {
42
- return failure(
43
- request.mode,
44
- 'ORCHESTRATION_STANDALONE_BUSY',
45
- 'standalone lifecycle execution is blocked by a pending orchestration delegation'
46
- );
47
- }
48
- return { ok: true, mode: request.mode, completionPlan: null, error: null };
49
- } catch (error) {
50
- return failure(
51
- request.mode,
52
- 'ORCHESTRATION_STATE_INVALID',
53
- error instanceof Error ? error.message : String(error)
54
- );
55
- }
41
+ return { ok: true, mode: request.mode, completionPlan: null, error: null };
56
42
  }
57
43
 
58
44
  if (request.agent) {
@@ -84,7 +70,10 @@ function inspectLifecycleExecution(
84
70
  function validateLifecycleExecution(
85
71
  taskRef: string,
86
72
  request: LifecycleExecutionRequest,
87
- options: Readonly<{ repoRoot?: string }> = {}
73
+ options: Readonly<{
74
+ repoRoot?: string;
75
+ now?: () => string;
76
+ }> = {}
88
77
  ): LifecycleExecutionResult {
89
78
  const inspected = inspectLifecycleExecution(taskRef, request, options);
90
79
  if (inspected.ok || request.mode === 'standalone' || request.dryRun) return inspected;
@@ -58,7 +58,7 @@ type TaskLifecycleRequest =
58
58
  | { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
59
59
  | { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
60
60
  | { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
61
- | { taskRef: string; intent: 'recover-started'; agent: string; stage: 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code'; round: number; artifact: string; reason: string; dryRun?: boolean };
61
+ | { taskRef: string; intent: 'recover-started'; agent: string; auto: true; dryRun?: boolean };
62
62
 
63
63
  type HotState = 'active' | 'blocked' | 'completed';
64
64
  type SourceState = HotState | 'staging';
@@ -209,7 +209,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
209
209
  if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
210
210
  if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
211
211
  if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
212
- if (request.intent === 'recover-started') return { action: 'Recover Started', note: request.reason };
212
+ if (request.intent === 'recover-started') return { action: 'Recover Started', note: 'automatic recovery' };
213
213
  return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
214
214
  }
215
215