@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
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
4
4
 
5
5
  import { syncPlatformComment } from '../platform/issue-comments.ts';
6
6
  import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
7
+ import { backfillCompletionComments } from '../platform/completion-backfill.ts';
7
8
  import type { PlatformResult } from '../platform/types.ts';
8
9
  import { taskIssueIdentity } from '../platform/task-identities.ts';
9
10
  import { parseTaskFrontmatter } from './frontmatter.ts';
@@ -25,8 +26,8 @@ import {
25
26
  type OperationWarningSeverity
26
27
  } from './operation-outcome.ts';
27
28
 
28
- const RECEIPT_VERSION = 3 as const;
29
- const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary', 'post-summary-verification'] as const;
29
+ const RECEIPT_VERSION = 4 as const;
30
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
30
31
  type FinalizationStep = typeof FINALIZATION_STEPS[number];
31
32
  type FinalizationStepState = 'pending' | 'done' | 'skipped';
32
33
  type FinalizationError = { code: string; message: string; retryable: boolean };
@@ -61,6 +62,7 @@ type TaskFinalizationOptions = Readonly<{
61
62
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
62
63
  metadataProvider?: TaskLifecycleOptions['metadataProvider'];
63
64
  lifecycle?: typeof applyTaskLifecycle;
65
+ backfill?: typeof backfillCompletionComments;
64
66
  commentSync?: typeof syncPlatformComment;
65
67
  verify?: typeof verifyTaskEvent;
66
68
  preflight?: typeof verifyTaskEvent;
@@ -76,10 +78,8 @@ type TaskFinalizationReceipt = Readonly<{
76
78
  taskComment: FinalizationStepState;
77
79
  verification: FinalizationStepState;
78
80
  summary: FinalizationStepState;
79
- postSummaryVerification: FinalizationStepState;
80
81
  warningProjection: WarningProjectionState;
81
82
  warnings: readonly FinalizationWarning[];
82
- summarySha256: string | null;
83
83
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
84
84
  updatedAt: string;
85
85
  lastError: FinalizationError | null;
@@ -104,11 +104,11 @@ type TaskFinalizationResult = Readonly<{
104
104
  status: 'completed' | 'failed' | 'blocked';
105
105
  changed: boolean;
106
106
  taskId: string | null;
107
+ backfill: TaskFinalizationStep | null;
107
108
  lifecycle: TaskFinalizationStep | null;
108
109
  taskComment: TaskFinalizationStep | null;
109
110
  verification: TaskFinalizationStep | null;
110
111
  summary: TaskFinalizationStep | null;
111
- postSummaryVerification: TaskFinalizationStep | null;
112
112
  completedSteps: readonly FinalizationStep[];
113
113
  pendingSteps: readonly FinalizationStep[];
114
114
  result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
@@ -144,11 +144,11 @@ function failed(
144
144
  status: error.retryable ? 'blocked' : 'failed',
145
145
  changed: false,
146
146
  taskId,
147
+ backfill: null,
147
148
  lifecycle: null,
148
149
  taskComment: null,
149
150
  verification: null,
150
151
  summary: null,
151
- postSummaryVerification: null,
152
152
  completedSteps: [],
153
153
  pendingSteps: [...FINALIZATION_STEPS],
154
154
  result: error.retryable ? 'blocked' : 'failed',
@@ -169,10 +169,8 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
169
169
  taskComment: 'pending',
170
170
  verification: 'pending',
171
171
  summary: 'pending',
172
- postSummaryVerification: 'pending',
173
172
  warningProjection: 'done',
174
173
  warnings: [],
175
- summarySha256: null,
176
174
  ...(controlBinding ? { controlBinding } : {}),
177
175
  updatedAt: now(),
178
176
  lastError: null
@@ -208,10 +206,8 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
208
206
  || !states.includes(String(receipt.taskComment))
209
207
  || !states.includes(String(receipt.verification))
210
208
  || !states.includes(String(receipt.summary))
211
- || !states.includes(String(receipt.postSummaryVerification))
212
209
  || !['pending', 'done'].includes(String(receipt.warningProjection))
213
210
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
214
- || (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
215
211
  || typeof receipt.updatedAt !== 'string'
216
212
  || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
217
213
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
@@ -250,7 +246,7 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
250
246
  function updateReceipt(
251
247
  repoRoot: string,
252
248
  receipt: TaskFinalizationReceipt,
253
- patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification' | 'warningProjection' | 'warnings' | 'summarySha256' | 'controlBinding' | 'lastError'>>
249
+ patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'warningProjection' | 'warnings' | 'controlBinding' | 'lastError'>>
254
250
  ): TaskFinalizationReceipt {
255
251
  const current = readReceipt(repoRoot, receipt.taskId);
256
252
  if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
@@ -272,7 +268,6 @@ function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
272
268
 
273
269
  function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
274
270
  if (step === 'task-comment') return receipt.taskComment;
275
- if (step === 'post-summary-verification') return receipt.postSummaryVerification;
276
271
  return receipt[step];
277
272
  }
278
273
 
@@ -598,15 +593,12 @@ async function syncPendingSummary(input: {
598
593
  commentSync: typeof syncPlatformComment;
599
594
  }): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
600
595
  let { receipt } = input;
601
- if (receipt.summary !== 'pending') {
602
- return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
603
- }
604
596
  try {
605
597
  const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
606
598
  if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
607
599
  if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
608
600
  receipt = updateReceipt(input.repoRoot, receipt, {
609
- summary: 'skipped', postSummaryVerification: 'skipped', summarySha256: null, lastError: null
601
+ summary: 'skipped', lastError: null
610
602
  });
611
603
  return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
612
604
  }
@@ -621,8 +613,7 @@ async function syncPendingSummary(input: {
621
613
  const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
622
614
  const warnings = resolveStepWarnings(receipt, 'summary');
623
615
  receipt = updateReceipt(input.repoRoot, receipt, {
624
- summary: skipped ? 'skipped' : 'done', postSummaryVerification: skipped ? 'skipped' : receipt.postSummaryVerification,
625
- summarySha256: skipped ? null : summary.sha256,
616
+ summary: skipped ? 'skipped' : 'done',
626
617
  taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
627
618
  warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
628
619
  warnings,
@@ -648,20 +639,15 @@ async function syncPendingSummary(input: {
648
639
  }
649
640
  }
650
641
 
651
- function clearDeliverySummary(taskDir: string): void {
652
- const file = path.join(taskDir, '.delivery-summary.json');
653
- if (fs.existsSync(file)) fs.unlinkSync(file);
654
- }
655
-
656
642
  function terminalResult(
657
643
  taskId: string,
658
644
  receipt: TaskFinalizationReceipt,
659
- steps: Partial<Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification'>>,
645
+ steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
660
646
  changed: boolean,
661
647
  error: FinalizationError | null = null
662
648
  ): TaskFinalizationResult {
663
649
  const pending = pendingSteps(receipt);
664
- const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary, steps.postSummaryVerification].some((step) => step?.status === 'blocked');
650
+ const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
665
651
  const warnings = openWarnings(receipt);
666
652
  const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
667
653
  const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
@@ -669,18 +655,18 @@ function terminalResult(
669
655
  status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
670
656
  changed,
671
657
  taskId,
658
+ backfill: steps.backfill ?? null,
672
659
  lifecycle: steps.lifecycle ?? null,
673
660
  taskComment: steps.taskComment ?? null,
674
661
  verification: steps.verification ?? null,
675
662
  summary: steps.summary ?? null,
676
- postSummaryVerification: steps.postSummaryVerification ?? null,
677
663
  completedSteps: completedSteps(receipt),
678
664
  pendingSteps: pending,
679
665
  result: hardError
680
666
  ? (error?.retryable ? 'blocked' : 'failed')
681
- : postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
667
+ : postLifecyclePending
682
668
  ? 'completed_with_warnings'
683
- : pending.length === 0 || postLifecyclePending
669
+ : pending.length === 0
684
670
  ? 'completed'
685
671
  : blocked
686
672
  ? 'blocked'
@@ -697,6 +683,7 @@ async function applyUnderLock(
697
683
  ): Promise<TaskFinalizationResult> {
698
684
  const repoRoot = path.resolve(options.repoRoot);
699
685
  const lifecycle = options.lifecycle ?? applyTaskLifecycle;
686
+ const backfill = options.backfill ?? backfillCompletionComments;
700
687
  const commentSync = options.commentSync ?? syncPlatformComment;
701
688
  const verify = options.verify ?? verifyTaskEvent;
702
689
  const consumedCapabilities = new Set<string>();
@@ -736,8 +723,36 @@ async function applyUnderLock(
736
723
  }
737
724
  }
738
725
 
739
- let lifecycleResult: TaskFinalizationStep | null = null;
740
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;
741
756
  try {
742
757
  const resolved = resolveTaskRef(taskId, { repoRoot });
743
758
  const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
@@ -749,6 +764,7 @@ async function applyUnderLock(
749
764
  };
750
765
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
751
766
  return terminalResult(taskId, receipt, {
767
+ backfill: backfillResult,
752
768
  lifecycle: { status: 'failed', changed: false, error: detail },
753
769
  taskComment: null,
754
770
  verification: null
@@ -763,6 +779,7 @@ async function applyUnderLock(
763
779
  };
764
780
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
765
781
  return terminalResult(taskId, receipt, {
782
+ backfill: backfillResult,
766
783
  lifecycle: { status: 'failed', changed: false, error: detail },
767
784
  taskComment: null,
768
785
  verification: null
@@ -780,15 +797,15 @@ async function applyUnderLock(
780
797
  lifecycleResult = lifecycleStep(result);
781
798
  if (result.status !== 'applied' && result.status !== 'no-op') {
782
799
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
783
- 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);
784
801
  }
785
- changed = result.changed;
802
+ changed = changed || result.changed;
786
803
  receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
787
804
  }
788
805
  } catch (error) {
789
806
  const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
790
807
  try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
791
- return failed(taskId, detail, { lifecycle: lifecycleResult });
808
+ return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
792
809
  }
793
810
 
794
811
  let taskComment: TaskFinalizationStep | null = null;
@@ -800,7 +817,7 @@ async function applyUnderLock(
800
817
  taskComment = initialComment.step;
801
818
  changed = changed || initialComment.changed;
802
819
  if (initialComment.error) {
803
- 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);
804
821
  }
805
822
 
806
823
  let verification: TaskFinalizationStep | null = null;
@@ -826,7 +843,7 @@ async function applyUnderLock(
826
843
  if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
827
844
  changed = changed || finalComment.changed;
828
845
  if (finalComment.error) {
829
- 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);
830
847
  }
831
848
  } else {
832
849
  receipt = updateReceipt(repoRoot, receipt, { lastError: null });
@@ -850,7 +867,7 @@ async function applyUnderLock(
850
867
  if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
851
868
  changed = changed || warningComment.changed;
852
869
  }
853
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
870
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
854
871
  }
855
872
  } catch (error) {
856
873
  const detail = errorOf(error, 'VERIFY_FAILED', true);
@@ -875,7 +892,7 @@ async function applyUnderLock(
875
892
  retryable: true
876
893
  };
877
894
  return terminalResult(taskId, receipt, {
878
- lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
895
+ backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
879
896
  }, changed, hardError);
880
897
  }
881
898
  try {
@@ -884,99 +901,18 @@ async function applyUnderLock(
884
901
  if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
885
902
  changed = changed || warningComment.changed;
886
903
  } catch { /* preserve the primary error */ }
887
- 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);
888
905
  }
889
906
 
890
- const summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
907
+ let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
891
908
  receipt = summary.receipt;
892
909
  changed = changed || summary.changed;
893
910
  if (summary.error) {
894
911
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
895
- const warningComment = await syncPendingTaskComment({
896
- repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
897
- });
898
- receipt = warningComment.receipt;
899
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
900
- changed = changed || warningComment.changed;
901
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
902
- }
903
-
904
- let postSummaryVerification: TaskFinalizationStep | null = null;
905
- if (receipt.postSummaryVerification === 'skipped') {
906
- postSummaryVerification = { status: 'skipped', changed: false, error: null };
907
- } else if (receipt.postSummaryVerification !== 'pending') {
908
- const replay = await commentSync(taskId, {
909
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
910
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
911
- });
912
- postSummaryVerification = commentStep(replay);
913
- if (replay.status !== 'applied' && replay.status !== 'no-op') {
914
- const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
915
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
916
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
917
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
918
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
919
- }
920
- } else {
921
- try {
922
- const summaryVerification = await commentSync(taskId, {
923
- kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
924
- summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
925
- });
926
- if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
927
- const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
928
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
929
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
930
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
931
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
932
- receipt = warningComment.receipt;
933
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
934
- changed = changed || warningComment.changed;
935
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
936
- }
937
- const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
938
- postSummaryVerification = verificationStep(result);
939
- if (result.status === 'pass') {
940
- const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
941
- const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
942
- receipt = updateReceipt(repoRoot, receipt, {
943
- postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
944
- warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
945
- });
946
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
947
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
948
- receipt = warningComment.receipt;
949
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
950
- changed = changed || warningComment.changed;
951
- } else {
952
- const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
953
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
954
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
955
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
956
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
957
- receipt = warningComment.receipt;
958
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
959
- changed = changed || warningComment.changed;
960
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
961
- }
962
- } catch (error) {
963
- const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
964
- const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
965
- receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
966
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
967
- const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
968
- receipt = warningComment.receipt;
969
- if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
970
- changed = changed || warningComment.changed;
971
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
972
- }
912
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
973
913
  }
974
914
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
975
- if (receipt.summary !== 'pending' && receipt.postSummaryVerification !== 'pending' && receipt.warningProjection === 'done' && openWarnings(receipt).length === 0) {
976
- const resolved = resolveTaskRef(taskId, { repoRoot });
977
- if (resolved.ok && resolved.state === 'completed') clearDeliverySummary(resolved.taskDir);
978
- }
979
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
915
+ return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
980
916
  }
981
917
 
982
918
  async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
@@ -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
 
@@ -14,6 +14,7 @@ import {
14
14
 
15
15
  const MANUAL_VALIDATION_RECEIPT_SCHEMA = 'agent-infra/manual-validation-receipt';
16
16
  const MANUAL_VALIDATION_RECEIPT_VERSION = 1;
17
+ const MANUAL_VALIDATION_RECEIPT_PLACEHOLDER = '[[manual-validation-receipt]]';
17
18
 
18
19
  type ManualValidationReceipt = Readonly<{
19
20
  schema: typeof MANUAL_VALIDATION_RECEIPT_SCHEMA;
@@ -73,13 +74,17 @@ function manualValidationFinalSummaryDigest(body: string): string {
73
74
  const projection = heading
74
75
  ? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
75
76
  : body;
76
- const preimage = projection.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
77
+ const preimage = projection
78
+ .replace(/\n*<!--\s*canonical-pr-change-report\s*-->\s*/gu, '')
79
+ .replace(/<!--\s*manual-validation-receipt:\s*[\s\S]*?-->/gu, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER)
80
+ .replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
77
81
  return createHash('sha256').update(preimage, 'utf8').digest('hex');
78
82
  }
79
83
 
80
84
  function manualValidationFinalSummaryProjectionMatches(body: string, receipt: ManualValidationReceipt): boolean {
81
85
  const identity = `transaction=${receipt.transactionId}; receipt=${receipt.receiptDigest}; evidence=${receipt.evidenceDigest}; head=${receipt.prHeadSha}`;
82
- return body.includes(identity) && manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
86
+ return (body.includes(identity) || body.includes(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER))
87
+ && manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
83
88
  }
84
89
 
85
90
  function validateManualValidationReceipt(value: unknown, expected?: ManualValidationReceiptIdentity): ManualValidationReceiptResult {
@@ -137,6 +142,7 @@ function readManualValidationReceipt(taskDir: string, expected?: ManualValidatio
137
142
  export {
138
143
  MANUAL_VALIDATION_RECEIPT_SCHEMA,
139
144
  MANUAL_VALIDATION_RECEIPT_VERSION,
145
+ MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
140
146
  createManualValidationReceipt,
141
147
  manualValidationFinalSummaryDigest,
142
148
  manualValidationFinalSummaryProjectionMatches,
@@ -1417,19 +1417,34 @@ function recoverPreparedOrchestrationDelegation(
1417
1417
  }
1418
1418
  }
1419
1419
 
1420
+ type ActivatedRecoveryEvent = Readonly<{
1421
+ receiptId: string;
1422
+ stage: DelegationStage;
1423
+ round: number;
1424
+ artifact: string;
1425
+ startedAgent: string;
1426
+ childId: string;
1427
+ stopRevision: number;
1428
+ consumer: string;
1429
+ consumedAt: string;
1430
+ }>;
1431
+
1432
+ function recoveryReceiptMatches(
1433
+ receipt: DelegationReceipt,
1434
+ taskId: string,
1435
+ event: ActivatedRecoveryEvent
1436
+ ): boolean {
1437
+ return receipt.taskId === taskId
1438
+ && receipt.stage === event.stage
1439
+ && receipt.round === event.round
1440
+ && receipt.artifact === event.artifact
1441
+ && receipt.childId === event.childId
1442
+ && normalizeAgentToken(event.startedAgent) === normalizeAgentToken(receipt.client);
1443
+ }
1444
+
1420
1445
  function recoverActivatedOrchestrationDelegationUnderLock(
1421
1446
  taskRef: string,
1422
- event: Readonly<{
1423
- receiptId: string;
1424
- stage: DelegationStage;
1425
- round: number;
1426
- artifact: string;
1427
- startedAgent: string;
1428
- childId: string;
1429
- stopRevision: number;
1430
- consumer: string;
1431
- consumedAt: string;
1432
- }>,
1447
+ event: ActivatedRecoveryEvent,
1433
1448
  options: OrchestrationOptions = {}
1434
1449
  ): OrchestrationResult {
1435
1450
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
@@ -1442,14 +1457,7 @@ function recoverActivatedOrchestrationDelegationUnderLock(
1442
1457
  ];
1443
1458
  const receipt = matches.find((candidate) => candidate.id === event.receiptId);
1444
1459
  if (!receipt) return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
1445
- if (
1446
- receipt.taskId !== resolved.taskId
1447
- || receipt.stage !== event.stage
1448
- || receipt.round !== event.round
1449
- || receipt.artifact !== event.artifact
1450
- || receipt.childId !== event.childId
1451
- || normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)
1452
- ) {
1460
+ if (!recoveryReceiptMatches(receipt, resolved.taskId, event)) {
1453
1461
  return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
1454
1462
  }
1455
1463
  if (receipt.status === 'aborted' && run.pendingDelegation === null) {
@@ -1461,6 +1469,9 @@ function recoverActivatedOrchestrationDelegationUnderLock(
1461
1469
  if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
1462
1470
  return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
1463
1471
  }
1472
+ if (run.status !== 'running' || run.pause !== null) {
1473
+ return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'activated recovery cannot replace an unrelated orchestration pause', resolved.taskId);
1474
+ }
1464
1475
  const aborted = abortActivatedDelegation(receipt, {
1465
1476
  childId: event.childId,
1466
1477
  stopRevision: event.stopRevision,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
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",
@@ -12,19 +12,33 @@
12
12
 
13
13
  `orchestration.json` records stages, model policy, child identity, and actual outcomes. Records use the current structure; trusted local operators may correct them and rerun validation. Existing task write locks and atomic writes protect updates.
14
14
 
15
- ## Codex Host
15
+ ## Current Host
16
16
 
17
- - Use native spawn, wait, and App Server results from the current host; forward task operations through the existing broker.
17
+ - Use native spawn, wait, and App Server results from the current host. A task-bound sandbox executes ordinary task orchestration locally in the current worktree.
18
+ - Codex controller `open`, `verify`, and `close` remain restricted broker operations. After `open`, complete typed `verify` with the same lease proof before accepting the task, control-generation, and controller-instance binding.
18
19
  - Prepare validates the current task, model policy, and host preflight. Start and terminal records associate observed parent/child identity; failure must not be recorded as success.
19
- - Local execution does not require capability, controller attestation, or one-use consumption authority. Do not add automatic child discovery, spawn under a new lock protocol, orphan recovery, or a dedicated recovery protocol.
20
+ - Client-specific preflight, event sources, and stop-evidence validation belong to the client adapter. The common orchestrator calls only the shared capability and contains no client-ID branches.
21
+
22
+ ## Activated Delegation Recovery
23
+
24
+ - Before `begin-or-resume`, the orchestrator calls internal `task-lifecycle <task> recover-started --agent <client> --auto`. This entry is not user-facing and never infers child termination from missing liveness evidence.
25
+ - The client adapter declares recovery support and validates trusted stop evidence for the current pending delegation. Unsupported adapters and runs without an activated pending delegation return `no-op`; uncertain termination fails closed.
26
+ - Recovery consumes stop evidence, saves an aborted receipt while clearing pending, then appends the Activity Log terminal row. If the run save succeeds and the log write fails, the next invocation appends only the missing row.
27
+ - Routing continues only after `no-op` or `applied`. Lost control responses are not reconstructed; the next operator invocation relies on the idempotent persisted state.
28
+
29
+ ## Current-only Cutover and Rollback
30
+
31
+ - Enter a quiet window before cutover: let in-flight orchestration intents return, stop the wrapper/controller, and preserve the worktree, task/run/orchestration state, receipts, registration, and audit. For an unknown result, inspect local `status`/`route` before using existing recover/advance semantics; do not replay an old broker request.
32
+ - Deploy or roll back the CLI, broker, controller adapter, and protocol as one versioned unit. After stopping the old container/broker, rebuild and run `ai sandbox start --recreate <task-ref>` to resume with a new control generation; never reuse an old token, proof, or request.
33
+ - On either the new or rolled-back version, first confirm the same task through `status`/`route`, then require a successful typed controller `open`→`verify`. Keep the quiet window and evidence intact if any check fails; do not mix component versions or rewrite domain facts.
20
34
 
21
35
  ## Model Policy
22
36
 
23
37
  - A new run persists model and reasoning effort for both roles. Explicit policy is atomic across all four role fields; only a fully absent explicit policy may fall back to the current client's `agentClients[].orchestration`.
24
38
  - Route resolves requested model/effort by role, and prepare matches both before snapshotting. Native spawn must not inherit session defaults.
25
- - Native start records host-observed actual model/effort. Each mismatch needs its own fallback reason (the claude-code path may leave this blank and follow the recording rules instead), and requested values must never be fabricated as actual evidence.
39
+ - Native start records host-observed actual model/effort. Fields declared unobservable by the adapter follow the recording rules; each observable mismatch needs its own fallback reason, and requested values must never be fabricated as actual evidence.
26
40
  - Model selection is labeled as a complete catalog, partial catalog, or interactive-only guidance; a local override enum must not be presented as complete.
27
- - claude-code's requested reasoning effort does not yet support per-role dispatch (concurrent tasks would race on a shared file); `delegationEvidence.actualReasoningEffort` is declared `spawn-ack`, meaning it is only recorded when honestly observed in a native spawn lifecycle event (Start/Stop) — it is not a promise of per-role dispatch and does not gate activation.
41
+ - An adapter must declare any policy field it cannot dispatch per role. Values observed in host events are actual evidence only; they do not promise dispatch or gate activation.
28
42
 
29
43
  ## Stable Pause Conditions
30
44
 
@@ -12,19 +12,33 @@
12
12
 
13
13
  `orchestration.json` 记录阶段、模型策略、child 身份及实际执行结果。记录必须符合当前结构;本地操作者可直接修正后重新校验。已有任务写锁与原子写入继续保护写操作。
14
14
 
15
- ## Codex 宿主
15
+ ## 当前宿主
16
16
 
17
- - 使用当前宿主的原生 spawn、wait 与 App Server 结果;任务操作通过现有 broker 转发。
17
+ - 使用当前宿主的原生 spawn、wait 与 App Server 结果;task-bound sandbox 在当前 worktree 本地执行普通任务编排。
18
+ - Codex controller 的 `open`、`verify`、`close` 继续通过受限 broker 执行。`open` 后必须用同一 lease proof 完成 typed `verify`,再接受 task、control generation 和 controller instance 绑定。
18
19
  - prepare 校验当前任务、模型策略与宿主 preflight。启动和完成记录关联实际 parent/child 身份,失败不得写成成功。
19
- - 不要求 capability、controller attestation 或一次性消费授权本地实现;不新增 child 自动发现、锁内启动交接、孤立 child 恢复或专用恢复协议。
20
+ - 客户端特有的 preflight、事件来源与停止证据校验由客户端适配器实现;公共总控只调用统一能力。不得在公共流程中新增客户端 ID 分支。
21
+
22
+ ## Activated delegation 自动恢复
23
+
24
+ - 总控在 `begin-or-resume` 前调用内部 `task-lifecycle <task> recover-started --agent <client> --auto`。该入口不公开给用户,也不从缺少存活证据推断 child 已终止。
25
+ - 客户端适配器决定是否支持恢复,并验证当前 pending delegation 的可信停止证据;不支持或没有 activated pending 时返回 `no-op`,无法确认停止时失败关闭。
26
+ - 恢复按固定顺序消费停止证据、保存 aborted receipt 并清除 pending,再写 Activity Log 结束记录。任务状态已保存而日志未写时,下次调用只补日志。
27
+ - 只有 `no-op` 或 `applied` 才能继续 route。控制响应丢失时不重建恢复结果;下一次人为调用依靠幂等状态继续。
28
+
29
+ ## Current-only 切换与回滚
30
+
31
+ - 切换前进入静默窗口:等待在途编排 intent 返回,停止 wrapper/controller,并保留 worktree、task/run/orchestration 状态、receipts、registration 与 audit。结果未知时先用本地 `status`/`route` 判断 domain 是否已提交,再按现有 recover/advance 语义处理;旧 broker request 不重放。
32
+ - CLI、broker、controller adapter 和协议必须作为同一版本单元部署或回退。停止旧 container/broker 后执行 rebuild 与 `ai sandbox start --recreate <task-ref>`,以新 control generation 恢复执行;旧 token、proof 和 request 不得复用。
33
+ - 新版本或回退版本都必须先核对同一任务的 `status`/`route`,再要求 controller typed `open`→`verify` 成功。任一检查失败时继续保持静默并保留证据,不局部混用组件或改写 domain 事实。
20
34
 
21
35
  ## 模型策略
22
36
 
23
37
  - 新 run 必须固化 executor/reviewer 各自的 model 与 reasoning effort;显式策略必须四字段原子完整,完全没有显式字段时才读取当前 client 的 `agentClients[].orchestration`。重入不得静默改写策略。
24
38
  - route 按 role 返回 requested model/effort,prepare 必须在工作区快照前精确匹配两者;原生 spawn 不能继承会话默认值。
25
- - 原生 start 必须记录宿主观察到的 actual model/effort。任一字段与 requested 不同时必须记录独立 fallback reason(claude-code 路径下允许留空,按记录规则处理);requested 值不得补造 actual 证据。
39
+ - 原生 start 必须记录宿主观察到的 actual model/effort。适配器声明无法观察的字段按记录规则处理;任一可观察字段与 requested 不同时必须记录独立 fallback reason,requested 值不得补造 actual 证据。
26
40
  - 模型选择能力必须标记 complete catalog、partial catalog 或 interactive-only guidance;局部 override 枚举不得冒充完整目录。
27
- - claude-code 的 requested reasoning effort 暂不支持按角色下发(跨任务共享文件存在写入竞态);`delegationEvidence.actualReasoningEffort` 声明为 `spawn-ack`,语义为"仅能在宿主原生 spawn 生命周期事件(Start/Stop)里如实观察到时记录,不构成按角色下发的承诺,也不构成放行门禁"。
41
+ - 客户端无法按角色下发的策略字段必须由适配器明确声明;宿主事件中观察到的值只作为实际证据,不构成下发承诺或放行门禁。
28
42
 
29
43
  ## 稳定暂停条件
30
44
 
@@ -21,7 +21,7 @@ Before generating the validation completion report, read `.agents/rules/evidence
21
21
  - This skill closes the manual-validation status in an existing PR summary comment; it does not create a parallel ordinary validation comment.
22
22
  - It must write `manual-validation.md` or `manual-validation-r{N}.md` so later PR summary refreshes can reuse the validation result.
23
23
  - If the `sync-pr` summary comment is missing, fail instead of creating a partial fallback summary; before the receipt, completion log, and final summary are all committed, the remote summary may only be pending/non-pass.
24
- - Before generating manual-validation artifact Markdown that will be synced to an Issue, read `.agents/rules/sync-content-generation.md` and follow its generator-side constraints; Issue sync remains transparent and does not parse or rewrite the body.
24
+ - Before generating manual-validation artifact Markdown referenced by the task comment and PR summary, read `.agents/rules/sync-content-generation.md` and follow its generator-side constraints. Keep the artifact local instead of publishing it as an Issue artifact comment.
25
25
  - After this skill runs, update `task.md` immediately.
26
26
 
27
27
  Version stamp rule: when creating or updating `task.md` frontmatter, read `.agents/rules/version-stamp.md` first and write or refresh `agent_infra_version`.
@@ -106,7 +106,7 @@ The coordinator owns pending summary, receipt, completion log, final promotion,
106
106
 
107
107
  After the transaction coordinator succeeds, the core has atomically recorded `manual-validation.completed` with the same transaction/receipt/head identity. Do not append the Activity Log manually.
108
108
 
109
- If the task has a valid `platform_issue_identity`, run `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`, then `agent-infra-internal platform-comment sync {task-id} --kind artifact --artifact {manual-validation-artifact} --agent {standard-agent-token}`.
109
+ If the task has a valid `platform_issue_identity`, run only `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`. Keep the manual-validation artifact as local evidence and PR-summary input; do not publish it as an Issue comment.
110
110
 
111
111
  ### 8. Verification Gate
112
112