@fitlab-ai/agent-infra 0.11.2 → 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 (79) 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/manual-validation.js +22 -27
  8. package/dist/lib/internal/sandbox-control.js +1 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +6 -2
  11. package/dist/lib/platform/issue-comments.js +2 -31
  12. package/dist/lib/platform/pr-summary.js +7 -2
  13. package/dist/lib/sandbox/control/client.js +3 -44
  14. package/dist/lib/sandbox/control/executor.js +14 -79
  15. package/dist/lib/sandbox/control/finalization-response.js +3 -5
  16. package/dist/lib/sandbox/control/protocol.js +5 -64
  17. package/dist/lib/sandbox/control/server.js +29 -206
  18. package/dist/lib/sandbox/control/state.js +1 -7
  19. package/dist/lib/sandbox/control/task-view.js +1 -2
  20. package/dist/lib/task/control-authority.js +34 -36
  21. package/dist/lib/task/control-recovery.js +3 -42
  22. package/dist/lib/task/delegation-receipts.js +2 -2
  23. package/dist/lib/task/finalization-artifacts.js +1 -3
  24. package/dist/lib/task/finalization-state.js +1 -1
  25. package/dist/lib/task/finalization.js +53 -126
  26. package/dist/lib/task/lifecycle.js +1 -1
  27. package/dist/lib/task/manual-validation-receipt.js +8 -3
  28. package/dist/lib/task/orchestration.js +12 -6
  29. package/dist/lifecycle-build-manifest.json +3 -3
  30. package/lib/agent-clients/adapter.ts +46 -0
  31. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  32. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  33. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  34. package/lib/agent-clients/adapters/codex.ts +10 -0
  35. package/lib/internal/manual-validation.ts +22 -26
  36. package/lib/internal/sandbox-control.ts +0 -42
  37. package/lib/internal/task-lifecycle.ts +2 -2
  38. package/lib/internal/task-operation-registry.ts +7 -4
  39. package/lib/platform/issue-comments.ts +1 -35
  40. package/lib/platform/pr-summary.ts +10 -2
  41. package/lib/sandbox/control/client.ts +5 -68
  42. package/lib/sandbox/control/executor.ts +14 -81
  43. package/lib/sandbox/control/finalization-response.ts +3 -5
  44. package/lib/sandbox/control/protocol.ts +7 -83
  45. package/lib/sandbox/control/server.ts +30 -220
  46. package/lib/sandbox/control/state.ts +1 -9
  47. package/lib/sandbox/control/task-view.ts +1 -2
  48. package/lib/task/control-authority.ts +38 -47
  49. package/lib/task/control-recovery.ts +3 -43
  50. package/lib/task/delegation-receipts.ts +2 -2
  51. package/lib/task/finalization-artifacts.ts +1 -3
  52. package/lib/task/finalization-state.ts +1 -1
  53. package/lib/task/finalization.ts +57 -121
  54. package/lib/task/lifecycle.ts +2 -2
  55. package/lib/task/manual-validation-receipt.ts +8 -2
  56. package/lib/task/orchestration.ts +30 -19
  57. package/package.json +1 -1
  58. package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
  59. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
  60. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  61. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  62. package/templates/.agents/skills/complete-task/SKILL.en.md +6 -7
  63. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +6 -7
  64. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  65. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  66. package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
  67. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
  68. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  69. package/dist/lib/sandbox/control/workflow-executor.js +0 -142
  70. package/dist/lib/task/lifecycle-recovery.js +0 -549
  71. package/dist/lib/task/recovery-warning.js +0 -19
  72. package/dist/lib/task/workflow-command.js +0 -21
  73. package/dist/lib/task/workflow-dispatch.js +0 -213
  74. package/lib/sandbox/control/task-workflow.ts +0 -95
  75. package/lib/sandbox/control/workflow-executor.ts +0 -156
  76. package/lib/task/lifecycle-recovery.ts +0 -691
  77. package/lib/task/recovery-warning.ts +0 -21
  78. package/lib/task/workflow-command.ts +0 -22
  79. package/lib/task/workflow-dispatch.ts +0 -214
@@ -1,7 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { isCompletionEvidence } from './orchestration.js';
3
- import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.js';
4
- import { sameRecoveryWarning } from './recovery-warning.js';
5
2
  /** Normalize the command result once at the control transport boundary. */
6
3
  export function parseControlOutput(output) {
7
4
  if (output === null)
@@ -20,20 +17,10 @@ export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
20
17
  'completed', 'evidence-written', 'publish-authorized'
21
18
  ]);
22
19
  const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
23
- const ORCHESTRATION_INTENTS = [
24
- 'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
25
- 'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
26
- ];
27
20
  export const SANDBOX_CONTROL_RECOVERY_OPERATIONS = Object.freeze([
28
21
  ...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle', intent, class: 'lifecycle-mutation' })),
29
22
  { family: 'task-finalization', intent: 'complete', class: 'finalization' },
30
- ...ORCHESTRATION_INTENTS.map((intent) => ({
31
- family: 'task-orchestration',
32
- intent,
33
- class: intent === 'route.clean-completion' ? 'route.clean-completion' : intent === 'route.read' || intent === 'status' ? 'read-only' : 'orchestration'
34
- })),
35
23
  { family: 'task-create', intent: 'create', class: 'task-create' },
36
- ...TASK_WORKFLOW_OPERATIONS.map((intent) => ({ family: 'task-workflow', intent, class: intent === 'artifact-inspect' || intent === 'decision-next-id' ? 'read-only' : 'workflow' })),
37
24
  ...['open', 'close', 'verify'].map((intent) => ({ family: 'codex-controller', intent, class: 'codex-controller' }))
38
25
  ]);
39
26
  export function digestControlRecoveryIntent(family, intent) {
@@ -53,30 +40,8 @@ function domainEvidenceMatches(operation, result, domain) {
53
40
  return false;
54
41
  if (domain.consistent !== true)
55
42
  return false;
56
- if (operation.class === 'route.clean-completion') {
57
- const completion = domain.completionEvidence;
58
- const snapshot = domain.snapshot;
59
- const lastReviewedCommit = domain.lastReviewedCommit;
60
- return result.status === 'completed'
61
- && domain.status === 'completed'
62
- && domain.pendingDelegation === null
63
- && isCompletionEvidence(completion)
64
- && snapshot?.head === completion.head
65
- && snapshot.headTree === completion.headTree
66
- && snapshot.worktreeTree === completion.worktreeTree
67
- && lastReviewedCommit === completion.lastReviewedCommit;
68
- }
69
- if (operation.family === 'task-lifecycle' && operation.intent === 'recover-started') {
70
- const retryRequired = domain.recoveryState === 'retry-required'
71
- && sameRecoveryWarning(domain.warning, result.warning);
72
- return result.targetState === 'active'
73
- && ['applied', 'no-op'].includes(String(result.status))
74
- && domain.recovery === true
75
- && domain.targetState === 'active'
76
- && (domain.recoveryState === 'released' || retryRequired);
77
- }
78
- if (operation.class === 'read-only')
79
- return result.changed === false && domain.snapshotValid === true;
43
+ if (operation.family === 'task-lifecycle' && operation.intent === 'recover-started')
44
+ return false;
80
45
  return true;
81
46
  }
82
47
  export function classifySandboxControlRecovery(input) {
@@ -127,11 +92,7 @@ export function classifySandboxControlRecovery(input) {
127
92
  return {
128
93
  outcome: 'success',
129
94
  responseReconstructable: true,
130
- reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
131
- ? 'RECOVERY_ROUTE_COMPLETION_NOOP'
132
- : input.operation.intent === 'recover-started' && input.domain?.recoveryState === 'retry-required'
133
- ? 'RECOVERY_RELEASE_RETRY_REQUIRED'
134
- : 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
95
+ reasonCode: 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
135
96
  };
136
97
  }
137
98
  export function operationRecoveryBinding(requestId, generation, taskId, family, intent) {
@@ -149,7 +149,7 @@ function hasStatusBoundEvidence(receipt) {
149
149
  && host !== undefined
150
150
  && Number.isSafeInteger(host.stopRevision)
151
151
  && host.stopRevision > host.startRevision
152
- && host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
152
+ && host.consumer === receipt.id
153
153
  && exactText(host.consumedAt);
154
154
  }
155
155
  if (!exactText(receipt.agent)
@@ -405,7 +405,7 @@ function abortActivatedDelegation(receipt, event) {
405
405
  if (event.childId !== receipt.childId
406
406
  || !Number.isSafeInteger(event.stopRevision)
407
407
  || event.stopRevision <= receipt.hostEvidence.startRevision
408
- || event.consumer !== `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
408
+ || event.consumer !== receipt.id
409
409
  || !exactText(event.consumedAt)) {
410
410
  return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery stop evidence does not match the activated delegation');
411
411
  }
@@ -5,9 +5,7 @@ const COMPLETION_BACKFILL_FAMILIES = [
5
5
  'plan',
6
6
  'review-plan',
7
7
  'code',
8
- 'review-code',
9
- 'manual-validation',
10
- 'validation-run'
8
+ 'review-code'
11
9
  ];
12
10
  function inspectCompletionArtifacts(taskRef, options = {}) {
13
11
  const artifacts = [];
@@ -1,7 +1,7 @@
1
1
  export function taskFinalizationReceiptState(receipt) {
2
2
  if (receipt.lifecycle !== 'done' || receipt.taskComment === 'pending'
3
3
  || receipt.verification === 'pending' || receipt.summary === 'pending'
4
- || receipt.postSummaryVerification === 'pending' || receipt.warningProjection !== 'done')
4
+ || receipt.warningProjection !== 'done')
5
5
  return 'pending';
6
6
  if (receipt.lastError !== null || receipt.warnings.some((warning) => warning.status === 'open'))
7
7
  return 'unresolved';
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { syncPlatformComment } from '../platform/issue-comments.js';
5
5
  import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
6
+ import { backfillCompletionComments } from '../platform/completion-backfill.js';
6
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
7
8
  import { parseTaskFrontmatter } from './frontmatter.js';
8
9
  import { applyTaskLifecycle } from './lifecycle.js';
@@ -12,8 +13,8 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
12
13
  import { verifyTaskEvent } from './verification.js';
13
14
  import { projectFinalizationWarning } from './workflow-warning-intents.js';
14
15
  import { mergeOperationWarnings } from './operation-outcome.js';
15
- const RECEIPT_VERSION = 3;
16
- const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary', 'post-summary-verification'];
16
+ const RECEIPT_VERSION = 4;
17
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
17
18
  function finalizationRoot(repoRoot) {
18
19
  return path.join(repoRoot, '.agents', 'workspace', '.task-finalization');
19
20
  }
@@ -34,11 +35,11 @@ function failed(taskId, error, overrides = {}) {
34
35
  status: error.retryable ? 'blocked' : 'failed',
35
36
  changed: false,
36
37
  taskId,
38
+ backfill: null,
37
39
  lifecycle: null,
38
40
  taskComment: null,
39
41
  verification: null,
40
42
  summary: null,
41
- postSummaryVerification: null,
42
43
  completedSteps: [],
43
44
  pendingSteps: [...FINALIZATION_STEPS],
44
45
  result: error.retryable ? 'blocked' : 'failed',
@@ -58,10 +59,8 @@ function emptyReceipt(taskId, controlBinding) {
58
59
  taskComment: 'pending',
59
60
  verification: 'pending',
60
61
  summary: 'pending',
61
- postSummaryVerification: 'pending',
62
62
  warningProjection: 'done',
63
63
  warnings: [],
64
- summarySha256: null,
65
64
  ...(controlBinding ? { controlBinding } : {}),
66
65
  updatedAt: now(),
67
66
  lastError: null
@@ -97,10 +96,8 @@ function validateReceipt(value, taskId) {
97
96
  || !states.includes(String(receipt.taskComment))
98
97
  || !states.includes(String(receipt.verification))
99
98
  || !states.includes(String(receipt.summary))
100
- || !states.includes(String(receipt.postSummaryVerification))
101
99
  || !['pending', 'done'].includes(String(receipt.warningProjection))
102
100
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
103
- || (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
104
101
  || typeof receipt.updatedAt !== 'string'
105
102
  || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
106
103
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
@@ -154,8 +151,6 @@ function pendingSteps(receipt) {
154
151
  function stepState(receipt, step) {
155
152
  if (step === 'task-comment')
156
153
  return receipt.taskComment;
157
- if (step === 'post-summary-verification')
158
- return receipt.postSummaryVerification;
159
154
  return receipt[step];
160
155
  }
161
156
  function lifecycleStep(result) {
@@ -436,16 +431,13 @@ function readDeliverySummary(taskDir, taskId) {
436
431
  }
437
432
  async function syncPendingSummary(input) {
438
433
  let { receipt } = input;
439
- if (receipt.summary !== 'pending') {
440
- return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
441
- }
442
434
  try {
443
435
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
444
436
  if (!resolved.ok || resolved.state !== 'completed')
445
437
  throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
446
438
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
447
439
  receipt = updateReceipt(input.repoRoot, receipt, {
448
- summary: 'skipped', postSummaryVerification: 'skipped', summarySha256: null, lastError: null
440
+ summary: 'skipped', lastError: null
449
441
  });
450
442
  return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
451
443
  }
@@ -460,8 +452,7 @@ async function syncPendingSummary(input) {
460
452
  const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
461
453
  const warnings = resolveStepWarnings(receipt, 'summary');
462
454
  receipt = updateReceipt(input.repoRoot, receipt, {
463
- summary: skipped ? 'skipped' : 'done', postSummaryVerification: skipped ? 'skipped' : receipt.postSummaryVerification,
464
- summarySha256: skipped ? null : summary.sha256,
455
+ summary: skipped ? 'skipped' : 'done',
465
456
  taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
466
457
  warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
467
458
  warnings,
@@ -488,14 +479,9 @@ async function syncPendingSummary(input) {
488
479
  return { receipt, step: { status: 'blocked', changed: false, error }, changed: false, error };
489
480
  }
490
481
  }
491
- function clearDeliverySummary(taskDir) {
492
- const file = path.join(taskDir, '.delivery-summary.json');
493
- if (fs.existsSync(file))
494
- fs.unlinkSync(file);
495
- }
496
482
  function terminalResult(taskId, receipt, steps, changed, error = null) {
497
483
  const pending = pendingSteps(receipt);
498
- const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary, steps.postSummaryVerification].some((step) => step?.status === 'blocked');
484
+ const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
499
485
  const warnings = openWarnings(receipt);
500
486
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
501
487
  const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
@@ -503,18 +489,18 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
503
489
  status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
504
490
  changed,
505
491
  taskId,
492
+ backfill: steps.backfill ?? null,
506
493
  lifecycle: steps.lifecycle ?? null,
507
494
  taskComment: steps.taskComment ?? null,
508
495
  verification: steps.verification ?? null,
509
496
  summary: steps.summary ?? null,
510
- postSummaryVerification: steps.postSummaryVerification ?? null,
511
497
  completedSteps: completedSteps(receipt),
512
498
  pendingSteps: pending,
513
499
  result: hardError
514
500
  ? (error?.retryable ? 'blocked' : 'failed')
515
- : postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
501
+ : postLifecyclePending
516
502
  ? 'completed_with_warnings'
517
- : pending.length === 0 || postLifecyclePending
503
+ : pending.length === 0
518
504
  ? 'completed'
519
505
  : blocked
520
506
  ? 'blocked'
@@ -526,6 +512,7 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
526
512
  async function applyUnderLock(request, taskId, options) {
527
513
  const repoRoot = path.resolve(options.repoRoot);
528
514
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
515
+ const backfill = options.backfill ?? backfillCompletionComments;
529
516
  const commentSync = options.commentSync ?? syncPlatformComment;
530
517
  const verify = options.verify ?? verifyTaskEvent;
531
518
  const consumedCapabilities = new Set();
@@ -562,8 +549,36 @@ async function applyUnderLock(request, taskId, options) {
562
549
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_PREFLIGHT_FAILED', true));
563
550
  }
564
551
  }
565
- let lifecycleResult = null;
566
552
  let changed = false;
553
+ let backfillResult = null;
554
+ try {
555
+ const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
556
+ backfillResult = commentStep(result);
557
+ const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
558
+ if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
559
+ const detail = backfillResult.error ?? {
560
+ code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
561
+ };
562
+ return failed(taskId, detail, {
563
+ backfill: backfillResult,
564
+ completedSteps: completedSteps(receipt),
565
+ pendingSteps: pendingSteps(receipt)
566
+ });
567
+ }
568
+ changed = result.changed;
569
+ if (result.changed && receipt.taskComment !== 'pending') {
570
+ receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
571
+ }
572
+ }
573
+ catch (error) {
574
+ const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
575
+ return failed(taskId, detail, {
576
+ backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
577
+ completedSteps: completedSteps(receipt),
578
+ pendingSteps: pendingSteps(receipt)
579
+ });
580
+ }
581
+ let lifecycleResult = null;
567
582
  try {
568
583
  const resolved = resolveTaskRef(taskId, { repoRoot });
569
584
  const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
@@ -575,6 +590,7 @@ async function applyUnderLock(request, taskId, options) {
575
590
  };
576
591
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
577
592
  return terminalResult(taskId, receipt, {
593
+ backfill: backfillResult,
578
594
  lifecycle: { status: 'failed', changed: false, error: detail },
579
595
  taskComment: null,
580
596
  verification: null
@@ -589,6 +605,7 @@ async function applyUnderLock(request, taskId, options) {
589
605
  };
590
606
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
591
607
  return terminalResult(taskId, receipt, {
608
+ backfill: backfillResult,
592
609
  lifecycle: { status: 'failed', changed: false, error: detail },
593
610
  taskComment: null,
594
611
  verification: null
@@ -604,9 +621,9 @@ async function applyUnderLock(request, taskId, options) {
604
621
  lifecycleResult = lifecycleStep(result);
605
622
  if (result.status !== 'applied' && result.status !== 'no-op') {
606
623
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
607
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
624
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
608
625
  }
609
- changed = result.changed;
626
+ changed = changed || result.changed;
610
627
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
611
628
  }
612
629
  }
@@ -616,7 +633,7 @@ async function applyUnderLock(request, taskId, options) {
616
633
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
617
634
  }
618
635
  catch { /* preserve the primary error */ }
619
- return failed(taskId, detail, { lifecycle: lifecycleResult });
636
+ return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
620
637
  }
621
638
  let taskComment = null;
622
639
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
@@ -627,7 +644,7 @@ async function applyUnderLock(request, taskId, options) {
627
644
  taskComment = initialComment.step;
628
645
  changed = changed || initialComment.changed;
629
646
  if (initialComment.error) {
630
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
647
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
631
648
  }
632
649
  let verification = null;
633
650
  try {
@@ -650,7 +667,7 @@ async function applyUnderLock(request, taskId, options) {
650
667
  taskComment = finalComment.step;
651
668
  changed = changed || finalComment.changed;
652
669
  if (finalComment.error) {
653
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
670
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
654
671
  }
655
672
  }
656
673
  else {
@@ -678,7 +695,7 @@ async function applyUnderLock(request, taskId, options) {
678
695
  taskComment = warningComment.step;
679
696
  changed = changed || warningComment.changed;
680
697
  }
681
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
698
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
682
699
  }
683
700
  }
684
701
  catch (error) {
@@ -706,7 +723,7 @@ async function applyUnderLock(request, taskId, options) {
706
723
  retryable: true
707
724
  };
708
725
  return terminalResult(taskId, receipt, {
709
- lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
726
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
710
727
  }, changed, hardError);
711
728
  }
712
729
  try {
@@ -717,107 +734,17 @@ async function applyUnderLock(request, taskId, options) {
717
734
  changed = changed || warningComment.changed;
718
735
  }
719
736
  catch { /* preserve the primary error */ }
720
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
737
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
721
738
  }
722
- const summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
739
+ let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
723
740
  receipt = summary.receipt;
724
741
  changed = changed || summary.changed;
725
742
  if (summary.error) {
726
743
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
727
- const warningComment = await syncPendingTaskComment({
728
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
729
- });
730
- receipt = warningComment.receipt;
731
- if (warningComment.step.status !== 'no-op')
732
- taskComment = warningComment.step;
733
- changed = changed || warningComment.changed;
734
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
735
- }
736
- let postSummaryVerification = null;
737
- if (receipt.postSummaryVerification === 'skipped') {
738
- postSummaryVerification = { status: 'skipped', changed: false, error: null };
739
- }
740
- else if (receipt.postSummaryVerification !== 'pending') {
741
- const replay = await commentSync(taskId, {
742
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
743
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
744
- });
745
- postSummaryVerification = commentStep(replay);
746
- if (replay.status !== 'applied' && replay.status !== 'no-op') {
747
- const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
748
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
749
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
750
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
751
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
752
- }
753
- }
754
- else {
755
- try {
756
- const summaryVerification = await commentSync(taskId, {
757
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
758
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
759
- });
760
- if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
761
- const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
762
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
763
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
764
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
765
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
766
- receipt = warningComment.receipt;
767
- if (warningComment.step.status !== 'no-op')
768
- taskComment = warningComment.step;
769
- changed = changed || warningComment.changed;
770
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
771
- }
772
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
773
- postSummaryVerification = verificationStep(result);
774
- if (result.status === 'pass') {
775
- const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
776
- const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
777
- receipt = updateReceipt(repoRoot, receipt, {
778
- postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
779
- warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
780
- });
781
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
782
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
783
- receipt = warningComment.receipt;
784
- if (warningComment.step.status !== 'no-op')
785
- taskComment = warningComment.step;
786
- changed = changed || warningComment.changed;
787
- }
788
- else {
789
- const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
790
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
791
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
792
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
793
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
794
- receipt = warningComment.receipt;
795
- if (warningComment.step.status !== 'no-op')
796
- taskComment = warningComment.step;
797
- changed = changed || warningComment.changed;
798
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
799
- }
800
- }
801
- catch (error) {
802
- const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
803
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
804
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
805
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
806
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
807
- receipt = warningComment.receipt;
808
- if (warningComment.step.status !== 'no-op')
809
- taskComment = warningComment.step;
810
- changed = changed || warningComment.changed;
811
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
812
- }
744
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
813
745
  }
814
746
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
815
- if (receipt.summary !== 'pending' && receipt.postSummaryVerification !== 'pending' && receipt.warningProjection === 'done' && openWarnings(receipt).length === 0) {
816
- const resolved = resolveTaskRef(taskId, { repoRoot });
817
- if (resolved.ok && resolved.state === 'completed')
818
- clearDeliverySummary(resolved.taskDir);
819
- }
820
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
747
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
821
748
  }
822
749
  async function applyTaskFinalization(request, options) {
823
750
  if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
@@ -151,7 +151,7 @@ function actionAndNote(request, restoredFiles = 0) {
151
151
  if (request.intent === 'close-dependabot')
152
152
  return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
153
153
  if (request.intent === 'recover-started')
154
- return { action: 'Recover Started', note: request.reason };
154
+ return { action: 'Recover Started', note: 'automatic recovery' };
155
155
  return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
156
156
  }
157
157
  function matchingCompletion(content, request, restoredFiles = 0) {
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
4
4
  import { MANUAL_VALIDATION_ARTIFACT as ARTIFACT, MANUAL_VALIDATION_SHA40 as SHA40, MANUAL_VALIDATION_SHA64 as SHA64, MANUAL_VALIDATION_TASK_ID as TASK_ID, exactKeys, isRecord, validTimestamp, writeJsonAtomic } from './manual-validation-shared.js';
5
5
  const MANUAL_VALIDATION_RECEIPT_SCHEMA = 'agent-infra/manual-validation-receipt';
6
6
  const MANUAL_VALIDATION_RECEIPT_VERSION = 1;
7
+ const MANUAL_VALIDATION_RECEIPT_PLACEHOLDER = '[[manual-validation-receipt]]';
7
8
  function invalid(message) {
8
9
  return { ok: false, error: { code: 'MANUAL_VALIDATION_RECEIPT_INVALID', message } };
9
10
  }
@@ -33,12 +34,16 @@ function manualValidationFinalSummaryDigest(body) {
33
34
  const projection = heading
34
35
  ? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
35
36
  : body;
36
- const preimage = projection.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
37
+ const preimage = projection
38
+ .replace(/\n*<!--\s*canonical-pr-change-report\s*-->\s*/gu, '')
39
+ .replace(/<!--\s*manual-validation-receipt:\s*[\s\S]*?-->/gu, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER)
40
+ .replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
37
41
  return createHash('sha256').update(preimage, 'utf8').digest('hex');
38
42
  }
39
43
  function manualValidationFinalSummaryProjectionMatches(body, receipt) {
40
44
  const identity = `transaction=${receipt.transactionId}; receipt=${receipt.receiptDigest}; evidence=${receipt.evidenceDigest}; head=${receipt.prHeadSha}`;
41
- return body.includes(identity) && manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
45
+ return (body.includes(identity) || body.includes(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER))
46
+ && manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
42
47
  }
43
48
  function validateManualValidationReceipt(value, expected) {
44
49
  if (!isRecord(value))
@@ -101,5 +106,5 @@ function readManualValidationReceipt(taskDir, expected) {
101
106
  }
102
107
  return validateManualValidationReceipt(value, expected);
103
108
  }
104
- export { MANUAL_VALIDATION_RECEIPT_SCHEMA, MANUAL_VALIDATION_RECEIPT_VERSION, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, manualValidationReceiptDigest, manualValidationReceiptPath, readManualValidationReceipt, validateManualValidationReceipt, writeManualValidationReceiptAtomic };
109
+ export { MANUAL_VALIDATION_RECEIPT_SCHEMA, MANUAL_VALIDATION_RECEIPT_VERSION, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, manualValidationReceiptDigest, manualValidationReceiptPath, readManualValidationReceipt, validateManualValidationReceipt, writeManualValidationReceiptAtomic };
105
110
  //# sourceMappingURL=manual-validation-receipt.js.map
@@ -1075,6 +1075,14 @@ function recoverPreparedOrchestrationDelegation(taskRef, options = {}) {
1075
1075
  return failed('ORCHESTRATION_PREPARED_RECOVERY_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
1076
1076
  }
1077
1077
  }
1078
+ function recoveryReceiptMatches(receipt, taskId, event) {
1079
+ return receipt.taskId === taskId
1080
+ && receipt.stage === event.stage
1081
+ && receipt.round === event.round
1082
+ && receipt.artifact === event.artifact
1083
+ && receipt.childId === event.childId
1084
+ && normalizeAgentToken(event.startedAgent) === normalizeAgentToken(receipt.client);
1085
+ }
1078
1086
  function recoverActivatedOrchestrationDelegationUnderLock(taskRef, event, options = {}) {
1079
1087
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1080
1088
  if (!resolved.ok)
@@ -1089,12 +1097,7 @@ function recoverActivatedOrchestrationDelegationUnderLock(taskRef, event, option
1089
1097
  const receipt = matches.find((candidate) => candidate.id === event.receiptId);
1090
1098
  if (!receipt)
1091
1099
  return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
1092
- if (receipt.taskId !== resolved.taskId
1093
- || receipt.stage !== event.stage
1094
- || receipt.round !== event.round
1095
- || receipt.artifact !== event.artifact
1096
- || receipt.childId !== event.childId
1097
- || normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)) {
1100
+ if (!recoveryReceiptMatches(receipt, resolved.taskId, event)) {
1098
1101
  return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
1099
1102
  }
1100
1103
  if (receipt.status === 'aborted' && run.pendingDelegation === null) {
@@ -1106,6 +1109,9 @@ function recoverActivatedOrchestrationDelegationUnderLock(taskRef, event, option
1106
1109
  if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
1107
1110
  return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
1108
1111
  }
1112
+ if (run.status !== 'running' || run.pause !== null) {
1113
+ return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'activated recovery cannot replace an unrelated orchestration pause', resolved.taskId);
1114
+ }
1109
1115
  const aborted = abortActivatedDelegation(receipt, {
1110
1116
  childId: event.childId,
1111
1117
  stopRevision: event.stopRevision,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.2",
4
- "sourceInputHash": "0a1ffa8b90a0d157f9b2aabea6a43f946d68a622bde337befe71059f63b3812b",
5
- "internalExecutableBuildHash": "b6742e1b5d1b997bf7e21e644bfde2bafaa608f7ede7ca308ff61c2cd19da831"
3
+ "packageVersion": "0.11.3",
4
+ "sourceInputHash": "99efbc575d475879773bf1676cc2de5ea628b9c193f69a9812a723313ba5ae7f",
5
+ "internalExecutableBuildHash": "6912a66863d013c476699b88cc1d4154082971164cbae3f98ea79dda39a46e7a"
6
6
  }
@@ -7,6 +7,10 @@ import type {
7
7
  AgentClientCapabilityMap,
8
8
  AgentClientId
9
9
  } from './types.ts';
10
+ import type {
11
+ OrchestrationOptions,
12
+ OrchestrationResult
13
+ } from '../task/orchestration.ts';
10
14
  import {
11
15
  SANDBOX_HOOK_PHASES
12
16
  } from '../sandbox/tool-types.ts';
@@ -43,6 +47,41 @@ type AgentClientDelegationEvidence = Readonly<{
43
47
  actualReasoningEffort: 'host-event' | 'app-server' | 'spawn-ack' | 'unavailable';
44
48
  }>;
45
49
 
50
+ type AgentClientLifecycleRecoveryRequest = Readonly<{
51
+ taskRef: string;
52
+ intent: 'recover-started';
53
+ agent: string;
54
+ auto: true;
55
+ }>;
56
+
57
+ type AgentClientLifecycleRecoveryResult = Readonly<{
58
+ status: 'applied' | 'no-op' | 'owner-unknown' | 'conflict';
59
+ changed: boolean;
60
+ targetState: 'active';
61
+ requestRef: string;
62
+ intent: 'recover-started';
63
+ taskId: string | null;
64
+ receiptId: string | null;
65
+ childId: string | null;
66
+ error: Readonly<{ code: string; message: string }> | null;
67
+ }>;
68
+
69
+ type AgentClientOrchestrationAdapter = Readonly<{
70
+ prepareDelegation?: (
71
+ taskRef: string,
72
+ input: Readonly<{
73
+ client: AgentClientId;
74
+ requestedModel?: string;
75
+ requestedReasoningEffort?: string;
76
+ }>,
77
+ options?: Readonly<{ repoRoot?: string; orchestrationOptions?: OrchestrationOptions }>
78
+ ) => Promise<OrchestrationResult>;
79
+ recoverStarted?: (
80
+ request: AgentClientLifecycleRecoveryRequest,
81
+ options: Readonly<{ repoRoot: string }>
82
+ ) => AgentClientLifecycleRecoveryResult | Promise<AgentClientLifecycleRecoveryResult>;
83
+ }>;
84
+
46
85
  type AgentClientSeedCommand = Readonly<{
47
86
  templates: Readonly<{
48
87
  en: string;
@@ -75,6 +114,7 @@ type AgentClientAdapter = Readonly<{
75
114
  capabilities: AgentClientCapabilities;
76
115
  modelSelection: AgentClientModelSelectionContext;
77
116
  delegationEvidence: AgentClientDelegationEvidence;
117
+ orchestrationAdapter?: AgentClientOrchestrationAdapter;
78
118
  project: AgentClientProjectDescriptor;
79
119
  sandbox: AgentClientSandboxDescriptor;
80
120
  }>;
@@ -628,6 +668,9 @@ function defineAgentClientAdapter(
628
668
  capabilities: Object.freeze(capabilities),
629
669
  modelSelection: frozenModelSelection,
630
670
  delegationEvidence: Object.freeze({ ...evidence }),
671
+ ...(candidate.orchestrationAdapter === undefined
672
+ ? {}
673
+ : { orchestrationAdapter: Object.freeze({ ...candidate.orchestrationAdapter }) }),
631
674
  project: Object.freeze({
632
675
  ownedPathPrefixes: Object.freeze(paths),
633
676
  ...projectAssets,
@@ -650,6 +693,9 @@ export type {
650
693
  AgentClientCapabilities,
651
694
  AgentClientCustomCommandDescriptor,
652
695
  AgentClientDelegationEvidence,
696
+ AgentClientLifecycleRecoveryRequest,
697
+ AgentClientLifecycleRecoveryResult,
698
+ AgentClientOrchestrationAdapter,
653
699
  AgentClientManifestEntry,
654
700
  AgentClientModelSelectionContext,
655
701
  AgentClientProjectDescriptor,