@fitlab-ai/agent-infra 0.9.15 → 0.10.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 (184) hide show
  1. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  3. package/dist/lib/fs/durable-write.js +2 -0
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/manual-validation.js +17 -8
  6. package/dist/lib/internal/task-artifact.js +1 -1
  7. package/dist/lib/internal/task-lifecycle.js +2 -2
  8. package/dist/lib/internal/task-operation-registry.js +0 -1
  9. package/dist/lib/internal/task-review.js +4 -4
  10. package/dist/lib/platform/issue-comments.js +16 -10
  11. package/dist/lib/platform/issues.js +1 -1
  12. package/dist/lib/platform/pr-change-report.js +5 -4
  13. package/dist/lib/platform/pr-summary.js +64 -32
  14. package/dist/lib/platform/pull-requests.js +3 -3
  15. package/dist/lib/platform/task-identities.js +2 -9
  16. package/dist/lib/platform/verification-sync.js +6 -2
  17. package/dist/lib/pr-review/evidence-grading.js +4 -8
  18. package/dist/lib/sandbox/commands/rm.js +21 -2
  19. package/dist/lib/sandbox/control/server.js +29 -9
  20. package/dist/lib/sandbox/control/state.js +7 -1
  21. package/dist/lib/sandbox/control/workflow-executor.js +4 -14
  22. package/dist/lib/sandbox/index.js +1 -2
  23. package/dist/lib/sandbox/removal.js +116 -1011
  24. package/dist/lib/sandbox/workspace-identity.js +17 -1
  25. package/dist/lib/task/artifact-command.js +6 -28
  26. package/dist/lib/task/artifact-operations.js +16 -230
  27. package/dist/lib/task/artifact-recovery.js +430 -0
  28. package/dist/lib/task/artifact-repair-intent.js +73 -39
  29. package/dist/lib/task/commit-operation.js +31 -4
  30. package/dist/lib/task/control-authority.js +38 -7
  31. package/dist/lib/task/control-recovery.js +14 -2
  32. package/dist/lib/task/create-service.js +1 -8
  33. package/dist/lib/task/delegation-receipts.js +64 -3
  34. package/dist/lib/task/events.js +164 -22
  35. package/dist/lib/task/human-override.js +5 -8
  36. package/dist/lib/task/intermediate-cleanup.js +66 -8
  37. package/dist/lib/task/lifecycle-recovery.js +549 -0
  38. package/dist/lib/task/lifecycle.js +12 -3
  39. package/dist/lib/task/local-artifact-finalization.js +148 -123
  40. package/dist/lib/task/manual-validation-receipt.js +5 -1
  41. package/dist/lib/task/orchestration.js +52 -2
  42. package/dist/lib/task/pr-delivery-fact.js +12 -56
  43. package/dist/lib/task/recovery-warning.js +19 -0
  44. package/dist/lib/task/review-command.js +1 -0
  45. package/dist/lib/task/review-finalization.js +104 -77
  46. package/dist/lib/task/short-id.js +15 -7
  47. package/dist/lib/task/task-execution-lock.js +1 -1
  48. package/dist/lib/task/verification-engine.js +3 -14
  49. package/dist/lib/task/workflow-command.js +0 -1
  50. package/dist/lib/version.js +1 -11
  51. package/dist/lifecycle-build-manifest.json +3 -3
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  53. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  54. package/lib/fs/durable-write.ts +1 -0
  55. package/lib/internal/cli-route-inventory.ts +1 -1
  56. package/lib/internal/manual-validation.ts +17 -8
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +4 -3
  59. package/lib/internal/task-operation-registry.ts +0 -1
  60. package/lib/internal/task-review.ts +4 -4
  61. package/lib/platform/issue-comments.ts +18 -10
  62. package/lib/platform/issues.ts +1 -1
  63. package/lib/platform/pr-change-report.ts +7 -5
  64. package/lib/platform/pr-summary.ts +64 -32
  65. package/lib/platform/pull-requests.ts +3 -3
  66. package/lib/platform/task-identities.ts +3 -14
  67. package/lib/platform/verification-sync.ts +6 -2
  68. package/lib/pr-review/evidence-grading.ts +4 -9
  69. package/lib/sandbox/commands/rm.ts +34 -2
  70. package/lib/sandbox/control/protocol.ts +5 -0
  71. package/lib/sandbox/control/server.ts +30 -10
  72. package/lib/sandbox/control/state.ts +9 -1
  73. package/lib/sandbox/control/workflow-executor.ts +4 -14
  74. package/lib/sandbox/index.ts +1 -2
  75. package/lib/sandbox/removal.ts +126 -1128
  76. package/lib/sandbox/workspace-identity.ts +19 -3
  77. package/lib/task/artifact-command.ts +8 -27
  78. package/lib/task/artifact-operations.ts +21 -251
  79. package/lib/task/artifact-recovery.ts +519 -0
  80. package/lib/task/artifact-repair-intent.ts +99 -51
  81. package/lib/task/commit-operation.ts +35 -4
  82. package/lib/task/control-authority.ts +47 -14
  83. package/lib/task/control-recovery.ts +13 -1
  84. package/lib/task/create-service.ts +1 -10
  85. package/lib/task/delegation-receipts.ts +73 -2
  86. package/lib/task/events.ts +187 -23
  87. package/lib/task/human-override.ts +5 -8
  88. package/lib/task/intermediate-cleanup.ts +50 -12
  89. package/lib/task/lifecycle-recovery.ts +691 -0
  90. package/lib/task/lifecycle.ts +13 -4
  91. package/lib/task/local-artifact-finalization.ts +176 -132
  92. package/lib/task/manual-validation-receipt.ts +5 -1
  93. package/lib/task/orchestration.ts +65 -0
  94. package/lib/task/pr-delivery-fact.ts +15 -62
  95. package/lib/task/recovery-warning.ts +21 -0
  96. package/lib/task/review-command.ts +1 -0
  97. package/lib/task/review-finalization.ts +106 -87
  98. package/lib/task/short-id.ts +27 -5
  99. package/lib/task/task-execution-lock.ts +1 -1
  100. package/lib/task/verification-engine.ts +3 -17
  101. package/lib/task/verification-types.ts +0 -2
  102. package/lib/task/workflow-command.ts +0 -1
  103. package/lib/version.ts +1 -15
  104. package/package.json +1 -1
  105. package/templates/.agents/rules/create-issue.en.md +1 -1
  106. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  107. package/templates/.agents/rules/issue-sync.en.md +4 -2
  108. package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
  109. package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
  110. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  113. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  114. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  115. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  116. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  118. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  119. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  120. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  121. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  122. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
  123. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  124. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  125. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  126. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  127. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  131. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  132. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  133. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  134. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  135. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  136. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  137. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  138. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  139. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  140. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  141. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  142. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  143. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  144. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  145. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  146. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  147. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  148. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  149. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  150. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  151. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  152. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  153. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  156. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  157. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  158. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  159. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  160. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  161. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  162. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  163. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  164. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  165. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  166. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  167. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  168. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  169. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  170. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  171. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  172. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  173. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  174. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  175. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  176. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  177. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  178. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  179. package/templates/.agents/templates/task.en.md +1 -1
  180. package/templates/.agents/templates/task.zh-CN.md +1 -1
  181. package/templates/.claude/commands/import-issue.en.md +1 -1
  182. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  183. package/templates/.claude/commands/restore-task.en.md +1 -1
  184. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
@@ -15,9 +15,11 @@ import { captureTaskWriteMetadata, writeTask } from './write.ts';
15
15
  import type { TaskFileSystem, TaskMutation, TaskOperationSummary, TaskWriteMetadata } from './write.ts';
16
16
  import { validateCurrentTaskContract } from './current-contract.ts';
17
17
  import { isValidAgentInfraVersion } from '../version.ts';
18
+ import { taskIssueIdentity } from '../platform/task-identities.ts';
19
+ import { resourceIdentityEquals } from '../platform/resource-identity.ts';
18
20
 
19
21
  const lifecycleIntentCatalog = [
20
- 'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
22
+ 'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'
21
23
  ] as const;
22
24
  const lifecycleFailureCatalog = [
23
25
  'LIFECYCLE_DIRECTORY_RENAME_FAILED',
@@ -55,7 +57,8 @@ type TaskLifecycleRequest =
55
57
  | { taskRef: string; intent: 'complete'; agent: string; dryRun?: boolean }
56
58
  | { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
57
59
  | { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
58
- | { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean };
60
+ | { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
61
+ | { taskRef: string; intent: 'recover-started'; agent: string; stage: 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code'; round: number; artifact: string; reason: string; dryRun?: boolean };
59
62
 
60
63
  type HotState = 'active' | 'blocked' | 'completed';
61
64
  type SourceState = HotState | 'staging';
@@ -206,6 +209,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
206
209
  if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
207
210
  if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
208
211
  if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
212
+ if (request.intent === 'recover-started') return { action: 'Recover Started', note: request.reason };
209
213
  return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
210
214
  }
211
215
 
@@ -249,8 +253,10 @@ function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent:
249
253
  if (!fs.existsSync(taskFile)) return { code: 'LIFECYCLE_STAGING_INVALID', message: 'restore staging has no task.md' };
250
254
  try {
251
255
  const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(taskFile, 'utf8'));
252
- if (frontmatter.id !== taskId || Number(frontmatter.issue_number) !== request.issueNumber || frontmatter.current_step === 'completed') {
253
- return { code: 'LIFECYCLE_STAGING_IDENTITY_INVALID', message: 'restore task identity, issue, or current_step is invalid' };
256
+ const issueIdentity = taskIssueIdentity(frontmatter);
257
+ const requestedIssueIdentity = { kind: 'number' as const, value: request.issueNumber };
258
+ if (frontmatter.id !== taskId || !issueIdentity || !resourceIdentityEquals(issueIdentity, requestedIssueIdentity) || frontmatter.current_step === 'completed') {
259
+ return { code: 'LIFECYCLE_STAGING_IDENTITY_INVALID', message: 'restore task identity, platform issue identity, or current_step is invalid' };
254
260
  }
255
261
  } catch (error) {
256
262
  return { code: 'LIFECYCLE_STAGING_INVALID', message: error instanceof Error ? error.message : String(error) };
@@ -410,6 +416,9 @@ function removeCopiedSource(source: string, target: string): void {
410
416
  }
411
417
 
412
418
  function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
419
+ if (requestInput.intent === 'recover-started') {
420
+ return failed(requestInput, { code: 'LIFECYCLE_RECOVERY_REQUIRES_AUTHORITY', message: 'recover-started must be dispatched through task control authority' });
421
+ }
413
422
  const normalized = normalizedRequest(requestInput);
414
423
  if ('code' in normalized) return failed(requestInput, normalized);
415
424
  const request = normalized;
@@ -5,6 +5,7 @@ import { scanVisibleMarkdown } from './markdown.ts';
5
5
  import { parseArtifactName } from './artifact-name.ts';
6
6
  import {
7
7
  hasOpenArtifactRound,
8
+ parseCodePlanInputReference,
8
9
  validateArtifactPublication,
9
10
  validateCompletedArtifact
10
11
  } from './artifact-lifecycle.ts';
@@ -12,8 +13,18 @@ import { resolveTaskRef } from './resolve-ref.ts';
12
13
  import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
13
14
  import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
14
15
  import { getArtifactSchema } from './artifact-schema.ts';
15
- import { readArtifactRepairIntent, writeArtifactRepairIntent } from './artifact-repair-intent.ts';
16
- import type { ArtifactRepairIntent } from './artifact-repair-intent.ts';
16
+ import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
17
+ import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
18
+ import {
19
+ beginArtifactRecovery,
20
+ commitArtifactRecovery,
21
+ consumeArtifactRecovery,
22
+ prepareArtifactRecoveryCommit,
23
+ recordArtifactRecoveryPassed,
24
+ recoveryContextFromIntent,
25
+ stageArtifactCandidate
26
+ } from './artifact-recovery.ts';
27
+ import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
17
28
  import {
18
29
  consumeLifecycleRecoveryAttestation,
19
30
  lifecycleRecoveryAttestationDigest,
@@ -38,18 +49,14 @@ type LocalArtifactDiagnosticCode =
38
49
  | 'LOCAL_REQUIRED_PATTERN_MISSING'
39
50
  | 'LOCAL_STRUCTURAL_INVALID'
40
51
  | 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
41
- | 'LOCAL_REPAIR_PROVENANCE_CONFLICT'
42
- | 'LOCAL_REPAIR_BASELINE_MISMATCH'
52
+ | 'LOCAL_RECOVERY_PROVENANCE_CONFLICT'
53
+ | 'LOCAL_RECOVERY_BASELINE_MISMATCH'
43
54
  | 'LOCAL_QUALIFICATION_AUDIT_INVALID';
44
55
 
45
56
  type LocalArtifactDiagnostic = {
46
57
  code: LocalArtifactDiagnosticCode;
47
58
  message: string;
48
- repairable: boolean;
49
59
  line: number | null;
50
- from?: string;
51
- to?: string;
52
- operation?: 'replace-line';
53
60
  };
54
61
 
55
62
  type LocalArtifactValidationOptions = {
@@ -63,7 +70,6 @@ type LocalArtifactValidationResult = {
63
70
  ok: boolean;
64
71
  family: LocalArtifactFamily;
65
72
  semanticDigest: string;
66
- repairable: boolean;
67
73
  diagnostics: readonly LocalArtifactDiagnostic[];
68
74
  };
69
75
 
@@ -73,23 +79,32 @@ type LocalArtifactFinalizationRequest = {
73
79
  artifact: string;
74
80
  repoRoot?: string;
75
81
  requiredSections?: readonly string[];
82
+ recoveryId?: string;
83
+ lockAlreadyHeld?: boolean;
76
84
  };
77
85
 
86
+ type LocalArtifactRecoveryInfo = Readonly<{
87
+ recoveryId: string;
88
+ candidatePath: string;
89
+ baselineSha256: string;
90
+ baselineSemanticDigest: string;
91
+ }>;
92
+
78
93
  type LocalArtifactFinalizationResult = {
79
94
  status: 'passed' | 'failed';
80
- changed: false;
95
+ changed: boolean;
81
96
  taskId: string | null;
82
97
  taskDir: string | null;
83
98
  family: LocalArtifactFamily;
84
99
  artifact: string;
85
100
  artifactSha256: string | null;
86
101
  semanticDigest: string | null;
87
- repairable: boolean;
88
102
  diagnostics: readonly LocalArtifactDiagnostic[];
103
+ recovery?: LocalArtifactRecoveryInfo;
89
104
  error: { code: string; message: string } | null;
90
105
  };
91
106
 
92
- type LocalArtifactFinalizationIntent = ArtifactRepairIntent;
107
+ type LocalArtifactFinalizationIntent = ArtifactRecoveryIntent;
93
108
 
94
109
  function authorityDigest(authority: LifecycleRecoveryAttestationV1): string {
95
110
  return lifecycleRecoveryAttestationDigest(authority);
@@ -113,18 +128,27 @@ function validFinalizerAuthority(
113
128
  && authority.operationId.length > 0;
114
129
  }
115
130
 
131
+ function recoveryInfo(context: ArtifactRecoveryContext): LocalArtifactRecoveryInfo {
132
+ return {
133
+ recoveryId: context.recoveryId,
134
+ candidatePath: context.stagingPath,
135
+ baselineSha256: context.baselineSha256,
136
+ baselineSemanticDigest: context.baselineSemanticDigest
137
+ };
138
+ }
139
+
116
140
  function consumeLocalArtifactFinalizationIntent(
117
141
  repoRoot: string,
118
- intent: LocalArtifactFinalizationIntent
142
+ intent: LocalArtifactFinalizationIntent,
143
+ options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
119
144
  ): LocalArtifactFinalizationIntent {
120
145
  if (intent.state === 'consumed') return intent;
121
- if (intent.state !== 'passed') throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: only passed provenance can be consumed');
122
- const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
123
- writeArtifactRepairIntent(repoRoot, consumed, { expected: intent });
124
- return consumed;
146
+ const resolved = resolveTaskRef(intent.taskId, { repoRoot });
147
+ if (!resolved.ok) throw new Error(resolved.message);
148
+ return consumeArtifactRecovery(recoveryContextFromIntent(repoRoot, resolved.taskDir, intent), options);
125
149
  }
126
150
 
127
- function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode | null {
151
+ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
128
152
  const map: Record<string, LocalArtifactDiagnosticCode> = {
129
153
  ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
130
154
  ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
@@ -134,20 +158,8 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode | null {
134
158
  return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
135
159
  }
136
160
 
137
- function localDiagnostic(
138
- item: { code: string; message: string; line: number | null; repairable: boolean; operation?: { kind: string; from: string; to: string } }
139
- ): LocalArtifactDiagnostic | null {
140
- const code = localDiagnosticCode(item.code);
141
- if (!code) return null;
142
- return {
143
- code,
144
- message: item.message,
145
- repairable: item.repairable,
146
- line: item.line,
147
- ...(item.operation?.kind === 'replace-line'
148
- ? { from: item.operation.from, to: item.operation.to, operation: 'replace-line' as const }
149
- : {})
150
- };
161
+ function localDiagnostic(item: { code: string; message: string; line: number | null }): LocalArtifactDiagnostic {
162
+ return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
151
163
  }
152
164
 
153
165
  function isStatusPattern(pattern: string): boolean {
@@ -159,14 +171,10 @@ function validateLocalArtifact(
159
171
  options: LocalArtifactValidationOptions
160
172
  ): LocalArtifactValidationResult {
161
173
  const schema = getArtifactSchema(options.family)!;
162
- const patterns = schema.requiredPatterns;
163
174
  const diagnostics: LocalArtifactDiagnostic[] = [];
164
175
  const scanned = scanVisibleMarkdown(content);
165
176
  const structure = inspectArtifactStructure(content, schema);
166
- for (const item of structure.diagnostics) {
167
- const mapped = localDiagnostic(item);
168
- if (mapped) diagnostics.push(mapped);
169
- }
177
+ for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
170
178
 
171
179
  const statusSection = schema.sections.find((section) => section.id === 'state-check');
172
180
  if (statusSection) {
@@ -178,9 +186,9 @@ function validateLocalArtifact(
178
186
  if (statusHeading) {
179
187
  const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
180
188
  const body = content.slice(statusHeading.end, next?.start ?? content.length);
181
- for (const pattern of patterns.filter(isStatusPattern)) {
189
+ for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
182
190
  if (!new RegExp(pattern, 'm').test(body)) {
183
- diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, repairable: false, line: content.slice(0, statusHeading.start).split('\n').length });
191
+ diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
184
192
  }
185
193
  }
186
194
  }
@@ -188,40 +196,28 @@ function validateLocalArtifact(
188
196
 
189
197
  const patternInspection = inspectArtifactPatterns(content, {
190
198
  ...schema,
191
- requiredPatterns: patterns.filter((item) => !isStatusPattern(item))
199
+ requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
192
200
  });
193
- for (const item of patternInspection.diagnostics) {
194
- diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, repairable: false, line: null });
195
- }
201
+ for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
196
202
 
197
203
  const decisionDetails = inspectDecisionDetailDuplicates(content);
198
- if (!decisionDetails.ok) {
199
- diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, repairable: false, line: null });
200
- }
204
+ if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
201
205
 
202
206
  if (options.taskContent !== undefined) {
203
207
  const expected = expectedQualificationRelations(options.taskContent, options.family);
204
- if (!expected.ok) {
205
- diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, repairable: false, line: null });
206
- }
208
+ if (!expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
207
209
  const qualification = validateQualificationAudit(options.taskContent, content, {
208
- family: options.family === 'analysis' ? 'analysis' : options.family === 'plan' ? 'plan' : 'code',
210
+ family: options.family,
209
211
  artifact: options.artifact,
210
212
  expectedUpstreamRelations: expected.ok ? expected.relations : undefined
211
213
  });
212
- if (!qualification.ok) {
213
- diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, repairable: false, line: null });
214
- }
214
+ if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
215
215
  }
216
216
 
217
- const repairable = structure.repair !== null && diagnostics.length === 1 && diagnostics[0]?.repairable === true;
218
- const semanticDigestValue = canonicalSemanticDigest(content, structure.repair);
219
-
220
217
  return {
221
218
  ok: diagnostics.length === 0,
222
219
  family: options.family,
223
- semanticDigest: semanticDigestValue,
224
- repairable,
220
+ semanticDigest: canonicalSemanticDigest(content),
225
221
  diagnostics
226
222
  };
227
223
  }
@@ -236,7 +232,7 @@ function failedFinalization(
236
232
  taskId: null, taskDir: null,
237
233
  family: request.family, artifact: request.artifact,
238
234
  artifactSha256: null, semanticDigest: null,
239
- repairable: false, diagnostics: [], error, ...extra
235
+ diagnostics: [], error, ...extra
240
236
  };
241
237
  }
242
238
 
@@ -244,114 +240,160 @@ type LocalArtifactPreparation = Readonly<{
244
240
  result: LocalArtifactFinalizationResult;
245
241
  content: string;
246
242
  repoRoot?: string;
247
- provenance?: ArtifactRepairIntent;
248
- expectedIntent?: ArtifactRepairIntent | null;
243
+ lockAlreadyHeld?: boolean;
244
+ recovery?: ArtifactRecoveryContext;
245
+ authority?: LifecycleRecoveryAttestationV1;
249
246
  }>;
250
247
 
251
- /** Read and validate once; callers publish this content before committing provenance. */
248
+ function tupleFor(
249
+ request: LocalArtifactFinalizationRequest,
250
+ taskId: string,
251
+ authority?: LifecycleRecoveryAttestationV1
252
+ ) {
253
+ const round = parseArtifactName(request.artifact)?.round ?? 0;
254
+ return {
255
+ taskId,
256
+ family: request.family,
257
+ artifact: request.artifact,
258
+ round,
259
+ requestId: authority?.lifecycleRequestId ?? `local-finalize:${taskId}:${request.family}:${round}`,
260
+ phase: authority?.phase ?? 'artifact.finalize-local' as const,
261
+ authorityDigest: authority ? authorityDigest(authority) : null
262
+ };
263
+ }
264
+
252
265
  function prepareLocalArtifact(
253
266
  request: LocalArtifactFinalizationRequest,
254
267
  candidate?: string,
255
268
  authority?: LifecycleRecoveryAttestationV1
256
269
  ): LocalArtifactPreparation {
257
- const failed = (code: string, message: string): LocalArtifactPreparation => ({
258
- result: failedFinalization(request, { code, message }), content: candidate ?? ''
270
+ const failed = (code: string, message: string, extra: Partial<LocalArtifactFinalizationResult> = {}): LocalArtifactPreparation => ({
271
+ result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
259
272
  });
260
273
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
261
274
  if (!resolved.ok) return failed(resolved.code, resolved.message);
262
275
  const parsed = parseArtifactName(request.artifact);
263
276
  if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
264
- let content: string;
277
+ if (authority && !validFinalizerAuthority(authority, request, resolved.taskId)) {
278
+ return failed('LOCAL_EXECUTION_AUTHORITY_INVALID', 'finalizer authority does not match the artifact tuple');
279
+ }
280
+
265
281
  let taskContent: string;
282
+ let content: string;
283
+ let recovery: ArtifactRecoveryContext | undefined;
266
284
  try {
267
285
  taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
268
- if (candidate !== undefined) {
269
- const error = validateArtifactPublication(resolved.taskDir, request.family, request.artifact);
270
- if (error) return failed(error.code, error.message);
271
- if (resolved.state !== 'active' || !hasOpenArtifactRound(taskContent, request.family, parsed.round)) {
272
- return failed('ARTIFACT_IDENTITY_INVALID', 'candidate must match one open started round in an active task');
273
- }
274
- content = candidate;
286
+ if (request.recoveryId) {
287
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
288
+ if (!intent || intent.recoveryOperationId !== request.recoveryId) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
289
+ if (!['awaiting-recovery', 'finalize-ready', 'commit-started'].includes(intent.state)) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
290
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
291
+ content = fs.existsSync(recovery.stagingPath)
292
+ ? fs.readFileSync(recovery.stagingPath, 'utf8')
293
+ : fs.readFileSync(recovery.formalPath, 'utf8');
275
294
  } else {
276
295
  const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
277
296
  if (!validated.ok) return failed(validated.error.code, validated.error.message);
278
- content = fs.readFileSync(validated.artifact.path, 'utf8');
297
+ content = candidate ?? fs.readFileSync(validated.artifact.path, 'utf8');
279
298
  }
280
299
  } catch (error) { return failed('ARTIFACT_NOT_READABLE', String(error)); }
300
+
301
+ if (resolved.state !== 'active' || !hasOpenArtifactRound(taskContent, request.family, parsed.round)) {
302
+ return failed('ARTIFACT_IDENTITY_INVALID', 'candidate must match one open started round in an active task');
303
+ }
281
304
  const validation = validateLocalArtifact(content, {
282
305
  family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
283
306
  });
284
307
  const artifactSha256 = sha256Content(content);
285
- const { semanticDigest, repairable, diagnostics } = validation;
286
308
  const result = failedFinalization(request, {
287
- code: 'LOCAL_ARTIFACT_INVALID', message: diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
288
- }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest, repairable, diagnostics });
289
- const preparedBase = { result, content, repoRoot: resolved.repoRoot };
290
- if (authority !== undefined && !validFinalizerAuthority(authority, request, resolved.taskId)) {
309
+ code: 'LOCAL_ARTIFACT_INVALID',
310
+ message: validation.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
311
+ }, {
312
+ taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
313
+ semanticDigest: validation.semanticDigest, diagnostics: validation.diagnostics
314
+ });
315
+
316
+ if (request.recoveryId) {
317
+ if (!validation.ok) return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
291
318
  return {
292
- ...preparedBase,
293
- result: { ...result, repairable: false, error: {
294
- code: 'LOCAL_EXECUTION_AUTHORITY_INVALID',
295
- message: 'finalizer authority does not match the artifact tuple'
296
- }, diagnostics: [] }
319
+ result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(recovery!) },
320
+ content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
297
321
  };
298
322
  }
299
- const reject = (code: LocalArtifactDiagnosticCode, message: string): LocalArtifactPreparation => ({
300
- ...prepared, result: { ...result, repairable: false, error: { code, message },
301
- diagnostics: [{ code, message, repairable: false, line: null }] }
302
- });
303
- let intent: ArtifactRepairIntent | null;
304
- try { intent = readArtifactRepairIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact); }
305
- catch (error) {
306
- return { ...preparedBase, result: { ...result, repairable: false, diagnostics: [],
307
- error: { code: 'LOCAL_FINALIZATION_INTENT_INVALID', message: String(error) } } };
308
- }
309
- const prepared = { ...preparedBase, expectedIntent: intent };
310
- if (intent && intent.recoveryOperationId !== null
311
- && (!authority || intent.recoveryOperationId !== authority.operationId)) {
312
- return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'local repair provenance is owned by another recovery operation');
313
- }
314
- if (repairable) {
315
- if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== semanticDigest)) {
316
- return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'a different local repair baseline is already recorded for this artifact');
317
- }
318
- if (intent && !authority) return prepared;
319
- } else {
320
- if (!validation.ok) return prepared;
321
- if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== semanticDigest) {
322
- return reject('LOCAL_REPAIR_BASELINE_MISMATCH', 'the repaired artifact semantic digest does not match the recorded repair baseline');
323
+
324
+ const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
325
+ if (existing?.state === 'passed' || existing?.state === 'consumed') {
326
+ if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
327
+ return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
323
328
  }
324
- if ((intent?.state === 'passed' || intent?.state === 'consumed')
325
- && (intent.artifactSha256 !== artifactSha256 || intent.semanticDigest !== semanticDigest)) {
326
- return reject('LOCAL_REPAIR_PROVENANCE_CONFLICT', 'the artifact changed after its finalization provenance was recorded');
329
+ const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
330
+ return {
331
+ result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
332
+ content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
333
+ };
334
+ }
335
+
336
+ try {
337
+ if (validation.ok) {
338
+ const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
339
+ repoRoot: resolved.repoRoot,
340
+ taskDir: resolved.taskDir,
341
+ expectedFinalSha256: artifactSha256,
342
+ expectedFinalSemanticDigest: validation.semanticDigest,
343
+ ...(authority ? { recoveryId: authority.operationId } : {}),
344
+ lockAlreadyHeld: request.lockAlreadyHeld
345
+ });
346
+ recovery = context;
347
+ return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
327
348
  }
328
- prepared.result = { ...result, status: 'passed', error: null };
329
- if (intent?.state === 'consumed') return prepared;
349
+ const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
350
+ repoRoot: resolved.repoRoot,
351
+ taskDir: resolved.taskDir,
352
+ ...(authority ? { recoveryId: authority.operationId } : {}),
353
+ lockAlreadyHeld: request.lockAlreadyHeld
354
+ });
355
+ recovery = context;
356
+ return {
357
+ result: { ...result, recovery: recoveryInfo(context) },
358
+ content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
359
+ };
360
+ } catch (error) {
361
+ return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
330
362
  }
331
- const timestamp = Date.now();
332
- return { ...prepared, provenance: {
333
- version: 2, taskId: resolved.taskId, family: request.family, artifact: request.artifact,
334
- state: repairable ? 'awaiting-repair' : 'passed',
335
- baselineSemanticDigest: repairable ? semanticDigest : intent?.baselineSemanticDigest ?? null,
336
- artifactSha256, semanticDigest,
337
- recoveryOperationId: authority?.operationId ?? intent?.recoveryOperationId ?? null,
338
- phase: authority?.phase ?? intent?.phase ?? null,
339
- authorityDigest: authority ? authorityDigest(authority) : intent?.authorityDigest ?? null,
340
- requestId: authority?.lifecycleRequestId ?? intent?.requestId ?? `local-finalize:${resolved.taskId}`,
341
- createdAt: intent?.createdAt ?? timestamp,
342
- updatedAt: timestamp
343
- } };
344
363
  }
345
364
 
346
365
  function commitLocalArtifactProvenance(prepared: LocalArtifactPreparation): LocalArtifactFinalizationResult {
366
+ if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery) return prepared.result;
367
+ if (prepared.result.status === 'failed') return prepared.result;
347
368
  try {
348
- if (prepared.provenance) {
349
- writeArtifactRepairIntent(prepared.repoRoot!, prepared.provenance, { expected: prepared.expectedIntent ?? null });
369
+ const intent = readArtifactRecoveryIntent(prepared.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
370
+ if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
371
+ let committed;
372
+ if (intent.state === 'passed' || intent.state === 'consumed') {
373
+ return {
374
+ ...prepared.result,
375
+ artifactSha256: intent.finalArtifactSha256,
376
+ semanticDigest: intent.finalSemanticDigest,
377
+ error: null
378
+ };
379
+ } else if (intent.state === 'finalize-ready' || intent.state === 'commit-started') {
380
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
381
+ } else {
382
+ const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
383
+ prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
384
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
350
385
  }
351
- return prepared.result;
386
+ if (committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
387
+ return {
388
+ ...prepared.result,
389
+ status: 'passed',
390
+ artifactSha256: committed.finalArtifactSha256,
391
+ semanticDigest: committed.finalSemanticDigest,
392
+ error: null
393
+ };
352
394
  } catch (error) {
353
395
  return { ...prepared.result, status: 'failed', error: {
354
- code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED', message: String(error)
396
+ code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
355
397
  } };
356
398
  }
357
399
  }
@@ -361,7 +403,8 @@ function finalizeLocalArtifact(
361
403
  authority?: LifecycleRecoveryAttestationV1,
362
404
  options: Readonly<{ deferLifecycleRecoveryConsumption?: boolean }> = {}
363
405
  ): LocalArtifactFinalizationResult {
364
- const result = commitLocalArtifactProvenance(prepareLocalArtifact(request, undefined, authority));
406
+ const prepared = prepareLocalArtifact(request, undefined, authority);
407
+ const result = commitLocalArtifactProvenance(prepared);
365
408
  if (authority && !options.deferLifecycleRecoveryConsumption && result.status === 'passed') {
366
409
  try { consumeLifecycleRecoveryAttestation(authority); }
367
410
  catch (error) {
@@ -380,7 +423,7 @@ export {
380
423
  finalizeLocalArtifact,
381
424
  prepareLocalArtifact,
382
425
  commitLocalArtifactProvenance,
383
- readArtifactRepairIntent as readLocalArtifactFinalizationIntent,
426
+ readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent,
384
427
  canonicalSemanticDigest as semanticDigest,
385
428
  sha256Content,
386
429
  validateLocalArtifact
@@ -394,5 +437,6 @@ export type {
394
437
  LocalArtifactFinalizationIntent,
395
438
  LocalArtifactPreparation,
396
439
  LocalArtifactValidationOptions,
397
- LocalArtifactValidationResult
440
+ LocalArtifactValidationResult,
441
+ LocalArtifactRecoveryInfo
398
442
  };
@@ -68,7 +68,11 @@ function manualValidationReceiptDigest(receipt: ManualValidationReceipt | Manual
68
68
 
69
69
  function manualValidationFinalSummaryDigest(body: string): string {
70
70
  const heading = /^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/mu.exec(body);
71
- const projection = heading ? body.slice(heading.index).replace(/\s+$/u, '') : body;
71
+ const afterHeading = heading ? body.slice(heading.index + heading[0].length) : '';
72
+ const nextSection = heading ? /^#{1,3}\s/mu.exec(afterHeading) : null;
73
+ const projection = heading
74
+ ? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
75
+ : body;
72
76
  const preimage = projection.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
73
77
  return createHash('sha256').update(preimage, 'utf8').digest('hex');
74
78
  }
@@ -11,6 +11,7 @@ import type { LedgerDocument } from './ledger.ts';
11
11
  import { resolveTaskRef } from './resolve-ref.ts';
12
12
  import {
13
13
  activateDelegation,
14
+ abortActivatedDelegation,
14
15
  abortPreparedDelegation,
15
16
  completeDelegationStage,
16
17
  consumeDelegation,
@@ -59,6 +60,7 @@ import { hasOpenLifecycleExecution } from './activity-log.ts';
59
60
  import { invalidationBlocks } from './invalidation.ts';
60
61
  import type { InvalidationDocument } from './invalidation.ts';
61
62
  import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
63
+ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
62
64
 
63
65
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
64
66
  type ModelPolicySource = Readonly<{
@@ -1444,6 +1446,68 @@ function recoverPreparedOrchestrationDelegation(
1444
1446
  }
1445
1447
  }
1446
1448
 
1449
+ function recoverActivatedOrchestrationDelegationUnderLock(
1450
+ taskRef: string,
1451
+ event: Readonly<{
1452
+ receiptId: string;
1453
+ stage: DelegationStage;
1454
+ round: number;
1455
+ artifact: string;
1456
+ startedAgent: string;
1457
+ childId: string;
1458
+ stopRevision: number;
1459
+ consumer: string;
1460
+ consumedAt: string;
1461
+ }>,
1462
+ options: OrchestrationOptions = {}
1463
+ ): OrchestrationResult {
1464
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1465
+ if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1466
+ const run = readRun(resolved.taskDir, options);
1467
+ if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
1468
+ const matches = [
1469
+ ...(run.pendingDelegation ? [run.pendingDelegation] : []),
1470
+ ...run.receipts.filter((receipt) => receipt.id === event.receiptId)
1471
+ ];
1472
+ const receipt = matches.find((candidate) => candidate.id === event.receiptId);
1473
+ if (!receipt) return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
1474
+ if (
1475
+ receipt.taskId !== resolved.taskId
1476
+ || receipt.stage !== event.stage
1477
+ || receipt.round !== event.round
1478
+ || receipt.artifact !== event.artifact
1479
+ || receipt.childId !== event.childId
1480
+ || normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)
1481
+ ) {
1482
+ return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
1483
+ }
1484
+ if (receipt.status === 'aborted' && run.pendingDelegation === null) {
1485
+ return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
1486
+ }
1487
+ if (run.pendingDelegation?.id !== receipt.id || receipt.status !== 'activated') {
1488
+ return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'recovery requires one matching activated pending delegation', resolved.taskId);
1489
+ }
1490
+ if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
1491
+ return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
1492
+ }
1493
+ const aborted = abortActivatedDelegation(receipt, {
1494
+ childId: event.childId,
1495
+ stopRevision: event.stopRevision,
1496
+ consumer: event.consumer,
1497
+ consumedAt: event.consumedAt
1498
+ });
1499
+ if (!aborted.ok) return failed(aborted.code, aborted.message, resolved.taskId);
1500
+ const updated = withUpdatedRun(run, {
1501
+ status: 'running',
1502
+ pause: null,
1503
+ nextStage: null,
1504
+ pendingDelegation: null,
1505
+ receipts: Object.freeze([...run.receipts, aborted.receipt])
1506
+ }, options.now);
1507
+ saveRun(resolved.taskDir, updated);
1508
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
1509
+ }
1510
+
1447
1511
  function completeOrchestrationStage(
1448
1512
  taskRef: string,
1449
1513
  event: Parameters<typeof completeDelegationStage>[1],
@@ -1598,6 +1662,7 @@ export {
1598
1662
  readRun,
1599
1663
  reconcileMatchingOrchestrationDelegation,
1600
1664
  recoverPreparedOrchestrationDelegation,
1665
+ recoverActivatedOrchestrationDelegationUnderLock,
1601
1666
  routeOrchestration,
1602
1667
  sealMatchingOrchestrationDelegation,
1603
1668
  sealMatchingOrchestrationDelegationWithHostEvidence,