@fitlab-ai/agent-infra 0.9.15 → 0.11.0

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 (224) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  7. package/dist/lib/fs/durable-write.js +2 -0
  8. package/dist/lib/internal/cli-route-inventory.js +3 -4
  9. package/dist/lib/internal/manual-validation.js +17 -8
  10. package/dist/lib/internal/platform-comment.js +60 -2
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-finalization.js +2 -2
  13. package/dist/lib/internal/task-lifecycle.js +4 -4
  14. package/dist/lib/internal/task-operation-registry.js +22 -13
  15. package/dist/lib/internal/task-orchestration.js +2 -2
  16. package/dist/lib/internal/task-review.js +12 -7
  17. package/dist/lib/platform/activity-recovery.js +159 -0
  18. package/dist/lib/platform/issue-comments.js +89 -31
  19. package/dist/lib/platform/issues.js +1 -1
  20. package/dist/lib/platform/pr-change-report.js +5 -4
  21. package/dist/lib/platform/pr-summary.js +64 -32
  22. package/dist/lib/platform/pull-requests.js +3 -3
  23. package/dist/lib/platform/task-comment-projection.js +70 -0
  24. package/dist/lib/platform/task-identities.js +2 -9
  25. package/dist/lib/platform/task-recovery.js +55 -0
  26. package/dist/lib/platform/verification-sync.js +45 -7
  27. package/dist/lib/pr-review/evidence-grading.js +4 -8
  28. package/dist/lib/sandbox/commands/rm.js +21 -2
  29. package/dist/lib/sandbox/control/executor.js +4 -2
  30. package/dist/lib/sandbox/control/server.js +29 -9
  31. package/dist/lib/sandbox/control/state.js +7 -1
  32. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  33. package/dist/lib/sandbox/control/workflow-executor.js +87 -35
  34. package/dist/lib/sandbox/index.js +1 -2
  35. package/dist/lib/sandbox/removal.js +116 -1011
  36. package/dist/lib/sandbox/workspace-identity.js +17 -1
  37. package/dist/lib/server/process-state.js +17 -5
  38. package/dist/lib/task/artifact-command.js +11 -30
  39. package/dist/lib/task/artifact-operations.js +17 -231
  40. package/dist/lib/task/artifact-recovery.js +476 -0
  41. package/dist/lib/task/artifact-repair-intent.js +90 -39
  42. package/dist/lib/task/commit-operation.js +31 -4
  43. package/dist/lib/task/control-authority.js +39 -8
  44. package/dist/lib/task/control-recovery.js +14 -2
  45. package/dist/lib/task/create-service.js +1 -8
  46. package/dist/lib/task/delegation-receipts.js +64 -3
  47. package/dist/lib/task/events.js +165 -23
  48. package/dist/lib/task/human-override.js +5 -8
  49. package/dist/lib/task/intermediate-cleanup.js +91 -8
  50. package/dist/lib/task/lifecycle-recovery.js +549 -0
  51. package/dist/lib/task/lifecycle.js +12 -3
  52. package/dist/lib/task/local-artifact-finalization.js +212 -123
  53. package/dist/lib/task/manual-validation-receipt.js +5 -1
  54. package/dist/lib/task/orchestration.js +52 -2
  55. package/dist/lib/task/pr-delivery-fact.js +12 -56
  56. package/dist/lib/task/recovery-warning.js +19 -0
  57. package/dist/lib/task/review-command.js +3 -2
  58. package/dist/lib/task/review-finalization.js +294 -74
  59. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  60. package/dist/lib/task/short-id.js +15 -7
  61. package/dist/lib/task/task-execution-lock.js +1 -1
  62. package/dist/lib/task/verification-engine.js +3 -14
  63. package/dist/lib/task/workflow-command.js +2 -1
  64. package/dist/lib/task/workflow-dispatch.js +161 -0
  65. package/dist/lib/version.js +1 -11
  66. package/dist/lifecycle-build-manifest.json +3 -3
  67. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  68. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  69. package/lib/fs/durable-write.ts +1 -0
  70. package/lib/internal/cli-route-inventory.ts +3 -4
  71. package/lib/internal/manual-validation.ts +17 -8
  72. package/lib/internal/platform-comment.ts +53 -2
  73. package/lib/internal/task-artifact.ts +1 -1
  74. package/lib/internal/task-finalization.ts +1 -1
  75. package/lib/internal/task-lifecycle.ts +5 -4
  76. package/lib/internal/task-operation-registry.ts +22 -13
  77. package/lib/internal/task-orchestration.ts +1 -1
  78. package/lib/internal/task-review.ts +11 -7
  79. package/lib/platform/activity-recovery.ts +179 -0
  80. package/lib/platform/issue-comments.ts +105 -30
  81. package/lib/platform/issues.ts +1 -1
  82. package/lib/platform/pr-change-report.ts +7 -5
  83. package/lib/platform/pr-summary.ts +64 -32
  84. package/lib/platform/pull-requests.ts +3 -3
  85. package/lib/platform/task-comment-projection.ts +79 -0
  86. package/lib/platform/task-identities.ts +3 -14
  87. package/lib/platform/task-recovery.ts +54 -0
  88. package/lib/platform/verification-sync.ts +48 -11
  89. package/lib/pr-review/evidence-grading.ts +4 -9
  90. package/lib/sandbox/commands/rm.ts +34 -2
  91. package/lib/sandbox/control/executor.ts +4 -2
  92. package/lib/sandbox/control/protocol.ts +6 -1
  93. package/lib/sandbox/control/server.ts +30 -10
  94. package/lib/sandbox/control/state.ts +9 -1
  95. package/lib/sandbox/control/task-workflow.ts +1 -1
  96. package/lib/sandbox/control/workflow-executor.ts +106 -35
  97. package/lib/sandbox/index.ts +1 -2
  98. package/lib/sandbox/removal.ts +126 -1128
  99. package/lib/sandbox/workspace-identity.ts +19 -3
  100. package/lib/server/process-state.ts +26 -5
  101. package/lib/task/artifact-command.ts +12 -29
  102. package/lib/task/artifact-operations.ts +22 -252
  103. package/lib/task/artifact-recovery.ts +572 -0
  104. package/lib/task/artifact-repair-intent.ts +126 -51
  105. package/lib/task/commit-operation.ts +35 -4
  106. package/lib/task/control-authority.ts +48 -15
  107. package/lib/task/control-recovery.ts +13 -1
  108. package/lib/task/create-service.ts +1 -10
  109. package/lib/task/delegation-receipts.ts +73 -2
  110. package/lib/task/events.ts +188 -24
  111. package/lib/task/human-override.ts +5 -8
  112. package/lib/task/intermediate-cleanup.ts +66 -12
  113. package/lib/task/lifecycle-recovery.ts +691 -0
  114. package/lib/task/lifecycle.ts +13 -4
  115. package/lib/task/local-artifact-finalization.ts +249 -131
  116. package/lib/task/manual-validation-receipt.ts +5 -1
  117. package/lib/task/orchestration.ts +65 -0
  118. package/lib/task/pr-delivery-fact.ts +15 -62
  119. package/lib/task/recovery-warning.ts +21 -0
  120. package/lib/task/review-command.ts +2 -1
  121. package/lib/task/review-finalization.ts +298 -84
  122. package/lib/{host-control → task}/secure-fs.ts +10 -32
  123. package/lib/task/short-id.ts +27 -5
  124. package/lib/task/task-execution-lock.ts +1 -1
  125. package/lib/task/verification-engine.ts +3 -17
  126. package/lib/task/verification-types.ts +0 -2
  127. package/lib/task/workflow-command.ts +2 -1
  128. package/lib/task/workflow-dispatch.ts +171 -0
  129. package/lib/version.ts +1 -15
  130. package/package.json +1 -1
  131. package/templates/.agents/rules/create-issue.en.md +1 -1
  132. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  133. package/templates/.agents/rules/issue-sync.en.md +12 -2
  134. package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
  135. package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
  136. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
  137. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  138. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  139. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  140. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  141. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  142. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  143. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  144. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  145. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  146. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  147. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  148. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
  149. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  150. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  151. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  152. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  153. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  154. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  155. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  156. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  157. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  158. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  159. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  160. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  161. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  162. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  163. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  164. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  165. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  166. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  167. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  168. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  169. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  170. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  171. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  172. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  173. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  174. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  175. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  176. package/templates/.agents/skills/restore-task/SKILL.en.md +11 -6
  177. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
  178. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  179. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  180. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  181. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  182. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  183. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  184. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  185. package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
  186. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
  187. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  188. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  189. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  190. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  191. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  192. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  193. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  194. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  195. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  196. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  197. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  198. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  199. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  200. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  201. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  202. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  203. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/templates/task.en.md +1 -1
  206. package/templates/.agents/templates/task.zh-CN.md +1 -1
  207. package/templates/.claude/commands/import-issue.en.md +1 -1
  208. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  209. package/templates/.claude/commands/restore-task.en.md +1 -1
  210. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
  211. package/dist/lib/host-control/audit.js +0 -19
  212. package/dist/lib/host-control/client.js +0 -142
  213. package/dist/lib/host-control/command.js +0 -49
  214. package/dist/lib/host-control/path.js +0 -160
  215. package/dist/lib/host-control/server.js +0 -159
  216. package/dist/lib/host-control/service.js +0 -112
  217. package/dist/lib/internal/host-control.js +0 -28
  218. package/lib/host-control/audit.ts +0 -22
  219. package/lib/host-control/client.ts +0 -172
  220. package/lib/host-control/command.ts +0 -60
  221. package/lib/host-control/path.ts +0 -167
  222. package/lib/host-control/server.ts +0 -180
  223. package/lib/host-control/service.ts +0 -133
  224. package/lib/internal/host-control.ts +0 -28
@@ -3,36 +3,63 @@ import path from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
4
 
5
5
  import type { ArtifactSchemaFamily } from './artifact-schema.ts';
6
+ import { parseArtifactName } from './artifact-name.ts';
6
7
 
7
- export type ArtifactRepairPhase =
8
+ export type ArtifactRecoveryPhase =
8
9
  | 'orchestration.prepare'
9
10
  | 'artifact.finalize-local'
10
11
  | 'task-event.completed';
11
- export type ArtifactRepairState = 'awaiting-repair' | 'finalize-ready' | 'commit-started' | 'passed' | 'consumed';
12
+ export type ArtifactRecoveryState =
13
+ | 'awaiting-preflight-recovery'
14
+ | 'preflight-ready'
15
+ | 'preflight-commit-started'
16
+ | 'preflight-passed'
17
+ | 'full-finalizer-ready'
18
+ | 'commit-started'
19
+ | 'passed'
20
+ | 'consumption-started'
21
+ | 'consumed'
22
+ | 'aborted';
12
23
 
13
- export type ArtifactRepairIntent = Readonly<{
14
- version: 2;
24
+ export type ArtifactRecoveryIntent = Readonly<{
25
+ version: 4;
15
26
  taskId: string;
16
27
  family: ArtifactSchemaFamily;
17
28
  artifact: string;
18
- state: ArtifactRepairState;
19
- baselineSemanticDigest: string | null;
20
- artifactSha256: string;
21
- semanticDigest: string;
22
- recoveryOperationId: string | null;
23
- phase: ArtifactRepairPhase | null;
29
+ round: number;
30
+ state: ArtifactRecoveryState;
31
+ baselineSha256: string;
32
+ baselineSemanticDigest: string;
33
+ stagingId: string;
34
+ candidateSha256: string | null;
35
+ preflightArtifactSha256: string | null;
36
+ preflightSemanticDigest: string | null;
37
+ activeGenerationSha256: string | null;
38
+ activeGenerationSemanticDigest: string | null;
39
+ pendingGenerationSha256: string | null;
40
+ pendingGenerationSemanticDigest: string | null;
41
+ finalArtifactSha256: string | null;
42
+ finalSemanticDigest: string | null;
43
+ recoveryOperationId: string;
44
+ phase: ArtifactRecoveryPhase | null;
24
45
  authorityDigest: string | null;
25
46
  requestId: string;
47
+ errorCode: string | null;
48
+ errorMessage: string | null;
26
49
  createdAt: number;
27
50
  updatedAt: number;
28
51
  }>;
29
52
 
30
- function intentRoot(repoRoot: string): string {
53
+ export function artifactRecoveryIntentRoot(repoRoot: string): string {
31
54
  return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
32
55
  }
33
56
 
57
+ export function artifactRecoveryRoot(taskDir: string, stagingId: string): string {
58
+ return path.join(taskDir, '.local-artifact-recovery', stagingId);
59
+ }
60
+
34
61
  function intentPath(repoRoot: string, taskId: string, family: ArtifactSchemaFamily, artifact: string): string {
35
- return path.join(intentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
62
+ return path.join(artifactRecoveryIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
36
63
  }
37
64
 
38
65
  function validDigest(value: unknown): value is string {
@@ -43,82 +70,132 @@ function validTaskId(value: unknown): value is string {
43
70
  return typeof value === 'string' && /^TASK-\d{8}-\d{6}$/u.test(value);
44
71
  }
45
72
 
73
+ function validToken(value: unknown): value is string {
74
+ return typeof value === 'string' && value.length > 0 && !/[\r\n]/u.test(value);
75
+ }
76
+
77
+ function validRecoveryId(value: unknown): value is string {
78
+ return typeof value === 'string' && /^[a-f0-9-]{16,64}$/u.test(value);
79
+ }
80
+
81
+ function validArtifact(value: unknown, family: unknown, round: unknown): value is string {
82
+ if (typeof value !== 'string' || path.basename(value) !== value || !value.endsWith('.md')) return false;
83
+ const parsed = parseArtifactName(value);
84
+ return Boolean(parsed && parsed.family === family && parsed.round === round);
85
+ }
86
+
46
87
  function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
47
88
  return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
48
89
  }
49
90
 
50
- export function isArtifactRepairIntent(value: unknown): value is ArtifactRepairIntent {
91
+ export function isArtifactRecoveryIntent(value: unknown): value is ArtifactRecoveryIntent {
51
92
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
52
93
  const intent = value as Record<string, unknown>;
53
94
  return exactKeys(intent, [
54
- 'artifact', 'artifactSha256', 'authorityDigest', 'baselineSemanticDigest', 'createdAt',
55
- 'family', 'phase', 'recoveryOperationId', 'requestId', 'semanticDigest', 'state',
56
- 'taskId', 'updatedAt', 'version'
95
+ 'activeGenerationSemanticDigest', 'activeGenerationSha256', 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
96
+ 'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
97
+ 'pendingGenerationSemanticDigest', 'pendingGenerationSha256', 'phase', 'preflightArtifactSha256', 'preflightSemanticDigest', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
98
+ 'version'
57
99
  ])
58
- && intent.version === 2
100
+ && intent.version === 4
59
101
  && validTaskId(intent.taskId)
60
102
  && typeof intent.family === 'string'
61
103
  && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
62
- && typeof intent.artifact === 'string' && intent.artifact.length > 0
63
- && ['awaiting-repair', 'finalize-ready', 'commit-started', 'passed', 'consumed'].includes(String(intent.state))
64
- && (intent.baselineSemanticDigest === null || validDigest(intent.baselineSemanticDigest))
65
- && validDigest(intent.artifactSha256)
66
- && validDigest(intent.semanticDigest)
67
- && (intent.recoveryOperationId === null || (typeof intent.recoveryOperationId === 'string' && /^[a-f0-9-]{16,64}$/u.test(intent.recoveryOperationId)))
104
+ && Number.isSafeInteger(intent.round) && (intent.round as number) > 0
105
+ && validArtifact(intent.artifact, intent.family, intent.round)
106
+ && ['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started', 'passed', 'consumption-started', 'consumed', 'aborted'].includes(String(intent.state))
107
+ && validDigest(intent.baselineSha256)
108
+ && validDigest(intent.baselineSemanticDigest)
109
+ && validRecoveryId(intent.stagingId)
110
+ && (intent.candidateSha256 === null || validDigest(intent.candidateSha256))
111
+ && (intent.preflightArtifactSha256 === null || validDigest(intent.preflightArtifactSha256))
112
+ && (intent.preflightSemanticDigest === null || validDigest(intent.preflightSemanticDigest))
113
+ && (intent.activeGenerationSha256 === null || validDigest(intent.activeGenerationSha256))
114
+ && (intent.activeGenerationSemanticDigest === null || validDigest(intent.activeGenerationSemanticDigest))
115
+ && (intent.pendingGenerationSha256 === null || validDigest(intent.pendingGenerationSha256))
116
+ && (intent.pendingGenerationSemanticDigest === null || validDigest(intent.pendingGenerationSemanticDigest))
117
+ && (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
118
+ && (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
119
+ && validRecoveryId(intent.recoveryOperationId)
120
+ && intent.stagingId === intent.recoveryOperationId
68
121
  && (intent.phase === null || ['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(String(intent.phase)))
69
122
  && (intent.authorityDigest === null || validDigest(intent.authorityDigest))
70
- && typeof intent.requestId === 'string' && intent.requestId.length > 0 && !/[\r\n]/u.test(intent.requestId)
71
- && Number.isSafeInteger(intent.createdAt) && Number.isSafeInteger(intent.updatedAt)
72
- && (intent.state === 'awaiting-repair' ? intent.baselineSemanticDigest === intent.semanticDigest : true);
123
+ && validToken(intent.requestId)
124
+ && (intent.errorCode === null || validToken(intent.errorCode))
125
+ && (intent.errorMessage === null || validToken(intent.errorMessage))
126
+ && Number.isSafeInteger(intent.createdAt)
127
+ && Number.isSafeInteger(intent.updatedAt)
128
+ && (intent.state === 'awaiting-preflight-recovery'
129
+ ? intent.preflightArtifactSha256 === null && intent.preflightSemanticDigest === null
130
+ && intent.activeGenerationSha256 === null && intent.activeGenerationSemanticDigest === null
131
+ && intent.pendingGenerationSha256 === null && intent.pendingGenerationSemanticDigest === null
132
+ && intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
133
+ : true)
134
+ && (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started' || intent.state === 'preflight-passed' || intent.state === 'full-finalizer-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
135
+ ? validDigest(intent.candidateSha256) && validDigest(intent.preflightArtifactSha256) && validDigest(intent.preflightSemanticDigest)
136
+ && validDigest(intent.activeGenerationSha256) && validDigest(intent.activeGenerationSemanticDigest)
137
+ : true)
138
+ && (intent.state === 'full-finalizer-ready' || intent.state === 'commit-started'
139
+ ? validDigest(intent.pendingGenerationSha256) && validDigest(intent.pendingGenerationSemanticDigest)
140
+ && validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
141
+ : true)
142
+ && (intent.state === 'passed' || intent.state === 'consumption-started' || intent.state === 'consumed'
143
+ ? validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
144
+ : true)
145
+ && (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
73
146
  }
74
147
 
75
- function readArtifactRepairIntent(
148
+ export function readArtifactRecoveryIntent(
76
149
  repoRoot: string,
77
150
  taskId: string,
78
151
  family: ArtifactSchemaFamily,
79
152
  artifact: string
80
- ): ArtifactRepairIntent | null {
153
+ ): ArtifactRecoveryIntent | null {
154
+ if (!validTaskId(taskId) || !validArtifact(artifact, family, parseArtifactName(artifact)?.round)) {
155
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal identity is invalid');
156
+ }
81
157
  const target = intentPath(repoRoot, taskId, family, artifact);
82
158
  if (!fs.existsSync(target)) return null;
83
159
  let value: unknown;
84
160
  try { value = JSON.parse(fs.readFileSync(target, 'utf8')) as unknown; }
85
- catch (error) { throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`); }
86
- if (!isArtifactRepairIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
87
- throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
161
+ catch (error) { throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`); }
162
+ if (!isArtifactRecoveryIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
163
+ throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
88
164
  }
89
165
  return value;
90
166
  }
91
167
 
92
- export function writeArtifactRepairIntent(
168
+ export function writeArtifactRecoveryIntent(
93
169
  repoRoot: string,
94
- value: ArtifactRepairIntent,
95
- options: Readonly<{ expected?: ArtifactRepairIntent | null }> = {}
170
+ value: unknown,
171
+ options: Readonly<{ expected?: ArtifactRecoveryIntent | null }> = {}
96
172
  ): void {
97
- if (!isArtifactRepairIntent(value)) throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
98
- const directory = intentRoot(repoRoot);
173
+ if (!isArtifactRecoveryIntent(value)) throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
174
+ const intent = value;
175
+ const directory = artifactRecoveryIntentRoot(repoRoot);
99
176
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
100
- const target = intentPath(repoRoot, value.taskId, value.family, value.artifact);
177
+ const target = intentPath(repoRoot, intent.taskId, intent.family, intent.artifact);
101
178
  const lock = `${target}.lock`;
102
179
  let descriptor: number;
103
180
  try { descriptor = fs.openSync(lock, 'wx', 0o600); }
104
181
  catch (error) {
105
182
  if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
106
- throw new Error('ARTIFACT_REPAIR_INTENT_CONFLICT: another provenance writer owns the intent');
183
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: another journal writer owns the intent');
107
184
  }
108
185
  throw error;
109
186
  }
110
187
  try {
111
188
  const current = fs.existsSync(target)
112
- ? readArtifactRepairIntent(repoRoot, value.taskId, value.family, value.artifact)
189
+ ? readArtifactRecoveryIntent(repoRoot, intent.taskId, intent.family, intent.artifact)
113
190
  : null;
114
191
  if (!isDeepStrictEqual(current, options.expected ?? null)) {
115
- throw new Error('ARTIFACT_REPAIR_INTENT_CONFLICT: repair provenance changed during finalization');
192
+ throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: recovery journal changed during finalization');
116
193
  }
117
194
  const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
118
- fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
195
+ fs.writeFileSync(temporary, `${JSON.stringify(intent)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
119
196
  try { fs.renameSync(temporary, target); }
120
197
  catch (error) {
121
- try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
198
+ try { fs.unlinkSync(temporary); } catch { /* preserve the primary error */ }
122
199
  throw error;
123
200
  }
124
201
  } finally {
@@ -127,14 +204,12 @@ export function writeArtifactRepairIntent(
127
204
  }
128
205
  }
129
206
 
130
- export function findArtifactRepairIntentsByOperation(
207
+ export function findArtifactRecoveryIntentsByOperation(
131
208
  repoRoot: string,
132
209
  operationId: string
133
- ): readonly ArtifactRepairIntent[] {
134
- if (!operationId || /[\r\n]/u.test(operationId)) {
135
- throw new Error('ARTIFACT_REPAIR_OPERATION_INVALID: recovery operation id is invalid');
136
- }
137
- const directory = intentRoot(repoRoot);
210
+ ): readonly ArtifactRecoveryIntent[] {
211
+ if (!validToken(operationId)) throw new Error('ARTIFACT_RECOVERY_OPERATION_INVALID: recovery operation id is invalid');
212
+ const directory = artifactRecoveryIntentRoot(repoRoot);
138
213
  if (!fs.existsSync(directory)) return Object.freeze([]);
139
214
  return Object.freeze(fs.readdirSync(directory)
140
215
  .filter((name) => name.endsWith('.json'))
@@ -142,11 +217,11 @@ export function findArtifactRepairIntentsByOperation(
142
217
  const target = path.join(directory, name);
143
218
  let value: unknown;
144
219
  try { value = JSON.parse(fs.readFileSync(target, 'utf8')) as unknown; }
145
- catch (error) { throw new Error(`ARTIFACT_REPAIR_INTENT_INVALID: ${String(error)}`); }
146
- if (!isArtifactRepairIntent(value)) throw new Error('ARTIFACT_REPAIR_INTENT_INVALID: repair provenance schema is invalid');
220
+ catch (error) { throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`); }
221
+ if (!isArtifactRecoveryIntent(value)) throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
147
222
  return value;
148
223
  })
149
224
  .filter((value) => value.recoveryOperationId === operationId));
150
225
  }
151
226
 
152
- export { readArtifactRepairIntent };
227
+ export { intentPath as artifactRecoveryIntentPath };
@@ -32,6 +32,8 @@ type CommitOperationInput = Readonly<{
32
32
  agent?: string;
33
33
  mode?: CommitExecutionMode;
34
34
  round?: number;
35
+ /** Permit one retry to rebase a prepared intent's expected tree while HEAD and intent identity remain unchanged. */
36
+ recoverPreparedIntent?: boolean;
35
37
  delivery?: Readonly<{
36
38
  mode: 'local' | 'push';
37
39
  remote: string;
@@ -280,6 +282,20 @@ function checkpointTimestamp(): string {
280
282
  return new Date().toISOString();
281
283
  }
282
284
 
285
+ function samePreparedCheckpointIdentity(
286
+ pending: NonNullable<ReturnType<typeof readCheckpointIntent>>,
287
+ identity: Parameters<typeof checkpointIntentDigest>[0]
288
+ ): boolean {
289
+ return pending.state === 'prepared'
290
+ && pending.taskId === identity.taskId
291
+ && pending.branch === identity.branch
292
+ && pending.mode === identity.mode
293
+ && pending.expectedHead === identity.expectedHead
294
+ && pending.message === identity.message
295
+ && pending.round === identity.round
296
+ && JSON.stringify(pending.paths) === JSON.stringify(identity.paths);
297
+ }
298
+
283
299
  function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
284
300
  const inspected = inspectGitWorkflow(input.cwd);
285
301
  if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
@@ -313,10 +329,25 @@ function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mo
313
329
  error: { code: 'COMMIT_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) }
314
330
  };
315
331
  }
316
- if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) return {
317
- status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
318
- error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
319
- };
332
+ if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) {
333
+ if (!input.recoverPreparedIntent || !samePreparedCheckpointIdentity(pendingIntent, identity)) return {
334
+ status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
335
+ error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
336
+ };
337
+ pendingIntent = {
338
+ ...pendingIntent,
339
+ expectedTree: identity.expectedTree,
340
+ digest: checkpointIntentDigest(identity),
341
+ updatedAt: checkpointTimestamp()
342
+ };
343
+ try { writeCheckpointIntent(task!.repoRoot, pendingIntent); }
344
+ catch (error) {
345
+ return {
346
+ status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
347
+ error: { code: 'COMMIT_INTENT_WRITE_FAILED', message: error instanceof Error ? error.message : String(error) }
348
+ };
349
+ }
350
+ }
320
351
  if (!pendingIntent) {
321
352
  const timestamp = checkpointTimestamp();
322
353
  pendingIntent = {
@@ -39,15 +39,21 @@ import {
39
39
  type TaskLifecycleRequest,
40
40
  type TaskLifecycleResult
41
41
  } from './lifecycle.ts';
42
+ import {
43
+ recoverStartedLifecycleUnderLock,
44
+ recoveryFailure,
45
+ type LifecycleRecoveryResult
46
+ } from './lifecycle-recovery.ts';
47
+ import type { LifecycleRecoveryRequest } from './lifecycle-recovery.ts';
42
48
  import { locateActivityLog } from './activity-log.ts';
43
49
  import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
44
50
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
45
51
  import { verifyTaskEvent } from './verification.ts';
46
52
  import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
47
53
  import {
48
- findArtifactRepairIntentsByOperation,
49
- writeArtifactRepairIntent,
50
- type ArtifactRepairIntent
54
+ findArtifactRecoveryIntentsByOperation,
55
+ writeArtifactRecoveryIntent,
56
+ type ArtifactRecoveryIntent
51
57
  } from './artifact-repair-intent.ts';
52
58
  import {
53
59
  computeLifecycleBuildIdentity,
@@ -333,7 +339,7 @@ export function queryLifecycleRecoveryOperation(
333
339
  const store = options.capabilityStore ?? createCodexCapabilityStore();
334
340
  const registry = authorityOperationMap.get(store);
335
341
  const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
336
- const intents = options.repoRoot ? findArtifactRepairIntentsByOperation(options.repoRoot, operationId) : [];
342
+ const intents = options.repoRoot ? findArtifactRecoveryIntentsByOperation(options.repoRoot, operationId) : [];
337
343
  const capabilities = store.findByRecoveryOperation(operationId);
338
344
  const phaseMap = new Map<string, LifecycleRecoveryOperationQueryV1['phases'][number]>();
339
345
  for (const capability of capabilities) {
@@ -416,13 +422,13 @@ function lifecycleRecoveryEventCommitted(
416
422
  }
417
423
 
418
424
  function lifecycleRecoveryIntentMatches(
419
- intent: ArtifactRepairIntent,
425
+ intent: ArtifactRecoveryIntent,
420
426
  selector: LifecycleAuthorityRequestV1
421
427
  ): boolean {
422
428
  return intent.taskId === selector.taskId
423
429
  && intent.family === selector.family
424
430
  && intent.artifact === selector.artifact
425
- && (intent.state === 'commit-started' || intent.state === 'consumed');
431
+ && (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
426
432
  }
427
433
 
428
434
  /**
@@ -473,19 +479,19 @@ export function recoverLifecycleRecoveryOperation(
473
479
  if (!phase) {
474
480
  throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
475
481
  }
476
- const intents = findArtifactRepairIntentsByOperation(options.repoRoot, selector.operationId)
482
+ const intents = findArtifactRecoveryIntentsByOperation(options.repoRoot, selector.operationId)
477
483
  .filter((intent) => intent.taskId === selector.taskId
478
484
  && intent.family === selector.family
479
485
  && intent.artifact === selector.artifact);
480
486
  if (intents.length !== 1) {
481
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not identify this task event');
487
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not identify this task event');
482
488
  }
483
489
  const intent = intents[0]!;
484
490
  if (!lifecycleRecoveryIntentMatches(intent, selector)
485
491
  || intent.phase !== selector.phase
486
492
  || intent.requestId !== selector.lifecycleRequestId
487
493
  || intent.authorityDigest === null) {
488
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not match the recovery request');
494
+ throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not match the recovery request');
489
495
  }
490
496
  if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
491
497
  throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
@@ -499,7 +505,7 @@ export function recoverLifecycleRecoveryOperation(
499
505
  throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
500
506
  }
501
507
  if (intent.state !== 'consumed') {
502
- writeArtifactRepairIntent(options.repoRoot, {
508
+ writeArtifactRecoveryIntent(options.repoRoot, {
503
509
  ...intent,
504
510
  state: 'consumed',
505
511
  updatedAt: (options.now ?? Date.now)()
@@ -565,7 +571,7 @@ export type TaskControlOperation =
565
571
  options?: Omit<OrchestrationOptions, 'repoRoot'>;
566
572
  }>;
567
573
 
568
- export type TaskControlDispatchResult = TaskLifecycleResult | TaskFinalizationResult | OrchestrationResult;
574
+ export type TaskControlDispatchResult = TaskLifecycleResult | LifecycleRecoveryResult | TaskFinalizationResult | OrchestrationResult;
569
575
 
570
576
  export function taskControlOperationKey(operation: TaskControlOperation): string {
571
577
  if (operation.family === 'task-finalization') return 'complete';
@@ -712,12 +718,15 @@ function lifecycleTaskId(request: TaskLifecycleControlRequest, repoRoot: string)
712
718
  async function applyLifecycleWithAuthority(
713
719
  context: TaskControlExecutionContext,
714
720
  request: TaskLifecycleControlRequest
715
- ): Promise<TaskLifecycleResult & { humanOverride?: unknown }> {
721
+ ): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> {
716
722
  const conflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
717
723
  if (conflict) {
718
724
  return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
719
725
  }
720
- const execute = async (): Promise<TaskLifecycleResult & { humanOverride?: unknown }> => {
726
+ const execute = async (): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> => {
727
+ if (request.intent === 'recover-started') {
728
+ return recoverStartedLifecycleUnderLock(request as LifecycleRecoveryRequest, { repoRoot: context.repoRoot });
729
+ }
721
730
  const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
722
731
  if (lifecycleResult.status !== 'failed' || !request.overrideTicket) return lifecycleResult;
723
732
  if (!request.overrideTarget || !request.overrideScope) {
@@ -751,6 +760,9 @@ async function applyLifecycleWithAuthority(
751
760
  );
752
761
  } catch (error) {
753
762
  if (!(error instanceof TaskExecutionLockError)) throw error;
763
+ if (request.intent === 'recover-started') {
764
+ return recoveryFailure(request as LifecycleRecoveryRequest, 'owner-unknown', error.code, error.message);
765
+ }
754
766
  return taskLifecycleFailure(request, { code: error.code, message: error.message }, taskId);
755
767
  }
756
768
  }
@@ -829,7 +841,7 @@ function orchestrationFailure(error: unknown): OrchestrationResult {
829
841
  export function dispatchTaskControlOperation(
830
842
  context: TaskControlExecutionContext,
831
843
  operation: Extract<TaskControlOperation, { family: 'task-lifecycle' }>
832
- ): Promise<TaskLifecycleResult & { humanOverride?: unknown }>;
844
+ ): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }>;
833
845
  export function dispatchTaskControlOperation(
834
846
  context: TaskControlExecutionContext,
835
847
  operation: Extract<TaskControlOperation, { family: 'task-finalization' }>
@@ -892,7 +904,7 @@ function operationInvalid(message: string): never {
892
904
 
893
905
  const LIFECYCLE_FLAGS = new Set([
894
906
  '--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
895
- '--override-ticket', '--override-target', '--override-scope', '--dry-run'
907
+ '--stage', '--round', '--artifact', '--override-ticket', '--override-target', '--override-scope', '--dry-run'
896
908
  ]);
897
909
 
898
910
  const FINALIZATION_FLAGS = new Set(['--agent']);
@@ -972,8 +984,29 @@ export function parseTaskControlOperation(
972
984
  ...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
973
985
  ...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
974
986
  ...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
987
+ ...(value(values, '--stage') ? { stage: value(values, '--stage') } : {}),
988
+ ...(value(values, '--round') ? { round: Number(value(values, '--round')) } : {}),
989
+ ...(value(values, '--artifact') ? { artifact: value(values, '--artifact') } : {}),
975
990
  ...(values['--dry-run'] === true ? { dryRun: true } : {})
976
991
  };
992
+ if (intent === 'recover-started') {
993
+ const recoveryFlags = new Set(['--agent', '--stage', '--round', '--artifact', '--reason']);
994
+ for (const flag of Object.keys(values)) {
995
+ if (!recoveryFlags.has(flag)) operationInvalid(`option '${flag}' is not supported for recover-started`);
996
+ }
997
+ required(values, ['--stage', '--round', '--artifact', '--reason']);
998
+ const stage = value(values, '--stage');
999
+ if (!['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(stage ?? '')) {
1000
+ operationInvalid('--stage must be a supported lifecycle stage');
1001
+ }
1002
+ const round = Number(value(values, '--round'));
1003
+ if (!Number.isSafeInteger(round) || round < 1) operationInvalid('--round must be a positive integer');
1004
+ if (value(values, '--reason')!.includes('\n') || value(values, '--reason')!.includes('\r')) {
1005
+ operationInvalid('--reason must be a single line');
1006
+ }
1007
+ } else if (values['--stage'] !== undefined || values['--round'] !== undefined || values['--artifact'] !== undefined) {
1008
+ operationInvalid('--stage, --round, and --artifact are only supported for recover-started');
1009
+ }
977
1010
  return { family, request: input as unknown as TaskLifecycleControlRequest };
978
1011
  }
979
1012
 
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { isCompletionEvidence } from './orchestration.ts';
3
3
  import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.ts';
4
+ import { sameRecoveryWarning } from './recovery-warning.ts';
4
5
 
5
6
  export type ControlRecoveryOutcome = 'not-executed' | 'in-progress' | 'success' | 'failure' | 'unknown' | 'rejected';
6
7
 
@@ -53,7 +54,7 @@ export type ControlRecoveryDecision = Readonly<{
53
54
  reasonCode: string;
54
55
  }>;
55
56
 
56
- const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'] as const;
57
+ const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'] as const;
57
58
  const ORCHESTRATION_INTENTS = [
58
59
  'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
59
60
  'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
@@ -107,6 +108,15 @@ function domainEvidenceMatches(
107
108
  && snapshot.worktreeTree === completion.worktreeTree
108
109
  && lastReviewedCommit === completion.lastReviewedCommit;
109
110
  }
111
+ if (operation.family === 'task-lifecycle' && operation.intent === 'recover-started') {
112
+ const retryRequired = domain.recoveryState === 'retry-required'
113
+ && sameRecoveryWarning(domain.warning, result.warning);
114
+ return result.targetState === 'active'
115
+ && ['applied', 'no-op'].includes(String(result.status))
116
+ && domain.recovery === true
117
+ && domain.targetState === 'active'
118
+ && (domain.recoveryState === 'released' || retryRequired);
119
+ }
110
120
  if (operation.class === 'read-only') return result.changed === false && domain.snapshotValid === true;
111
121
  return true;
112
122
  }
@@ -159,6 +169,8 @@ export function classifySandboxControlRecovery(input: ControlRecoveryInput): Con
159
169
  responseReconstructable: true,
160
170
  reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
161
171
  ? 'RECOVERY_ROUTE_COMPLETION_NOOP'
172
+ : input.operation.intent === 'recover-started' && input.domain?.recoveryState === 'retry-required'
173
+ ? 'RECOVERY_RELEASE_RETRY_REQUIRED'
162
174
  : 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
163
175
  };
164
176
  }
@@ -1,6 +1,3 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
1
  import { syncPlatformComment } from '../platform/issue-comments.ts';
5
2
  import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
6
3
  import type { PlatformResult } from '../platform/types.ts';
@@ -190,12 +187,6 @@ function platformOperations(prefix: string, operations: readonly { name: string;
190
187
  }));
191
188
  }
192
189
 
193
- function issueFromTask(repoRoot: string, taskId: string): { number: number; url: string } | null {
194
- const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
195
- const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
196
- return match ? { number: Number(match[1]), url: '' } : null;
197
- }
198
-
199
190
  async function createTask(value: unknown, options: CreateTaskOptions): Promise<TaskCreateResult> {
200
191
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
201
192
  let candidate: TaskCreateCandidateV1;
@@ -215,7 +206,7 @@ async function createTask(value: unknown, options: CreateTaskOptions): Promise<T
215
206
  const operations: TaskCreateOperation[] = [{
216
207
  name: 'task:local', status: local.status, reasonCode: null
217
208
  }];
218
- let issue = issueFromTask(options.repoRoot, local.task.id);
209
+ let issue: TaskCreateResult['issue'] = null;
219
210
  const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
220
211
  operations.push(...platformOperations('platform-create', created.operations));
221
212
  if (created.issue) issue = { number: created.issue.number, url: created.issue.url };