@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,37 +1,16 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
 
3
4
  import { inspectDecisionDetailDuplicates } from './decision-details.ts';
4
5
  import { scanVisibleMarkdown } from './markdown.ts';
5
6
  import { parseArtifactName } from './artifact-name.ts';
6
7
  import {
7
8
  hasOpenArtifactRound,
8
- validateArtifactPublication,
9
- validateCompletedArtifact
10
9
  } from './artifact-lifecycle.ts';
11
10
  import { resolveTaskRef } from './resolve-ref.ts';
12
11
  import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
13
12
  import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
14
13
  import { getArtifactSchema } from './artifact-schema.ts';
15
- import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
16
- import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
17
- import {
18
- beginArtifactRecovery,
19
- commitArtifactRecovery,
20
- consumeArtifactRecovery,
21
- prepareArtifactRecoveryFinal,
22
- prepareArtifactRecoveryCommit,
23
- recordArtifactRecoveryPassed,
24
- reconcileArtifactRecovery,
25
- recoveryContextFromIntent,
26
- stageArtifactCandidate
27
- } from './artifact-recovery.ts';
28
- import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
29
- import {
30
- consumeLifecycleRecoveryAttestation,
31
- lifecycleRecoveryAttestationDigest,
32
- validateLifecycleRecoveryAttestation
33
- } from './control-authority.ts';
34
- import type { LifecycleRecoveryAttestationV1 } from './control-authority.ts';
35
14
 
36
15
  type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
37
16
 
@@ -50,8 +29,6 @@ type LocalArtifactDiagnosticCode =
50
29
  | 'LOCAL_REQUIRED_PATTERN_MISSING'
51
30
  | 'LOCAL_STRUCTURAL_INVALID'
52
31
  | 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
53
- | 'LOCAL_RECOVERY_PROVENANCE_CONFLICT'
54
- | 'LOCAL_RECOVERY_BASELINE_MISMATCH'
55
32
  | 'LOCAL_QUALIFICATION_AUDIT_INVALID';
56
33
 
57
34
  type LocalArtifactDiagnostic = {
@@ -80,17 +57,9 @@ type LocalArtifactFinalizationRequest = {
80
57
  artifact: string;
81
58
  repoRoot?: string;
82
59
  requiredSections?: readonly string[];
83
- recoveryId?: string;
84
60
  lockAlreadyHeld?: boolean;
85
61
  };
86
62
 
87
- type LocalArtifactRecoveryInfo = Readonly<{
88
- recoveryId: string;
89
- candidatePath: string;
90
- baselineSha256: string;
91
- baselineSemanticDigest: string;
92
- }>;
93
-
94
63
  type LocalArtifactFinalizationResult = {
95
64
  status: 'passed' | 'failed';
96
65
  changed: boolean;
@@ -101,54 +70,9 @@ type LocalArtifactFinalizationResult = {
101
70
  artifactSha256: string | null;
102
71
  semanticDigest: string | null;
103
72
  diagnostics: readonly LocalArtifactDiagnostic[];
104
- recovery?: LocalArtifactRecoveryInfo;
105
73
  error: { code: string; message: string } | null;
106
74
  };
107
75
 
108
- type LocalArtifactFinalizationIntent = ArtifactRecoveryIntent;
109
-
110
- function authorityDigest(authority: LifecycleRecoveryAttestationV1): string {
111
- return lifecycleRecoveryAttestationDigest(authority);
112
- }
113
-
114
- function validFinalizerAuthority(
115
- authority: LifecycleRecoveryAttestationV1 | undefined,
116
- request: LocalArtifactFinalizationRequest,
117
- taskId: string
118
- ): boolean {
119
- if (!authority) return false;
120
- try { validateLifecycleRecoveryAttestation(authority); }
121
- catch { return false; }
122
- return authority.version === 1
123
- && authority.phase === 'artifact.finalize-local'
124
- && authority.taskId === taskId
125
- && authority.family === request.family
126
- && authority.artifact === request.artifact
127
- && authority.round === (parseArtifactName(request.artifact)?.round ?? 0)
128
- && authority.requestId.length > 0
129
- && authority.operationId.length > 0;
130
- }
131
-
132
- function recoveryInfo(context: ArtifactRecoveryContext): LocalArtifactRecoveryInfo {
133
- return {
134
- recoveryId: context.recoveryId,
135
- candidatePath: context.stagingPath,
136
- baselineSha256: context.baselineSha256,
137
- baselineSemanticDigest: context.baselineSemanticDigest
138
- };
139
- }
140
-
141
- function consumeLocalArtifactFinalizationIntent(
142
- repoRoot: string,
143
- intent: LocalArtifactFinalizationIntent,
144
- options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
145
- ): LocalArtifactFinalizationIntent {
146
- if (intent.state === 'consumed') return intent;
147
- const resolved = resolveTaskRef(intent.taskId, { repoRoot });
148
- if (!resolved.ok) throw new Error(resolved.message);
149
- return consumeArtifactRecovery(recoveryContextFromIntent(repoRoot, resolved.taskDir, intent), options);
150
- }
151
-
152
76
  function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
153
77
  const map: Record<string, LocalArtifactDiagnosticCode> = {
154
78
  ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
@@ -245,32 +169,11 @@ type LocalArtifactPreparation = Readonly<{
245
169
  content: string;
246
170
  repoRoot?: string;
247
171
  lockAlreadyHeld?: boolean;
248
- recovery?: ArtifactRecoveryContext;
249
- authority?: LifecycleRecoveryAttestationV1;
250
172
  }>;
251
173
 
252
- function tupleFor(
253
- request: LocalArtifactFinalizationRequest,
254
- taskId: string,
255
- authority?: LifecycleRecoveryAttestationV1
256
- ) {
257
- const round = parseArtifactName(request.artifact)?.round ?? 0;
258
- return {
259
- taskId,
260
- family: request.family,
261
- artifact: request.artifact,
262
- round,
263
- requestId: authority?.lifecycleRequestId ?? `local-finalize:${taskId}:${request.family}:${round}`,
264
- phase: authority?.phase ?? 'artifact.finalize-local' as const,
265
- authorityDigest: authority ? authorityDigest(authority) : null
266
- };
267
- }
268
-
269
174
  function prepareLocalArtifact(
270
175
  request: LocalArtifactFinalizationRequest,
271
- candidate?: string,
272
- authority?: LifecycleRecoveryAttestationV1,
273
- preflightOnly = false
176
+ candidate?: string
274
177
  ): LocalArtifactPreparation {
275
178
  const failed = (code: string, message: string, extra: Partial<LocalArtifactFinalizationResult> = {}): LocalArtifactPreparation => ({
276
179
  result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
@@ -279,34 +182,17 @@ function prepareLocalArtifact(
279
182
  if (!resolved.ok) return failed(resolved.code, resolved.message);
280
183
  const parsed = parseArtifactName(request.artifact);
281
184
  if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
282
- if (authority && !validFinalizerAuthority(authority, request, resolved.taskId)) {
283
- return failed('LOCAL_EXECUTION_AUTHORITY_INVALID', 'finalizer authority does not match the artifact tuple');
284
- }
285
-
286
185
  let taskContent: string;
287
186
  let content: string;
288
- let recovery: ArtifactRecoveryContext | undefined;
289
- let recoveryState: ArtifactRecoveryIntent['state'] | undefined;
290
187
  try {
291
188
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
292
- if (request.recoveryId) {
293
- const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
294
- if (!intent || intent.recoveryOperationId !== request.recoveryId) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
295
- if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
296
- recoveryState = intent.state;
297
- recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
298
- content = fs.existsSync(recovery.stagingPath)
299
- ? fs.readFileSync(recovery.stagingPath, 'utf8')
300
- : fs.readFileSync(recovery.formalPath, 'utf8');
301
- } else {
302
- const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
303
- if (!validated.ok) return failed(validated.error.code, validated.error.message);
304
- content = candidate ?? fs.readFileSync(validated.artifact.path, 'utf8');
305
- }
189
+ const artifactPath = path.join(resolved.taskDir, request.artifact);
190
+ if (!fs.statSync(artifactPath).isFile()) return failed('ARTIFACT_NOT_REGULAR', `artifact '${request.artifact}' is not a regular file`);
191
+ content = candidate ?? fs.readFileSync(artifactPath, 'utf8');
306
192
  } catch (error) { return failed('ARTIFACT_NOT_READABLE', String(error)); }
307
193
 
308
- if (resolved.state !== 'active' || !hasOpenArtifactRound(taskContent, request.family, parsed.round)) {
309
- return failed('ARTIFACT_IDENTITY_INVALID', 'candidate must match one open started round in an active task');
194
+ if (resolved.state !== 'active') {
195
+ return failed('ARTIFACT_IDENTITY_INVALID', 'artifact finalization requires an active task');
310
196
  }
311
197
  const validation = validateLocalArtifact(content, {
312
198
  family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
@@ -320,186 +206,44 @@ function prepareLocalArtifact(
320
206
  semanticDigest: validation.semanticDigest, diagnostics: validation.diagnostics
321
207
  });
322
208
 
323
- if (request.recoveryId) {
324
- if (!validation.ok) return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
325
- if (preflightOnly && recovery && recoveryState === 'awaiting-preflight-recovery') {
326
- const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
327
- prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
328
- }
329
- return {
330
- result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(recovery!) },
331
- content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
332
- };
333
- }
334
-
335
- const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
336
- if (existing?.state === 'passed' || existing?.state === 'consumed') {
337
- if (existing.state === 'consumed' && (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest)) {
338
- return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
339
- }
340
- if (existing.finalArtifactSha256 === artifactSha256 && existing.finalSemanticDigest === validation.semanticDigest) {
341
- const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
342
- return {
343
- result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
344
- content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
345
- };
346
- }
347
- }
348
-
349
- if (existing && ['preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready'].includes(existing.state)) {
350
- const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
351
- return {
352
- result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
353
- content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
354
- };
355
- }
356
- if (existing?.state === 'commit-started') {
357
- if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
358
- return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match the interrupted recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
359
- }
360
- const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
361
- const reconciled = reconcileArtifactRecovery(context, {
362
- lockAlreadyHeld: request.lockAlreadyHeld,
363
- validateFinal: () => validation.semanticDigest === existing.finalSemanticDigest
364
- });
365
- if (reconciled.status !== 'passed') {
366
- return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `interrupted recovery could not be reconciled: ${reconciled.status}`), content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
367
- }
368
- return {
369
- result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
370
- content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
371
- };
372
- }
209
+ // Trusted local operators edit the canonical artifact directly. A
210
+ // finalization result is a fresh observation of those bytes, never a
211
+ // capability minted by an earlier recovery attempt or generation.
212
+ return {
213
+ result: validation.ok ? { ...result, status: 'passed', error: null } : result,
214
+ content,
215
+ repoRoot: resolved.repoRoot,
216
+ lockAlreadyHeld: request.lockAlreadyHeld
217
+ };
373
218
 
374
- try {
375
- if (validation.ok) {
376
- if (preflightOnly) {
377
- const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
378
- repoRoot: resolved.repoRoot, taskDir: resolved.taskDir,
379
- ...(authority ? { recoveryId: authority.operationId } : {}), lockAlreadyHeld: request.lockAlreadyHeld
380
- });
381
- const staged = stageArtifactCandidate(context, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
382
- prepareArtifactRecoveryCommit(context, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
383
- return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
384
- }
385
- const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
386
- repoRoot: resolved.repoRoot,
387
- taskDir: resolved.taskDir,
388
- expectedFinalSha256: artifactSha256,
389
- expectedFinalSemanticDigest: validation.semanticDigest,
390
- ...(authority ? { recoveryId: authority.operationId } : {}),
391
- lockAlreadyHeld: request.lockAlreadyHeld
392
- });
393
- recovery = context;
394
- return { result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
395
- }
396
- const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
397
- repoRoot: resolved.repoRoot,
398
- taskDir: resolved.taskDir,
399
- ...(authority ? { recoveryId: authority.operationId } : {}),
400
- lockAlreadyHeld: request.lockAlreadyHeld
401
- });
402
- recovery = context;
403
- return {
404
- result: { ...result, recovery: recoveryInfo(context) },
405
- content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
406
- };
407
- } catch (error) {
408
- return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
409
- }
410
219
  }
411
220
 
412
221
  function commitLocalArtifactProvenance(
413
- prepared: LocalArtifactPreparation,
414
- options: Readonly<{ afterPublish?: () => void }> = {}
222
+ prepared: LocalArtifactPreparation
415
223
  ): LocalArtifactFinalizationResult {
416
- if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery) return prepared.result;
417
- if (prepared.result.status === 'failed') return prepared.result;
418
- try {
419
- const intent = readArtifactRecoveryIntent(prepared.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
420
- if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
421
- let committed;
422
- if (intent.state === 'passed' || intent.state === 'consumed') {
423
- return {
424
- ...prepared.result,
425
- changed: false,
426
- artifactSha256: intent.finalArtifactSha256,
427
- semanticDigest: intent.finalSemanticDigest,
428
- error: null
429
- };
430
- } else if (intent.state === 'commit-started') {
431
- const reconciled = reconcileArtifactRecovery(prepared.recovery, {
432
- lockAlreadyHeld: prepared.lockAlreadyHeld,
433
- afterPublish: options.afterPublish
434
- });
435
- committed = reconciled.intent;
436
- } else if (intent.state === 'awaiting-preflight-recovery') {
437
- const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
438
- prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
439
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
440
- }
441
- if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
442
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
443
- }
444
- if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
445
- prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
446
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
447
- } else if (intent.state === 'full-finalizer-ready') {
448
- committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
449
- }
450
- if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
451
- return {
452
- ...prepared.result,
453
- status: 'passed',
454
- changed: false,
455
- artifactSha256: committed.finalArtifactSha256,
456
- semanticDigest: committed.finalSemanticDigest,
457
- error: null
458
- };
459
- } catch (error) {
460
- if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
461
- return { ...prepared.result, status: 'failed', error: {
462
- code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
463
- } };
464
- }
224
+ return prepared.result;
465
225
  }
466
226
 
467
227
  function finalizeLocalArtifact(
468
- request: LocalArtifactFinalizationRequest,
469
- authority?: LifecycleRecoveryAttestationV1,
470
- options: Readonly<{ deferLifecycleRecoveryConsumption?: boolean }> = {}
228
+ request: LocalArtifactFinalizationRequest
471
229
  ): LocalArtifactFinalizationResult {
472
- const prepared = prepareLocalArtifact(request, undefined, authority);
473
- const result = commitLocalArtifactProvenance(prepared);
474
- if (authority && !options.deferLifecycleRecoveryConsumption && result.status === 'passed') {
475
- try { consumeLifecycleRecoveryAttestation(authority); }
476
- catch (error) {
477
- return { ...result, status: 'failed', error: {
478
- code: 'LOCAL_EXECUTION_AUTHORITY_CONSUME_FAILED',
479
- message: error instanceof Error ? error.message : String(error)
480
- } };
481
- }
482
- }
483
- return result;
230
+ return prepareLocalArtifact(request).result;
484
231
  }
485
232
 
486
233
  /** Validate and seal an immutable preflight generation without publishing the formal artifact. */
487
234
  function preflightLocalArtifact(
488
- request: LocalArtifactFinalizationRequest,
489
- authority?: LifecycleRecoveryAttestationV1
235
+ request: LocalArtifactFinalizationRequest
490
236
  ): LocalArtifactFinalizationResult {
491
- const prepared = prepareLocalArtifact(request, undefined, authority, true);
237
+ const prepared = prepareLocalArtifact(request);
492
238
  return prepared.result;
493
239
  }
494
240
 
495
241
  export {
496
242
  LOCAL_ARTIFACT_REQUIRED_SECTIONS,
497
- consumeLocalArtifactFinalizationIntent,
498
243
  finalizeLocalArtifact,
499
244
  preflightLocalArtifact,
500
245
  prepareLocalArtifact,
501
246
  commitLocalArtifactProvenance,
502
- readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent,
503
247
  canonicalSemanticDigest as semanticDigest,
504
248
  sha256Content,
505
249
  validateLocalArtifact
@@ -510,9 +254,7 @@ export type {
510
254
  LocalArtifactFamily,
511
255
  LocalArtifactFinalizationRequest,
512
256
  LocalArtifactFinalizationResult,
513
- LocalArtifactFinalizationIntent,
514
257
  LocalArtifactPreparation,
515
258
  LocalArtifactValidationOptions,
516
- LocalArtifactValidationResult,
517
- LocalArtifactRecoveryInfo
259
+ LocalArtifactValidationResult
518
260
  };
@@ -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,
@@ -41,7 +41,6 @@ import type {
41
41
  OrchestrationModelPolicy,
42
42
  OrchestrationRolePolicy
43
43
  } from '../agent-clients/types.ts';
44
- import type { LifecycleRecoveryAttestationV1 } from './control-authority.ts';
45
44
  import {
46
45
  captureRepositorySnapshot,
47
46
  captureWorkspaceSnapshot,
@@ -174,9 +173,6 @@ type OrchestrationOptions = {
174
173
  }>;
175
174
  diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
176
175
  supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
177
- validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
178
- consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
179
- lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
180
176
  token?: () => string;
181
177
  monotonicNow?: () => number;
182
178
  sleep?: (milliseconds: number) => Promise<void>;
@@ -819,7 +815,6 @@ function prepareOrchestrationDelegationUnlocked(
819
815
  requestedModel?: string;
820
816
  requestedReasoningEffort?: string;
821
817
  lifecycleProvenance?: DelegationLifecycleProvenance;
822
- lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
823
818
  }>,
824
819
  options: OrchestrationOptions = {}
825
820
  ): OrchestrationResult {
@@ -861,16 +856,6 @@ function prepareOrchestrationDelegationUnlocked(
861
856
  if (!validModel(input.requestedReasoningEffort)) {
862
857
  return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_REQUIRED', 'prepare requires the exact requested reasoning effort', resolved.taskId);
863
858
  }
864
- if (input.client === 'codex' && !input.lifecycleProvenance) {
865
- return failed('ORCHESTRATION_CODEX_PROVENANCE_REQUIRED', 'Codex prepare requires lifecycle provenance', resolved.taskId);
866
- }
867
- if (input.lifecycleRecoveryAttestation
868
- && (input.lifecycleRecoveryAttestation.phase !== 'orchestration.prepare'
869
- || input.lifecycleRecoveryAttestation.taskId !== resolved.taskId
870
- || input.lifecycleRecoveryAttestation.artifact !== next.artifact
871
- || input.lifecycleRecoveryAttestation.round !== next.round)) {
872
- return failed('LIFECYCLE_AUTHORITY_TUPLE_MISMATCH', 'orchestration authority does not match the routed stage', resolved.taskId);
873
- }
874
859
  const expectedPolicy = run.modelPolicy[next.role];
875
860
  if (input.requestedModel !== expectedPolicy.model) {
876
861
  return failed('ORCHESTRATION_REQUESTED_MODEL_MISMATCH', `requested model does not match the persisted ${next.role} model`, resolved.taskId);
@@ -878,15 +863,6 @@ function prepareOrchestrationDelegationUnlocked(
878
863
  if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
879
864
  return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
880
865
  }
881
- const capabilityValidationError = options.validateLifecycleCapability?.();
882
- if (capabilityValidationError) {
883
- return failed(
884
- capabilityValidationError.code,
885
- capabilityValidationError.message,
886
- resolved.taskId,
887
- capabilityValidationError.detail ? { detail: capabilityValidationError.detail } : {}
888
- );
889
- }
890
866
  let beforeFingerprint: string;
891
867
  try {
892
868
  beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
@@ -911,15 +887,6 @@ function prepareOrchestrationDelegationUnlocked(
911
887
  lifecycleProvenance: input.lifecycleProvenance ?? null,
912
888
  beforeFingerprint
913
889
  }, { id: options.id, now: options.now, monotonicNow: options.monotonicNow });
914
- const capabilityError = options.consumeLifecycleCapability?.();
915
- if (capabilityError) {
916
- return failed(
917
- capabilityError.code,
918
- capabilityError.message,
919
- resolved.taskId,
920
- capabilityError.detail ? { detail: capabilityError.detail } : {}
921
- );
922
- }
923
890
  const updated = withUpdatedRun(run, {
924
891
  nextStage: next.stage,
925
892
  pendingDelegation: receipt
@@ -935,7 +902,6 @@ function prepareOrchestrationDelegation(
935
902
  requestedModel?: string;
936
903
  requestedReasoningEffort?: string;
937
904
  lifecycleProvenance?: DelegationLifecycleProvenance;
938
- lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
939
905
  }>,
940
906
  options: OrchestrationOptions = {}
941
907
  ): OrchestrationResult {
@@ -1451,19 +1417,34 @@ function recoverPreparedOrchestrationDelegation(
1451
1417
  }
1452
1418
  }
1453
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
+
1454
1445
  function recoverActivatedOrchestrationDelegationUnderLock(
1455
1446
  taskRef: string,
1456
- event: Readonly<{
1457
- receiptId: string;
1458
- stage: DelegationStage;
1459
- round: number;
1460
- artifact: string;
1461
- startedAgent: string;
1462
- childId: string;
1463
- stopRevision: number;
1464
- consumer: string;
1465
- consumedAt: string;
1466
- }>,
1447
+ event: ActivatedRecoveryEvent,
1467
1448
  options: OrchestrationOptions = {}
1468
1449
  ): OrchestrationResult {
1469
1450
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
@@ -1476,14 +1457,7 @@ function recoverActivatedOrchestrationDelegationUnderLock(
1476
1457
  ];
1477
1458
  const receipt = matches.find((candidate) => candidate.id === event.receiptId);
1478
1459
  if (!receipt) return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
1479
- if (
1480
- receipt.taskId !== resolved.taskId
1481
- || receipt.stage !== event.stage
1482
- || receipt.round !== event.round
1483
- || receipt.artifact !== event.artifact
1484
- || receipt.childId !== event.childId
1485
- || normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)
1486
- ) {
1460
+ if (!recoveryReceiptMatches(receipt, resolved.taskId, event)) {
1487
1461
  return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
1488
1462
  }
1489
1463
  if (receipt.status === 'aborted' && run.pendingDelegation === null) {
@@ -1495,6 +1469,9 @@ function recoverActivatedOrchestrationDelegationUnderLock(
1495
1469
  if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
1496
1470
  return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
1497
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
+ }
1498
1475
  const aborted = abortActivatedDelegation(receipt, {
1499
1476
  childId: event.childId,
1500
1477
  stopRevision: event.stopRevision,
@@ -7,7 +7,6 @@ export function parseReviewCommand(args: readonly string[]): ReviewFinalizationR
7
7
  const values: Record<string, string | boolean> = {};
8
8
  const flags = {
9
9
  '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
10
- '--recovery-id': 'recoveryId',
11
10
  '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
12
11
  } as const;
13
12
  for (let index = 2; index < args.length; index += 1) {