@fitlab-ai/agent-infra 0.9.14 → 0.9.15

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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -1,4 +1,6 @@
1
1
  import path from 'node:path';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
2
4
 
3
5
  import { normalizeAgentToken } from '../agent-clients/tokens.ts';
4
6
  import { isAgentClientId } from '../agent-clients/types.ts';
@@ -37,9 +39,478 @@ import {
37
39
  type TaskLifecycleRequest,
38
40
  type TaskLifecycleResult
39
41
  } from './lifecycle.ts';
42
+ import { locateActivityLog } from './activity-log.ts';
40
43
  import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
41
44
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
42
45
  import { verifyTaskEvent } from './verification.ts';
46
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
47
+ import {
48
+ findArtifactRepairIntentsByOperation,
49
+ writeArtifactRepairIntent,
50
+ type ArtifactRepairIntent
51
+ } from './artifact-repair-intent.ts';
52
+ import {
53
+ computeLifecycleBuildIdentity,
54
+ verifyLifecycleBuildIdentity,
55
+ type LifecycleBuildIdentity
56
+ } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
57
+
58
+ export type LifecycleAuthorityPhase =
59
+ | 'orchestration.prepare'
60
+ | 'artifact.finalize-local'
61
+ | 'task-event.completed';
62
+
63
+ export type LifecycleAuthorityRequestV1 = Readonly<{
64
+ version: 1;
65
+ requestId: string;
66
+ operationId: string;
67
+ phase: LifecycleAuthorityPhase;
68
+ taskId: string;
69
+ family: 'analysis' | 'plan' | 'code';
70
+ artifact: string;
71
+ round: number;
72
+ lifecycleRequestId: string;
73
+ authorityRef: string;
74
+ expectedControlGeneration: string;
75
+ expectedControllerInstanceDigest: string;
76
+ expectedBuildIdentityDigest: string;
77
+ expectedHookDefinitionHash: string;
78
+ }>;
79
+
80
+ export type LifecycleRecoveryAttestationV1 = Readonly<{
81
+ version: 1;
82
+ attestationId: string;
83
+ authorityRefDigest: string;
84
+ operationId: string;
85
+ phase: LifecycleAuthorityPhase;
86
+ requestId: string;
87
+ lifecycleRequestId: string;
88
+ taskId: string;
89
+ family: 'analysis' | 'plan' | 'code';
90
+ artifact: string;
91
+ round: number;
92
+ controlGeneration: string;
93
+ controllerInstanceDigest: string;
94
+ sessionId: string;
95
+ turnId: string;
96
+ toolUseId: string;
97
+ buildIdentityDigest: string;
98
+ hookDefinitionHash: string;
99
+ issuedAt: number;
100
+ expiresAt: number;
101
+ }>;
102
+
103
+ export type LifecycleAuthorityResponseV1 = Readonly<{
104
+ version: 1;
105
+ requestId: string;
106
+ operationId: string;
107
+ phase: LifecycleAuthorityPhase;
108
+ status: 'issued' | 'already-completed' | 'rejected';
109
+ attestation: LifecycleRecoveryAttestationV1 | null;
110
+ error: { code: string; message: string } | null;
111
+ }>;
112
+
113
+ type LifecycleAuthorityIssuerOptions = Readonly<{
114
+ controllerBinding?: TaskControlControllerBinding | null;
115
+ capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
116
+ buildIdentity?: LifecycleBuildIdentity;
117
+ now?: () => number;
118
+ operationAttestations?: Map<string, LifecycleRecoveryAttestationV1>;
119
+ }>;
120
+
121
+ export type LifecycleRecoveryOperationQueryV1 = Readonly<{
122
+ version: 1;
123
+ operationId: string;
124
+ status: 'unknown' | 'in-progress' | 'committed';
125
+ capabilityState: 'unknown' | 'reserved' | 'consumed';
126
+ phases: readonly Readonly<{
127
+ phase: LifecycleAuthorityPhase;
128
+ requestId: string;
129
+ attestationId: string | null;
130
+ state: 'issued' | 'consumed' | 'observed';
131
+ }>[];
132
+ }>;
133
+
134
+ const lifecycleAuthorityPhases: readonly LifecycleAuthorityPhase[] = [
135
+ 'orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'
136
+ ];
137
+ const lifecycleAuthorityFamilies = ['analysis', 'plan', 'code'] as const;
138
+ const lifecycleAuthorityArtifact = /^(?:analysis|plan|code)(?:-r[1-9]\d*)?\.md$/u;
139
+ const lifecycleAuthorityDigest = (value: string): string => crypto.createHash('sha256').update(value, 'utf8').digest('hex');
140
+ const lifecycleAuthorityBuildDigest = (value: LifecycleBuildIdentity): string => lifecycleAuthorityDigest(JSON.stringify(value));
141
+
142
+ export function lifecycleRecoveryAttestationDigest(value: LifecycleRecoveryAttestationV1): string {
143
+ return lifecycleAuthorityDigest(JSON.stringify(value));
144
+ }
145
+
146
+ function authorityExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
147
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
148
+ }
149
+
150
+ export function validateLifecycleAuthorityRequest(value: unknown): LifecycleAuthorityRequestV1 {
151
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
152
+ const request = value as Record<string, unknown>;
153
+ if (!authorityExactKeys(request, [
154
+ 'artifact', 'authorityRef', 'expectedBuildIdentityDigest', 'expectedControlGeneration',
155
+ 'expectedControllerInstanceDigest', 'expectedHookDefinitionHash', 'family', 'lifecycleRequestId',
156
+ 'operationId', 'phase', 'requestId', 'round', 'taskId', 'version'
157
+ ])
158
+ || request.version !== 1
159
+ || typeof request.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.requestId)
160
+ || typeof request.operationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.operationId)
161
+ || !lifecycleAuthorityPhases.includes(request.phase as LifecycleAuthorityPhase)
162
+ || typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)
163
+ || !lifecycleAuthorityFamilies.includes(request.family as typeof lifecycleAuthorityFamilies[number])
164
+ || typeof request.artifact !== 'string' || !lifecycleAuthorityArtifact.test(request.artifact)
165
+ || !Number.isSafeInteger(request.round) || (request.round as number) < 1
166
+ || typeof request.lifecycleRequestId !== 'string' || !request.lifecycleRequestId || /[\r\n]/u.test(request.lifecycleRequestId)
167
+ || typeof request.authorityRef !== 'string' || !request.authorityRef || /[\r\n]/u.test(request.authorityRef)
168
+ || typeof request.expectedControlGeneration !== 'string' || !request.expectedControlGeneration
169
+ || typeof request.expectedControllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedControllerInstanceDigest)
170
+ || typeof request.expectedBuildIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedBuildIdentityDigest)
171
+ || typeof request.expectedHookDefinitionHash !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedHookDefinitionHash)) {
172
+ throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
173
+ }
174
+ const expectedArtifactFamily = request.artifact as string;
175
+ if (!expectedArtifactFamily.startsWith(request.family as string)) throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
176
+ return request as LifecycleAuthorityRequestV1;
177
+ }
178
+
179
+ export function validateLifecycleRecoveryAttestation(
180
+ value: unknown,
181
+ now = Date.now()
182
+ ): LifecycleRecoveryAttestationV1 {
183
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_INVALID');
184
+ const attestation = value as Record<string, unknown>;
185
+ if (!authorityExactKeys(attestation, [
186
+ 'artifact', 'attestationId', 'authorityRefDigest', 'buildIdentityDigest', 'controlGeneration',
187
+ 'controllerInstanceDigest', 'expiresAt', 'family', 'hookDefinitionHash', 'issuedAt',
188
+ 'lifecycleRequestId', 'operationId', 'phase', 'requestId', 'round', 'sessionId',
189
+ 'taskId', 'toolUseId', 'turnId', 'version'
190
+ ])
191
+ || attestation.version !== 1
192
+ || typeof attestation.attestationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.attestationId)
193
+ || typeof attestation.authorityRefDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.authorityRefDigest)
194
+ || typeof attestation.operationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.operationId)
195
+ || !lifecycleAuthorityPhases.includes(attestation.phase as LifecycleAuthorityPhase)
196
+ || typeof attestation.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.requestId)
197
+ || typeof attestation.lifecycleRequestId !== 'string' || !attestation.lifecycleRequestId
198
+ || typeof attestation.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(attestation.taskId)
199
+ || !lifecycleAuthorityFamilies.includes(attestation.family as typeof lifecycleAuthorityFamilies[number])
200
+ || typeof attestation.artifact !== 'string' || !lifecycleAuthorityArtifact.test(attestation.artifact)
201
+ || !Number.isSafeInteger(attestation.round) || (attestation.round as number) < 1
202
+ || typeof attestation.controlGeneration !== 'string' || !attestation.controlGeneration
203
+ || typeof attestation.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.controllerInstanceDigest)
204
+ || typeof attestation.sessionId !== 'string' || !attestation.sessionId
205
+ || typeof attestation.turnId !== 'string' || !attestation.turnId
206
+ || typeof attestation.toolUseId !== 'string' || !attestation.toolUseId
207
+ || typeof attestation.buildIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.buildIdentityDigest)
208
+ || typeof attestation.hookDefinitionHash !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.hookDefinitionHash)
209
+ || !Number.isSafeInteger(attestation.issuedAt) || !Number.isSafeInteger(attestation.expiresAt)
210
+ || (attestation.expiresAt as number) <= (attestation.issuedAt as number)
211
+ || (attestation.expiresAt as number) <= now) {
212
+ throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_INVALID');
213
+ }
214
+ return attestation as LifecycleRecoveryAttestationV1;
215
+ }
216
+
217
+ const authorityOperationMap = new WeakMap<object, Map<string, LifecycleRecoveryAttestationV1>>();
218
+ const authorityRecords = new Map<string, Readonly<{
219
+ store: ReturnType<typeof createCodexCapabilityStore>;
220
+ capabilityRef: string;
221
+ expected: Readonly<{
222
+ taskId: string;
223
+ hookDefinitionHash: string;
224
+ buildIdentity: LifecycleBuildIdentity;
225
+ controller: TaskControlControllerBinding;
226
+ }>;
227
+ }>>();
228
+ const consumedAuthorityPhases = new Set<string>();
229
+
230
+ export function issueLifecycleRecoveryAttestation(
231
+ input: LifecycleAuthorityRequestV1,
232
+ options: LifecycleAuthorityIssuerOptions = {}
233
+ ): LifecycleAuthorityResponseV1 {
234
+ let request: LifecycleAuthorityRequestV1;
235
+ try { request = validateLifecycleAuthorityRequest(input); }
236
+ catch (error) {
237
+ return { version: 1, requestId: String((input as { requestId?: unknown })?.requestId ?? ''), operationId: String((input as { operationId?: unknown })?.operationId ?? ''), phase: (input as { phase?: LifecycleAuthorityPhase })?.phase ?? 'artifact.finalize-local', status: 'rejected', attestation: null, error: { code: 'LIFECYCLE_AUTHORITY_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } };
238
+ }
239
+ const binding = options.controllerBinding ?? null;
240
+ if (!binding) return authorityRejected(request, 'LOCAL_EXECUTION_AUTHORITY_UNAVAILABLE', 'current controller authority is unavailable');
241
+ if (binding.controlGeneration !== request.expectedControlGeneration
242
+ || binding.instanceDigest !== request.expectedControllerInstanceDigest) {
243
+ return authorityRejected(request, 'LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the request');
244
+ }
245
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(process.cwd());
246
+ if (lifecycleAuthorityBuildDigest(buildIdentity) !== request.expectedBuildIdentityDigest) {
247
+ return authorityRejected(request, 'LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the request');
248
+ }
249
+ const store = options.capabilityStore ?? createCodexCapabilityStore();
250
+ const registry = options.operationAttestations ?? authorityOperationMap.get(store) ?? new Map<string, LifecycleRecoveryAttestationV1>();
251
+ authorityOperationMap.set(store, registry);
252
+ const key = `${request.requestId}\0${request.operationId}\0${request.phase}`;
253
+ const prior = registry.get(key);
254
+ if (prior) return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'already-completed', attestation: prior, error: null };
255
+ try {
256
+ const expected = {
257
+ taskId: request.taskId,
258
+ hookDefinitionHash: request.expectedHookDefinitionHash,
259
+ buildIdentity,
260
+ controller: binding
261
+ };
262
+ const record = store.reserveReference(request.authorityRef, request.operationId, expected);
263
+ if (!record.sessionId || !record.turnId || !record.toolUseId) throw new Error('CODEX_CAPABILITY_IDENTITY_INVALID: capability hook attestation is incomplete');
264
+ const now = (options.now ?? Date.now)();
265
+ const attestation = validateLifecycleRecoveryAttestation({
266
+ version: 1,
267
+ attestationId: crypto.randomUUID(),
268
+ authorityRefDigest: lifecycleAuthorityDigest(request.authorityRef),
269
+ operationId: request.operationId,
270
+ phase: request.phase,
271
+ requestId: request.requestId,
272
+ lifecycleRequestId: request.lifecycleRequestId,
273
+ taskId: request.taskId,
274
+ family: request.family,
275
+ artifact: request.artifact,
276
+ round: request.round,
277
+ controlGeneration: binding.controlGeneration,
278
+ controllerInstanceDigest: binding.instanceDigest,
279
+ sessionId: record.sessionId,
280
+ turnId: record.turnId,
281
+ toolUseId: record.toolUseId,
282
+ buildIdentityDigest: lifecycleAuthorityBuildDigest(buildIdentity),
283
+ hookDefinitionHash: request.expectedHookDefinitionHash,
284
+ issuedAt: now,
285
+ expiresAt: Math.min(record.expiresAt, now + 30_000)
286
+ }, now);
287
+ store.reserveRecoveryPhase(
288
+ request.authorityRef,
289
+ request.operationId,
290
+ request.phase,
291
+ request.requestId,
292
+ expected
293
+ );
294
+ registry.set(key, attestation);
295
+ authorityRecords.set(attestation.attestationId, { store, capabilityRef: request.authorityRef, expected });
296
+ return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'issued', attestation, error: null };
297
+ } catch (error) {
298
+ return authorityRejected(request, error instanceof Error && error.name.startsWith('CODEX_') ? error.name : 'LIFECYCLE_AUTHORITY_REJECTED', error instanceof Error ? error.message : String(error));
299
+ }
300
+ }
301
+
302
+ export function consumeLifecycleRecoveryAttestation(
303
+ value: LifecycleRecoveryAttestationV1,
304
+ now = Date.now()
305
+ ): void {
306
+ const attestation = validateLifecycleRecoveryAttestation(value, now);
307
+ if (consumedAuthorityPhases.has(attestation.attestationId)) return;
308
+ const record = authorityRecords.get(attestation.attestationId);
309
+ if (!record) throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_UNKNOWN');
310
+ record.store.consumeRecoveryPhase(
311
+ record.capabilityRef,
312
+ attestation.operationId,
313
+ attestation.phase,
314
+ attestation.requestId,
315
+ record.expected
316
+ );
317
+ if (attestation.phase === 'task-event.completed') {
318
+ record.store.consumeReference(record.capabilityRef, attestation.operationId, record.expected);
319
+ }
320
+ consumedAuthorityPhases.add(attestation.attestationId);
321
+ }
322
+
323
+ export function queryLifecycleRecoveryOperation(
324
+ operationId: string,
325
+ options: Readonly<{
326
+ repoRoot?: string;
327
+ capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
328
+ }> = {}
329
+ ): LifecycleRecoveryOperationQueryV1 {
330
+ if (!/^[a-f0-9-]{16,64}$/u.test(operationId)) {
331
+ throw new Error('LIFECYCLE_RECOVERY_OPERATION_INVALID');
332
+ }
333
+ const store = options.capabilityStore ?? createCodexCapabilityStore();
334
+ const registry = authorityOperationMap.get(store);
335
+ const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
336
+ const intents = options.repoRoot ? findArtifactRepairIntentsByOperation(options.repoRoot, operationId) : [];
337
+ const capabilities = store.findByRecoveryOperation(operationId);
338
+ const phaseMap = new Map<string, LifecycleRecoveryOperationQueryV1['phases'][number]>();
339
+ for (const capability of capabilities) {
340
+ for (const phase of capability.recoveryPhases) {
341
+ phaseMap.set(`${phase.phase}\0${phase.requestId}`, {
342
+ phase: phase.phase,
343
+ requestId: phase.requestId,
344
+ attestationId: null,
345
+ state: phase.state === 'consumed' ? 'consumed' : 'observed'
346
+ });
347
+ }
348
+ }
349
+ for (const attestation of known) {
350
+ const key = `${attestation.phase}\0${attestation.requestId}`;
351
+ const durable = phaseMap.get(key);
352
+ phaseMap.set(key, {
353
+ phase: attestation.phase,
354
+ requestId: attestation.requestId,
355
+ attestationId: attestation.attestationId,
356
+ state: durable?.state === 'consumed' || consumedAuthorityPhases.has(attestation.attestationId)
357
+ ? 'consumed' : 'issued'
358
+ });
359
+ }
360
+ for (const intent of intents) {
361
+ if (!intent.phase) continue;
362
+ const key = `${intent.phase}\0${intent.requestId}`;
363
+ if (phaseMap.has(key)) continue;
364
+ phaseMap.set(key, {
365
+ phase: intent.phase,
366
+ requestId: intent.requestId,
367
+ attestationId: null,
368
+ state: intent.state === 'consumed' ? 'consumed' : 'observed'
369
+ });
370
+ }
371
+ const capabilityState = capabilities.some((record) => record.recoveryState === 'consumed')
372
+ ? 'consumed'
373
+ : capabilities.some((record) => record.recoveryState === 'reserved') ? 'reserved' : 'unknown';
374
+ const committed = capabilityState === 'consumed' || intents.some((intent) => intent.state === 'consumed');
375
+ return {
376
+ version: 1,
377
+ operationId,
378
+ status: committed ? 'committed' : capabilityState === 'reserved' || phaseMap.size > 0 ? 'in-progress' : 'unknown',
379
+ capabilityState,
380
+ phases: Object.freeze([...phaseMap.values()])
381
+ };
382
+ }
383
+
384
+ type LifecycleRecoveryCompensationOptions = Readonly<{
385
+ repoRoot: string;
386
+ controllerBinding: TaskControlControllerBinding;
387
+ capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
388
+ buildIdentity?: LifecycleBuildIdentity;
389
+ now?: () => number;
390
+ }>;
391
+
392
+ function lifecycleRecoveryCompensationError(code: string, message: string): Error {
393
+ const error = new Error(`${code}: ${message}`);
394
+ error.name = code;
395
+ return error;
396
+ }
397
+
398
+ function lifecycleRecoveryEventCommitted(
399
+ selector: LifecycleAuthorityRequestV1,
400
+ repoRoot: string
401
+ ): boolean {
402
+ const resolved = resolveTaskRef(selector.taskId, { repoRoot });
403
+ if (!resolved.ok || resolved.taskId !== selector.taskId) return false;
404
+ let content: string;
405
+ try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
406
+ catch { return false; }
407
+ const section = locateActivityLog(content);
408
+ if (!section) return false;
409
+ const labels = { analysis: 'Analyze Task', plan: 'Plan Task', code: 'Code Task' } as const;
410
+ const prefix = `${labels[selector.family]} (Round ${selector.round}`;
411
+ return section.entries.some((entry) => {
412
+ if (entry.step.endsWith(' [started]') || !entry.step.startsWith(prefix)) return false;
413
+ const delimiter = entry.step[prefix.length];
414
+ return (delimiter === ',' || delimiter === ')') && entry.note.endsWith(`→ ${selector.artifact}`);
415
+ });
416
+ }
417
+
418
+ function lifecycleRecoveryIntentMatches(
419
+ intent: ArtifactRepairIntent,
420
+ selector: LifecycleAuthorityRequestV1
421
+ ): boolean {
422
+ return intent.taskId === selector.taskId
423
+ && intent.family === selector.family
424
+ && intent.artifact === selector.artifact
425
+ && (intent.state === 'commit-started' || intent.state === 'consumed');
426
+ }
427
+
428
+ /**
429
+ * Completes only the durable tail of a committed task event after a process
430
+ * restart. The task event itself is the commit boundary and is never replayed.
431
+ */
432
+ export function recoverLifecycleRecoveryOperation(
433
+ input: LifecycleAuthorityRequestV1,
434
+ options: LifecycleRecoveryCompensationOptions
435
+ ): LifecycleRecoveryOperationQueryV1 {
436
+ const selector = validateLifecycleAuthorityRequest(input);
437
+ if (selector.phase !== 'task-event.completed') {
438
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_UNSUPPORTED', 'only task-event.completed supports durable compensation');
439
+ }
440
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(options.repoRoot);
441
+ if (selector.expectedBuildIdentityDigest !== lifecycleAuthorityBuildDigest(buildIdentity)) {
442
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the recovery request');
443
+ }
444
+ if (selector.expectedControlGeneration !== options.controllerBinding.controlGeneration
445
+ || selector.expectedControllerInstanceDigest !== options.controllerBinding.instanceDigest) {
446
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the recovery request');
447
+ }
448
+ const store = options.capabilityStore ?? createCodexCapabilityStore();
449
+ const expected = {
450
+ taskId: selector.taskId,
451
+ hookDefinitionHash: selector.expectedHookDefinitionHash,
452
+ buildIdentity,
453
+ controller: options.controllerBinding
454
+ };
455
+ const capabilities = store.findByRecoveryOperation(selector.operationId);
456
+ if (capabilities.length !== 1 || capabilities[0]!.capabilityRefDigest !== lifecycleAuthorityDigest(selector.authorityRef)) {
457
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not identify this operation');
458
+ }
459
+ const capability = capabilities[0]!;
460
+ const build = verifyLifecycleBuildIdentity(capability.buildIdentity, buildIdentity);
461
+ const expiredReserved = capability.status === 'expired' && capability.recoveryState === 'reserved';
462
+ if (!build.ok
463
+ || capability.taskId !== selector.taskId
464
+ || capability.hookDefinitionHash !== selector.expectedHookDefinitionHash
465
+ || capability.controller?.controlGeneration !== options.controllerBinding.controlGeneration
466
+ || capability.controller?.instanceDigest !== options.controllerBinding.instanceDigest
467
+ || capability.recoveryState === 'unreserved'
468
+ || capability.recoveryOperationId !== selector.operationId
469
+ || (!['attested', 'consumed'].includes(capability.status) && !expiredReserved)) {
470
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not match the recovery request');
471
+ }
472
+ const phase = capability.recoveryPhases.find((entry) => entry.phase === selector.phase);
473
+ if (!phase) {
474
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
475
+ }
476
+ const intents = findArtifactRepairIntentsByOperation(options.repoRoot, selector.operationId)
477
+ .filter((intent) => intent.taskId === selector.taskId
478
+ && intent.family === selector.family
479
+ && intent.artifact === selector.artifact);
480
+ if (intents.length !== 1) {
481
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not identify this task event');
482
+ }
483
+ const intent = intents[0]!;
484
+ if (!lifecycleRecoveryIntentMatches(intent, selector)
485
+ || intent.phase !== selector.phase
486
+ || intent.requestId !== selector.lifecycleRequestId
487
+ || intent.authorityDigest === null) {
488
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not match the recovery request');
489
+ }
490
+ if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
491
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
492
+ }
493
+ if (capability.recoveryState === 'reserved') {
494
+ if (phase.state === 'issued') {
495
+ store.consumeRecoveryPhase(selector.authorityRef, selector.operationId, selector.phase, phase.requestId, expected, { allowExpiredReserved: true });
496
+ }
497
+ store.consumeReference(selector.authorityRef, selector.operationId, expected, { allowExpiredReserved: true });
498
+ } else if (phase.state !== 'consumed') {
499
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
500
+ }
501
+ if (intent.state !== 'consumed') {
502
+ writeArtifactRepairIntent(options.repoRoot, {
503
+ ...intent,
504
+ state: 'consumed',
505
+ updatedAt: (options.now ?? Date.now)()
506
+ }, { expected: intent });
507
+ }
508
+ return queryLifecycleRecoveryOperation(selector.operationId, { repoRoot: options.repoRoot, capabilityStore: store });
509
+ }
510
+
511
+ function authorityRejected(request: LifecycleAuthorityRequestV1, code: string, message: string): LifecycleAuthorityResponseV1 {
512
+ return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'rejected', attestation: null, error: { code, message } };
513
+ }
43
514
 
44
515
  export type TaskControlControllerBinding = Readonly<{
45
516
  instanceDigest: string;
@@ -53,6 +524,7 @@ export type TaskControlExecutionContext =
53
524
  repoRoot: string;
54
525
  runtimeDir?: string;
55
526
  controllerBinding?: TaskControlControllerBinding | null;
527
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
56
528
  }>
57
529
  | Readonly<{
58
530
  source: 'sandbox-executor';
@@ -66,6 +538,7 @@ export type TaskControlExecutionContext =
66
538
  requestId: string;
67
539
  diagnosticLog?: OrchestrationDiagnosticLogger;
68
540
  controllerBinding?: TaskControlControllerBinding | null;
541
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
69
542
  }>;
70
543
 
71
544
  export type TaskControlOrchestrationIntent =
@@ -94,6 +567,14 @@ export type TaskControlOperation =
94
567
 
95
568
  export type TaskControlDispatchResult = TaskLifecycleResult | TaskFinalizationResult | OrchestrationResult;
96
569
 
570
+ export function taskControlOperationKey(operation: TaskControlOperation): string {
571
+ if (operation.family === 'task-finalization') return 'complete';
572
+ if (operation.family === 'task-lifecycle') return operation.request.intent;
573
+ return operation.intent === 'route'
574
+ ? operation.input && operation.input.completed === true ? 'route.clean-completion' : 'route.read'
575
+ : operation.intent;
576
+ }
577
+
97
578
  export type TaskLifecycleControlRequest = TaskLifecycleRequest & Readonly<{
98
579
  overrideTicket?: string;
99
580
  overrideTarget?: string;
@@ -121,13 +602,18 @@ export function createDirectHostExecutionContext(params: Readonly<{
121
602
  repoRoot: string;
122
603
  runtimeDir?: string;
123
604
  controllerBinding?: TaskControlControllerBinding | null;
605
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
124
606
  }>): TaskControlExecutionContext {
125
607
  return {
126
608
  source: 'direct-host',
127
609
  mode: 'direct-host',
128
610
  repoRoot: absolute('repoRoot', params.repoRoot),
129
611
  ...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) }),
130
- ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
612
+ ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
613
+ ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
614
+ lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
615
+ ? null : validateLifecycleRecoveryAttestation(params.lifecycleRecoveryAttestation)
616
+ })
131
617
  };
132
618
  }
133
619
 
@@ -141,6 +627,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
141
627
  requestId: string;
142
628
  diagnosticLog?: OrchestrationDiagnosticLogger;
143
629
  controllerBinding?: TaskControlControllerBinding | null;
630
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
144
631
  }>): TaskControlExecutionContext {
145
632
  if (!/^TASK-\d{8}-\d{6}$/u.test(params.taskId) || !params.generation || !params.requestId) {
146
633
  invalidContext('sandbox binding is incomplete');
@@ -160,7 +647,11 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
160
647
  manifestPath: absolute('manifestPath', params.manifestPath),
161
648
  requestId: params.requestId,
162
649
  ...(params.diagnosticLog === undefined ? {} : { diagnosticLog: params.diagnosticLog }),
163
- ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
650
+ ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
651
+ ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
652
+ lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
653
+ ? null : validateLifecycleRecoveryAttestation(params.lifecycleRecoveryAttestation)
654
+ })
164
655
  };
165
656
  }
166
657
 
@@ -170,6 +661,7 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
170
661
  absolute('repoRoot', context.repoRoot);
171
662
  if (context.runtimeDir !== undefined) absolute('runtimeDir', context.runtimeDir);
172
663
  binding(context.controllerBinding);
664
+ if (context.lifecycleRecoveryAttestation) validateLifecycleRecoveryAttestation(context.lifecycleRecoveryAttestation);
173
665
  return;
174
666
  }
175
667
  if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
@@ -184,6 +676,7 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
184
676
  invalidContext('runtimeDir is not bound to the manifest control root');
185
677
  }
186
678
  binding(context.controllerBinding);
679
+ if (context.lifecycleRecoveryAttestation) validateLifecycleRecoveryAttestation(context.lifecycleRecoveryAttestation);
187
680
  }
188
681
 
189
682
  function operationTaskId(context: TaskControlExecutionContext, taskRef: string): void {
@@ -284,12 +777,16 @@ function orchestration(
284
777
  client: input.client as AgentClientId,
285
778
  requestedModel: input.requestedModel as string | undefined,
286
779
  requestedReasoningEffort: input.requestedReasoningEffort as string | undefined,
287
- ...(input.capabilityToken === undefined ? {} : { capabilityToken: input.capabilityToken as string })
780
+ ...(input.capabilityRef === undefined ? {} : { capabilityRef: input.capabilityRef as string }),
781
+ ...(context.lifecycleRecoveryAttestation === undefined
782
+ ? {}
783
+ : { lifecycleRecoveryAttestation: context.lifecycleRecoveryAttestation })
288
784
  };
289
785
  if (prepareInput.client === 'codex') {
290
786
  return prepareCodexOrchestrationDelegation(operation.taskRef, prepareInput, {
291
787
  repoRoot: context.repoRoot,
292
788
  orchestrationOptions: options,
789
+ deferLifecycleRecoveryConsumption: context.source === 'sandbox-executor',
293
790
  ...(context.controllerBinding ? { controllerBinding: context.controllerBinding } : {})
294
791
  });
295
792
  }
@@ -403,7 +900,7 @@ const FINALIZATION_FLAGS = new Set(['--agent']);
403
900
  const ORCHESTRATION_FLAGS = new Set([
404
901
  '--agent', '--max-steps', '--executor-model', '--executor-reasoning-effort', '--reviewer-model',
405
902
  '--reviewer-reasoning-effort', '--client', '--requested-model', '--requested-reasoning-effort',
406
- '--capability-token', '--parent-id', '--before-fingerprint', '--stage', '--round', '--artifact', '--role',
903
+ '--capability-ref', '--parent-id', '--before-fingerprint', '--stage', '--round', '--artifact', '--role',
407
904
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
408
905
  '--model-fallback-reason', '--reasoning-effort-fallback-reason', '--exit-code', '--after-fingerprint',
409
906
  '--changed-paths', '--code', '--message', '--recoverable', '--git-worktree-root'
@@ -519,7 +1016,7 @@ export function parseTaskControlOperation(
519
1016
  if (parsedIntent === 'prepare') {
520
1017
  input.requestedModel = value(values, '--requested-model');
521
1018
  input.requestedReasoningEffort = value(values, '--requested-reasoning-effort');
522
- input.capabilityToken = value(values, '--capability-token');
1019
+ input.capabilityRef = value(values, '--capability-ref');
523
1020
  }
524
1021
  if (parsedIntent === 'await-activation') {
525
1022
  required(values, ['--stage', '--round', '--artifact', '--role']);