@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
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.js';
4
4
  import { parseTaskFrontmatter } from './frontmatter.js';
5
+ import { checkpointCommitMatches } from './commit-identity.js';
5
6
  import { resolveTaskContext, resolveTaskRef } from './resolve-ref.js';
6
7
  import { commitPushDecision } from './commit-policy.js';
7
8
  import { mergeOperationWarnings } from './operation-outcome.js';
@@ -163,21 +164,6 @@ function syncTaskCommit(input, task, committedHead) {
163
164
  return warning('TASK_STATUS_SYNC_FAILED', error instanceof Error ? error.message : String(error), true, 'task', task.taskId, 'ACTION_REQUIRED');
164
165
  }
165
166
  }
166
- function checkpointChildMatches(task, intent, head) {
167
- try {
168
- const parent = gitText(task.repoRoot, ['rev-parse', `${head}^`]);
169
- const tree = gitText(task.repoRoot, ['rev-parse', `${head}^{tree}`]);
170
- const message = gitText(task.repoRoot, ['show', '-s', '--format=%s', head]);
171
- const changed = gitText(task.repoRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', head]).split('\n').filter(Boolean).sort();
172
- return parent === intent.expectedHead
173
- && tree === intent.expectedTree
174
- && message === intent.message
175
- && JSON.stringify(changed) === JSON.stringify([...intent.paths].sort());
176
- }
177
- catch {
178
- return false;
179
- }
180
- }
181
167
  function checkpointIdentity(input, task, delivery) {
182
168
  if (delivery.mode !== 'local')
183
169
  throw new Error('checkpoint identity requires local delivery');
@@ -256,7 +242,7 @@ function executeUnlocked(input, task, mode) {
256
242
  operations: [{ name: 'commit', status: 'no-op' }], error: null
257
243
  };
258
244
  }
259
- if (inspected.snapshot.head !== identity.expectedHead && !checkpointChildMatches(task, pendingIntent, inspected.snapshot.head))
245
+ if (inspected.snapshot.head !== identity.expectedHead && !checkpointCommitMatches(task.repoRoot, pendingIntent, inspected.snapshot.head))
260
246
  return {
261
247
  status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
262
248
  error: { code: 'COMMIT_INTENT_HEAD_CONFLICT', message: 'Current HEAD does not match the pending checkpoint intent' }
@@ -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
  import { normalizeAgentToken } from '../agent-clients/tokens.js';
3
5
  import { isAgentClientId } from '../agent-clients/types.js';
4
6
  import { consumeHumanOverride, failureId, overrideDryRunConflict } from './human-override.js';
@@ -6,9 +8,357 @@ import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegatio
6
8
  import { prepareCodexOrchestrationDelegation } from './codex-orchestration.js';
7
9
  import { applyTaskFinalization } from './finalization.js';
8
10
  import { applyTaskLifecycle, taskLifecycleFailure } from './lifecycle.js';
11
+ import { locateActivityLog } from './activity-log.js';
9
12
  import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
10
13
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
11
14
  import { verifyTaskEvent } from './verification.js';
15
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
16
+ import { findArtifactRepairIntentsByOperation, writeArtifactRepairIntent } from './artifact-repair-intent.js';
17
+ import { computeLifecycleBuildIdentity, verifyLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
18
+ const lifecycleAuthorityPhases = [
19
+ 'orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'
20
+ ];
21
+ const lifecycleAuthorityFamilies = ['analysis', 'plan', 'code'];
22
+ const lifecycleAuthorityArtifact = /^(?:analysis|plan|code)(?:-r[1-9]\d*)?\.md$/u;
23
+ const lifecycleAuthorityDigest = (value) => crypto.createHash('sha256').update(value, 'utf8').digest('hex');
24
+ const lifecycleAuthorityBuildDigest = (value) => lifecycleAuthorityDigest(JSON.stringify(value));
25
+ export function lifecycleRecoveryAttestationDigest(value) {
26
+ return lifecycleAuthorityDigest(JSON.stringify(value));
27
+ }
28
+ function authorityExactKeys(value, keys) {
29
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
30
+ }
31
+ export function validateLifecycleAuthorityRequest(value) {
32
+ if (!value || typeof value !== 'object' || Array.isArray(value))
33
+ throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
34
+ const request = value;
35
+ if (!authorityExactKeys(request, [
36
+ 'artifact', 'authorityRef', 'expectedBuildIdentityDigest', 'expectedControlGeneration',
37
+ 'expectedControllerInstanceDigest', 'expectedHookDefinitionHash', 'family', 'lifecycleRequestId',
38
+ 'operationId', 'phase', 'requestId', 'round', 'taskId', 'version'
39
+ ])
40
+ || request.version !== 1
41
+ || typeof request.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.requestId)
42
+ || typeof request.operationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.operationId)
43
+ || !lifecycleAuthorityPhases.includes(request.phase)
44
+ || typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)
45
+ || !lifecycleAuthorityFamilies.includes(request.family)
46
+ || typeof request.artifact !== 'string' || !lifecycleAuthorityArtifact.test(request.artifact)
47
+ || !Number.isSafeInteger(request.round) || request.round < 1
48
+ || typeof request.lifecycleRequestId !== 'string' || !request.lifecycleRequestId || /[\r\n]/u.test(request.lifecycleRequestId)
49
+ || typeof request.authorityRef !== 'string' || !request.authorityRef || /[\r\n]/u.test(request.authorityRef)
50
+ || typeof request.expectedControlGeneration !== 'string' || !request.expectedControlGeneration
51
+ || typeof request.expectedControllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedControllerInstanceDigest)
52
+ || typeof request.expectedBuildIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedBuildIdentityDigest)
53
+ || typeof request.expectedHookDefinitionHash !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedHookDefinitionHash)) {
54
+ throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
55
+ }
56
+ const expectedArtifactFamily = request.artifact;
57
+ if (!expectedArtifactFamily.startsWith(request.family))
58
+ throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
59
+ return request;
60
+ }
61
+ export function validateLifecycleRecoveryAttestation(value, now = Date.now()) {
62
+ if (!value || typeof value !== 'object' || Array.isArray(value))
63
+ throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_INVALID');
64
+ const attestation = value;
65
+ if (!authorityExactKeys(attestation, [
66
+ 'artifact', 'attestationId', 'authorityRefDigest', 'buildIdentityDigest', 'controlGeneration',
67
+ 'controllerInstanceDigest', 'expiresAt', 'family', 'hookDefinitionHash', 'issuedAt',
68
+ 'lifecycleRequestId', 'operationId', 'phase', 'requestId', 'round', 'sessionId',
69
+ 'taskId', 'toolUseId', 'turnId', 'version'
70
+ ])
71
+ || attestation.version !== 1
72
+ || typeof attestation.attestationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.attestationId)
73
+ || typeof attestation.authorityRefDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.authorityRefDigest)
74
+ || typeof attestation.operationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.operationId)
75
+ || !lifecycleAuthorityPhases.includes(attestation.phase)
76
+ || typeof attestation.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.requestId)
77
+ || typeof attestation.lifecycleRequestId !== 'string' || !attestation.lifecycleRequestId
78
+ || typeof attestation.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(attestation.taskId)
79
+ || !lifecycleAuthorityFamilies.includes(attestation.family)
80
+ || typeof attestation.artifact !== 'string' || !lifecycleAuthorityArtifact.test(attestation.artifact)
81
+ || !Number.isSafeInteger(attestation.round) || attestation.round < 1
82
+ || typeof attestation.controlGeneration !== 'string' || !attestation.controlGeneration
83
+ || typeof attestation.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.controllerInstanceDigest)
84
+ || typeof attestation.sessionId !== 'string' || !attestation.sessionId
85
+ || typeof attestation.turnId !== 'string' || !attestation.turnId
86
+ || typeof attestation.toolUseId !== 'string' || !attestation.toolUseId
87
+ || typeof attestation.buildIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.buildIdentityDigest)
88
+ || typeof attestation.hookDefinitionHash !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.hookDefinitionHash)
89
+ || !Number.isSafeInteger(attestation.issuedAt) || !Number.isSafeInteger(attestation.expiresAt)
90
+ || attestation.expiresAt <= attestation.issuedAt
91
+ || attestation.expiresAt <= now) {
92
+ throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_INVALID');
93
+ }
94
+ return attestation;
95
+ }
96
+ const authorityOperationMap = new WeakMap();
97
+ const authorityRecords = new Map();
98
+ const consumedAuthorityPhases = new Set();
99
+ export function issueLifecycleRecoveryAttestation(input, options = {}) {
100
+ let request;
101
+ try {
102
+ request = validateLifecycleAuthorityRequest(input);
103
+ }
104
+ catch (error) {
105
+ return { version: 1, requestId: String(input?.requestId ?? ''), operationId: String(input?.operationId ?? ''), phase: input?.phase ?? 'artifact.finalize-local', status: 'rejected', attestation: null, error: { code: 'LIFECYCLE_AUTHORITY_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } };
106
+ }
107
+ const binding = options.controllerBinding ?? null;
108
+ if (!binding)
109
+ return authorityRejected(request, 'LOCAL_EXECUTION_AUTHORITY_UNAVAILABLE', 'current controller authority is unavailable');
110
+ if (binding.controlGeneration !== request.expectedControlGeneration
111
+ || binding.instanceDigest !== request.expectedControllerInstanceDigest) {
112
+ return authorityRejected(request, 'LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the request');
113
+ }
114
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(process.cwd());
115
+ if (lifecycleAuthorityBuildDigest(buildIdentity) !== request.expectedBuildIdentityDigest) {
116
+ return authorityRejected(request, 'LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the request');
117
+ }
118
+ const store = options.capabilityStore ?? createCodexCapabilityStore();
119
+ const registry = options.operationAttestations ?? authorityOperationMap.get(store) ?? new Map();
120
+ authorityOperationMap.set(store, registry);
121
+ const key = `${request.requestId}\0${request.operationId}\0${request.phase}`;
122
+ const prior = registry.get(key);
123
+ if (prior)
124
+ return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'already-completed', attestation: prior, error: null };
125
+ try {
126
+ const expected = {
127
+ taskId: request.taskId,
128
+ hookDefinitionHash: request.expectedHookDefinitionHash,
129
+ buildIdentity,
130
+ controller: binding
131
+ };
132
+ const record = store.reserveReference(request.authorityRef, request.operationId, expected);
133
+ if (!record.sessionId || !record.turnId || !record.toolUseId)
134
+ throw new Error('CODEX_CAPABILITY_IDENTITY_INVALID: capability hook attestation is incomplete');
135
+ const now = (options.now ?? Date.now)();
136
+ const attestation = validateLifecycleRecoveryAttestation({
137
+ version: 1,
138
+ attestationId: crypto.randomUUID(),
139
+ authorityRefDigest: lifecycleAuthorityDigest(request.authorityRef),
140
+ operationId: request.operationId,
141
+ phase: request.phase,
142
+ requestId: request.requestId,
143
+ lifecycleRequestId: request.lifecycleRequestId,
144
+ taskId: request.taskId,
145
+ family: request.family,
146
+ artifact: request.artifact,
147
+ round: request.round,
148
+ controlGeneration: binding.controlGeneration,
149
+ controllerInstanceDigest: binding.instanceDigest,
150
+ sessionId: record.sessionId,
151
+ turnId: record.turnId,
152
+ toolUseId: record.toolUseId,
153
+ buildIdentityDigest: lifecycleAuthorityBuildDigest(buildIdentity),
154
+ hookDefinitionHash: request.expectedHookDefinitionHash,
155
+ issuedAt: now,
156
+ expiresAt: Math.min(record.expiresAt, now + 30_000)
157
+ }, now);
158
+ store.reserveRecoveryPhase(request.authorityRef, request.operationId, request.phase, request.requestId, expected);
159
+ registry.set(key, attestation);
160
+ authorityRecords.set(attestation.attestationId, { store, capabilityRef: request.authorityRef, expected });
161
+ return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'issued', attestation, error: null };
162
+ }
163
+ catch (error) {
164
+ return authorityRejected(request, error instanceof Error && error.name.startsWith('CODEX_') ? error.name : 'LIFECYCLE_AUTHORITY_REJECTED', error instanceof Error ? error.message : String(error));
165
+ }
166
+ }
167
+ export function consumeLifecycleRecoveryAttestation(value, now = Date.now()) {
168
+ const attestation = validateLifecycleRecoveryAttestation(value, now);
169
+ if (consumedAuthorityPhases.has(attestation.attestationId))
170
+ return;
171
+ const record = authorityRecords.get(attestation.attestationId);
172
+ if (!record)
173
+ throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_UNKNOWN');
174
+ record.store.consumeRecoveryPhase(record.capabilityRef, attestation.operationId, attestation.phase, attestation.requestId, record.expected);
175
+ if (attestation.phase === 'task-event.completed') {
176
+ record.store.consumeReference(record.capabilityRef, attestation.operationId, record.expected);
177
+ }
178
+ consumedAuthorityPhases.add(attestation.attestationId);
179
+ }
180
+ export function queryLifecycleRecoveryOperation(operationId, options = {}) {
181
+ if (!/^[a-f0-9-]{16,64}$/u.test(operationId)) {
182
+ throw new Error('LIFECYCLE_RECOVERY_OPERATION_INVALID');
183
+ }
184
+ const store = options.capabilityStore ?? createCodexCapabilityStore();
185
+ const registry = authorityOperationMap.get(store);
186
+ const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
187
+ const intents = options.repoRoot ? findArtifactRepairIntentsByOperation(options.repoRoot, operationId) : [];
188
+ const capabilities = store.findByRecoveryOperation(operationId);
189
+ const phaseMap = new Map();
190
+ for (const capability of capabilities) {
191
+ for (const phase of capability.recoveryPhases) {
192
+ phaseMap.set(`${phase.phase}\0${phase.requestId}`, {
193
+ phase: phase.phase,
194
+ requestId: phase.requestId,
195
+ attestationId: null,
196
+ state: phase.state === 'consumed' ? 'consumed' : 'observed'
197
+ });
198
+ }
199
+ }
200
+ for (const attestation of known) {
201
+ const key = `${attestation.phase}\0${attestation.requestId}`;
202
+ const durable = phaseMap.get(key);
203
+ phaseMap.set(key, {
204
+ phase: attestation.phase,
205
+ requestId: attestation.requestId,
206
+ attestationId: attestation.attestationId,
207
+ state: durable?.state === 'consumed' || consumedAuthorityPhases.has(attestation.attestationId)
208
+ ? 'consumed' : 'issued'
209
+ });
210
+ }
211
+ for (const intent of intents) {
212
+ if (!intent.phase)
213
+ continue;
214
+ const key = `${intent.phase}\0${intent.requestId}`;
215
+ if (phaseMap.has(key))
216
+ continue;
217
+ phaseMap.set(key, {
218
+ phase: intent.phase,
219
+ requestId: intent.requestId,
220
+ attestationId: null,
221
+ state: intent.state === 'consumed' ? 'consumed' : 'observed'
222
+ });
223
+ }
224
+ const capabilityState = capabilities.some((record) => record.recoveryState === 'consumed')
225
+ ? 'consumed'
226
+ : capabilities.some((record) => record.recoveryState === 'reserved') ? 'reserved' : 'unknown';
227
+ const committed = capabilityState === 'consumed' || intents.some((intent) => intent.state === 'consumed');
228
+ return {
229
+ version: 1,
230
+ operationId,
231
+ status: committed ? 'committed' : capabilityState === 'reserved' || phaseMap.size > 0 ? 'in-progress' : 'unknown',
232
+ capabilityState,
233
+ phases: Object.freeze([...phaseMap.values()])
234
+ };
235
+ }
236
+ function lifecycleRecoveryCompensationError(code, message) {
237
+ const error = new Error(`${code}: ${message}`);
238
+ error.name = code;
239
+ return error;
240
+ }
241
+ function lifecycleRecoveryEventCommitted(selector, repoRoot) {
242
+ const resolved = resolveTaskRef(selector.taskId, { repoRoot });
243
+ if (!resolved.ok || resolved.taskId !== selector.taskId)
244
+ return false;
245
+ let content;
246
+ try {
247
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ const section = locateActivityLog(content);
253
+ if (!section)
254
+ return false;
255
+ const labels = { analysis: 'Analyze Task', plan: 'Plan Task', code: 'Code Task' };
256
+ const prefix = `${labels[selector.family]} (Round ${selector.round}`;
257
+ return section.entries.some((entry) => {
258
+ if (entry.step.endsWith(' [started]') || !entry.step.startsWith(prefix))
259
+ return false;
260
+ const delimiter = entry.step[prefix.length];
261
+ return (delimiter === ',' || delimiter === ')') && entry.note.endsWith(`→ ${selector.artifact}`);
262
+ });
263
+ }
264
+ function lifecycleRecoveryIntentMatches(intent, selector) {
265
+ return intent.taskId === selector.taskId
266
+ && intent.family === selector.family
267
+ && intent.artifact === selector.artifact
268
+ && (intent.state === 'commit-started' || intent.state === 'consumed');
269
+ }
270
+ /**
271
+ * Completes only the durable tail of a committed task event after a process
272
+ * restart. The task event itself is the commit boundary and is never replayed.
273
+ */
274
+ export function recoverLifecycleRecoveryOperation(input, options) {
275
+ const selector = validateLifecycleAuthorityRequest(input);
276
+ if (selector.phase !== 'task-event.completed') {
277
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_UNSUPPORTED', 'only task-event.completed supports durable compensation');
278
+ }
279
+ const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(options.repoRoot);
280
+ if (selector.expectedBuildIdentityDigest !== lifecycleAuthorityBuildDigest(buildIdentity)) {
281
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the recovery request');
282
+ }
283
+ if (selector.expectedControlGeneration !== options.controllerBinding.controlGeneration
284
+ || selector.expectedControllerInstanceDigest !== options.controllerBinding.instanceDigest) {
285
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the recovery request');
286
+ }
287
+ const store = options.capabilityStore ?? createCodexCapabilityStore();
288
+ const expected = {
289
+ taskId: selector.taskId,
290
+ hookDefinitionHash: selector.expectedHookDefinitionHash,
291
+ buildIdentity,
292
+ controller: options.controllerBinding
293
+ };
294
+ const capabilities = store.findByRecoveryOperation(selector.operationId);
295
+ if (capabilities.length !== 1 || capabilities[0].capabilityRefDigest !== lifecycleAuthorityDigest(selector.authorityRef)) {
296
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not identify this operation');
297
+ }
298
+ const capability = capabilities[0];
299
+ const build = verifyLifecycleBuildIdentity(capability.buildIdentity, buildIdentity);
300
+ const expiredReserved = capability.status === 'expired' && capability.recoveryState === 'reserved';
301
+ if (!build.ok
302
+ || capability.taskId !== selector.taskId
303
+ || capability.hookDefinitionHash !== selector.expectedHookDefinitionHash
304
+ || capability.controller?.controlGeneration !== options.controllerBinding.controlGeneration
305
+ || capability.controller?.instanceDigest !== options.controllerBinding.instanceDigest
306
+ || capability.recoveryState === 'unreserved'
307
+ || capability.recoveryOperationId !== selector.operationId
308
+ || (!['attested', 'consumed'].includes(capability.status) && !expiredReserved)) {
309
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not match the recovery request');
310
+ }
311
+ const phase = capability.recoveryPhases.find((entry) => entry.phase === selector.phase);
312
+ if (!phase) {
313
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
314
+ }
315
+ const intents = findArtifactRepairIntentsByOperation(options.repoRoot, selector.operationId)
316
+ .filter((intent) => intent.taskId === selector.taskId
317
+ && intent.family === selector.family
318
+ && intent.artifact === selector.artifact);
319
+ if (intents.length !== 1) {
320
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not identify this task event');
321
+ }
322
+ const intent = intents[0];
323
+ if (!lifecycleRecoveryIntentMatches(intent, selector)
324
+ || intent.phase !== selector.phase
325
+ || intent.requestId !== selector.lifecycleRequestId
326
+ || intent.authorityDigest === null) {
327
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not match the recovery request');
328
+ }
329
+ if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
330
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
331
+ }
332
+ if (capability.recoveryState === 'reserved') {
333
+ if (phase.state === 'issued') {
334
+ store.consumeRecoveryPhase(selector.authorityRef, selector.operationId, selector.phase, phase.requestId, expected, { allowExpiredReserved: true });
335
+ }
336
+ store.consumeReference(selector.authorityRef, selector.operationId, expected, { allowExpiredReserved: true });
337
+ }
338
+ else if (phase.state !== 'consumed') {
339
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
340
+ }
341
+ if (intent.state !== 'consumed') {
342
+ writeArtifactRepairIntent(options.repoRoot, {
343
+ ...intent,
344
+ state: 'consumed',
345
+ updatedAt: (options.now ?? Date.now)()
346
+ }, { expected: intent });
347
+ }
348
+ return queryLifecycleRecoveryOperation(selector.operationId, { repoRoot: options.repoRoot, capabilityStore: store });
349
+ }
350
+ function authorityRejected(request, code, message) {
351
+ return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'rejected', attestation: null, error: { code, message } };
352
+ }
353
+ export function taskControlOperationKey(operation) {
354
+ if (operation.family === 'task-finalization')
355
+ return 'complete';
356
+ if (operation.family === 'task-lifecycle')
357
+ return operation.request.intent;
358
+ return operation.intent === 'route'
359
+ ? operation.input && operation.input.completed === true ? 'route.clean-completion' : 'route.read'
360
+ : operation.intent;
361
+ }
12
362
  function invalidContext(message) {
13
363
  throw new Error(`TASK_CONTROL_CONTEXT_INVALID: ${message}`);
14
364
  }
@@ -31,7 +381,11 @@ export function createDirectHostExecutionContext(params) {
31
381
  mode: 'direct-host',
32
382
  repoRoot: absolute('repoRoot', params.repoRoot),
33
383
  ...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) }),
34
- ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
384
+ ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
385
+ ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
386
+ lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
387
+ ? null : validateLifecycleRecoveryAttestation(params.lifecycleRecoveryAttestation)
388
+ })
35
389
  };
36
390
  }
37
391
  export function createSandboxExecutorExecutionContext(params) {
@@ -53,7 +407,11 @@ export function createSandboxExecutorExecutionContext(params) {
53
407
  manifestPath: absolute('manifestPath', params.manifestPath),
54
408
  requestId: params.requestId,
55
409
  ...(params.diagnosticLog === undefined ? {} : { diagnosticLog: params.diagnosticLog }),
56
- ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
410
+ ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
411
+ ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
412
+ lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
413
+ ? null : validateLifecycleRecoveryAttestation(params.lifecycleRecoveryAttestation)
414
+ })
57
415
  };
58
416
  }
59
417
  export function assertTaskControlExecutionContext(context) {
@@ -64,6 +422,8 @@ export function assertTaskControlExecutionContext(context) {
64
422
  if (context.runtimeDir !== undefined)
65
423
  absolute('runtimeDir', context.runtimeDir);
66
424
  binding(context.controllerBinding);
425
+ if (context.lifecycleRecoveryAttestation)
426
+ validateLifecycleRecoveryAttestation(context.lifecycleRecoveryAttestation);
67
427
  return;
68
428
  }
69
429
  if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
@@ -78,6 +438,8 @@ export function assertTaskControlExecutionContext(context) {
78
438
  invalidContext('runtimeDir is not bound to the manifest control root');
79
439
  }
80
440
  binding(context.controllerBinding);
441
+ if (context.lifecycleRecoveryAttestation)
442
+ validateLifecycleRecoveryAttestation(context.lifecycleRecoveryAttestation);
81
443
  }
82
444
  function operationTaskId(context, taskRef) {
83
445
  if (context.source === 'sandbox-executor' && taskRef !== context.taskId) {
@@ -166,12 +528,16 @@ function orchestration(context, operation) {
166
528
  client: input.client,
167
529
  requestedModel: input.requestedModel,
168
530
  requestedReasoningEffort: input.requestedReasoningEffort,
169
- ...(input.capabilityToken === undefined ? {} : { capabilityToken: input.capabilityToken })
531
+ ...(input.capabilityRef === undefined ? {} : { capabilityRef: input.capabilityRef }),
532
+ ...(context.lifecycleRecoveryAttestation === undefined
533
+ ? {}
534
+ : { lifecycleRecoveryAttestation: context.lifecycleRecoveryAttestation })
170
535
  };
171
536
  if (prepareInput.client === 'codex') {
172
537
  return prepareCodexOrchestrationDelegation(operation.taskRef, prepareInput, {
173
538
  repoRoot: context.repoRoot,
174
539
  orchestrationOptions: options,
540
+ deferLifecycleRecoveryConsumption: context.source === 'sandbox-executor',
175
541
  ...(context.controllerBinding ? { controllerBinding: context.controllerBinding } : {})
176
542
  });
177
543
  }
@@ -256,7 +622,7 @@ const FINALIZATION_FLAGS = new Set(['--agent']);
256
622
  const ORCHESTRATION_FLAGS = new Set([
257
623
  '--agent', '--max-steps', '--executor-model', '--executor-reasoning-effort', '--reviewer-model',
258
624
  '--reviewer-reasoning-effort', '--client', '--requested-model', '--requested-reasoning-effort',
259
- '--capability-token', '--parent-id', '--before-fingerprint', '--stage', '--round', '--artifact', '--role',
625
+ '--capability-ref', '--parent-id', '--before-fingerprint', '--stage', '--round', '--artifact', '--role',
260
626
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
261
627
  '--model-fallback-reason', '--reasoning-effort-fallback-reason', '--exit-code', '--after-fingerprint',
262
628
  '--changed-paths', '--code', '--message', '--recoverable', '--git-worktree-root'
@@ -372,7 +738,7 @@ export function parseTaskControlOperation(family, args) {
372
738
  if (parsedIntent === 'prepare') {
373
739
  input.requestedModel = value(values, '--requested-model');
374
740
  input.requestedReasoningEffort = value(values, '--requested-reasoning-effort');
375
- input.capabilityToken = value(values, '--capability-token');
741
+ input.capabilityRef = value(values, '--capability-ref');
376
742
  }
377
743
  if (parsedIntent === 'await-activation') {
378
744
  required(values, ['--stage', '--round', '--artifact', '--role']);
@@ -0,0 +1,128 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isCompletionEvidence } from './orchestration.js';
3
+ import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.js';
4
+ /** Normalize the command result once at the control transport boundary. */
5
+ export function parseControlOutput(output) {
6
+ if (output === null)
7
+ return null;
8
+ try {
9
+ const value = JSON.parse(output);
10
+ if (!value || typeof value !== 'object' || Array.isArray(value))
11
+ return null;
12
+ return value.result && typeof value.result === 'object' && !Array.isArray(value.result) ? value.result : value;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
19
+ 'completed', 'evidence-written', 'publish-authorized'
20
+ ]);
21
+ const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'];
22
+ const ORCHESTRATION_INTENTS = [
23
+ 'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
24
+ 'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
25
+ ];
26
+ export const SANDBOX_CONTROL_RECOVERY_OPERATIONS = Object.freeze([
27
+ ...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle', intent, class: 'lifecycle-mutation' })),
28
+ { family: 'task-finalization', intent: 'complete', class: 'finalization' },
29
+ ...ORCHESTRATION_INTENTS.map((intent) => ({
30
+ family: 'task-orchestration',
31
+ intent,
32
+ class: intent === 'route.clean-completion' ? 'route.clean-completion' : intent === 'route.read' || intent === 'status' ? 'read-only' : 'orchestration'
33
+ })),
34
+ { family: 'task-create', intent: 'create', class: 'task-create' },
35
+ ...TASK_WORKFLOW_OPERATIONS.map((intent) => ({ family: 'task-workflow', intent, class: intent === 'artifact-inspect' || intent === 'decision-next-id' ? 'read-only' : 'workflow' })),
36
+ ...['open', 'close', 'verify'].map((intent) => ({ family: 'codex-controller', intent, class: 'codex-controller' }))
37
+ ]);
38
+ export function digestControlRecoveryIntent(family, intent) {
39
+ return createHash('sha256').update(`${family}\0${intent}`, 'utf8').digest('hex');
40
+ }
41
+ export function findSandboxControlRecoveryOperation(family, intent) {
42
+ return SANDBOX_CONTROL_RECOVERY_OPERATIONS.find((operation) => operation.family === family && operation.intent === intent) ?? null;
43
+ }
44
+ function bindingMatchesResult(binding, result) {
45
+ return result.requestId === binding.requestId
46
+ && result.generation === binding.generation
47
+ && (result.taskId === undefined || result.taskId === binding.taskId)
48
+ && (result.intentDigest === undefined || result.intentDigest === binding.intentDigest);
49
+ }
50
+ function domainEvidenceMatches(operation, result, domain) {
51
+ if (!domain)
52
+ return false;
53
+ if (domain.consistent !== true)
54
+ return false;
55
+ if (operation.class === 'route.clean-completion') {
56
+ const completion = domain.completionEvidence;
57
+ const snapshot = domain.snapshot;
58
+ const lastReviewedCommit = domain.lastReviewedCommit;
59
+ return result.status === 'completed'
60
+ && domain.status === 'completed'
61
+ && domain.pendingDelegation === null
62
+ && isCompletionEvidence(completion)
63
+ && snapshot?.head === completion.head
64
+ && snapshot.headTree === completion.headTree
65
+ && snapshot.worktreeTree === completion.worktreeTree
66
+ && lastReviewedCommit === completion.lastReviewedCommit;
67
+ }
68
+ if (operation.class === 'read-only')
69
+ return result.changed === false && domain.snapshotValid === true;
70
+ return true;
71
+ }
72
+ export function classifySandboxControlRecovery(input) {
73
+ const result = input.terminalResult ?? null;
74
+ if (input.evidenceConflict)
75
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_EVIDENCE_CONFLICT' };
76
+ if (input.explicitRejection && !input.startedCommitted) {
77
+ return { outcome: 'not-executed', responseReconstructable: true, reasonCode: 'RECOVERY_EXPLICIT_REJECTION' };
78
+ }
79
+ if (!input.startedCommitted) {
80
+ return { outcome: 'not-executed', responseReconstructable: false, reasonCode: 'RECOVERY_NOT_STARTED' };
81
+ }
82
+ if (!result)
83
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_TERMINAL_RESULT_MISSING' };
84
+ if (!bindingMatchesResult(input.binding, result)) {
85
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_RESULT_BINDING_MISMATCH' };
86
+ }
87
+ if (!SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES.every((phase) => input.criticalPhases?.includes(phase))) {
88
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_CRITICAL_AUDIT_INCOMPLETE' };
89
+ }
90
+ const status = result.status;
91
+ const failed = status === 'failed' || result.error !== null && result.error !== undefined;
92
+ if (failed) {
93
+ if (input.domain && input.domain.consistent === false) {
94
+ return { outcome: 'failure', responseReconstructable: true, reasonCode: 'RECOVERY_TERMINAL_FAILURE' };
95
+ }
96
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_FAILURE_EVIDENCE_INCOMPLETE' };
97
+ }
98
+ if (result.changed === null) {
99
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_RESULT_CHANGE_FLAG_MISSING' };
100
+ }
101
+ if (!domainEvidenceMatches(input.operation, result, input.domain)) {
102
+ if (input.journal?.exists && (input.journal.completedSteps?.length ?? 0) > 0) {
103
+ return { outcome: 'in-progress', responseReconstructable: false, reasonCode: 'RECOVERY_DOMAIN_EVIDENCE_INCOMPLETE' };
104
+ }
105
+ return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_DOMAIN_EVIDENCE_MISSING' };
106
+ }
107
+ if (input.operation.family === 'task-lifecycle' && input.journal?.exists) {
108
+ if (input.journal.failure) {
109
+ return { outcome: 'failure', responseReconstructable: true, reasonCode: 'RECOVERY_LIFECYCLE_JOURNAL_FAILURE' };
110
+ }
111
+ return {
112
+ outcome: input.journal.completedSteps && input.journal.completedSteps.length > 0 ? 'in-progress' : 'unknown',
113
+ responseReconstructable: false,
114
+ reasonCode: 'RECOVERY_LIFECYCLE_JOURNAL_PRESENT'
115
+ };
116
+ }
117
+ return {
118
+ outcome: 'success',
119
+ responseReconstructable: true,
120
+ reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
121
+ ? 'RECOVERY_ROUTE_COMPLETION_NOOP'
122
+ : 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
123
+ };
124
+ }
125
+ export function operationRecoveryBinding(requestId, generation, taskId, family, intent) {
126
+ return { requestId, generation, taskId, intentDigest: digestControlRecoveryIntent(family, intent) };
127
+ }
128
+ //# sourceMappingURL=control-recovery.js.map