@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
@@ -1,4 +1,3 @@
1
- import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.js';
2
1
  import { isResourceIdentity, parseResourceIdentity, resourceIdentityNumber, serializeResourceIdentity } from '../platform/resource-identity.js';
3
2
  const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
4
3
  function factError(message) {
@@ -28,11 +27,6 @@ function timestamp(value, label) {
28
27
  throw factError(`${label} must be a valid timestamp`);
29
28
  return new Date(result).toISOString();
30
29
  }
31
- function positiveNumber(value, label) {
32
- if (!Number.isSafeInteger(value) || value <= 0)
33
- throw factError(`${label} must be a positive integer`);
34
- return value;
35
- }
36
30
  function parseRef(value, label) {
37
31
  if (!isRecord(value))
38
32
  throw factError(`${label} must be an object`);
@@ -51,36 +45,17 @@ function canonicalIdentity(value) {
51
45
  base: parseRef(value.base, 'identity.base')
52
46
  };
53
47
  }
54
- function decodeLegacyIdentity(value) {
55
- if (!isRecord(value))
56
- throw factError('legacy identity must be an object');
57
- // TODO(compat): Remove the v1 identity decoder before the first stable v1.0.0 release.
58
- exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'legacy identity');
59
- const legacy = {
60
- number: positiveNumber(value.number, 'legacy identity.number'),
61
- repository: text(value.repository, 'identity.repository'),
62
- nodeId: text(value.nodeId, 'legacy identity.nodeId'),
63
- url: text(value.url, 'identity.url'),
64
- head: parseRef(value.head, 'identity.head'),
65
- base: parseRef(value.base, 'identity.base')
66
- };
67
- return {
68
- resource: { kind: 'number', value: legacy.number },
69
- repository: legacy.repository,
70
- url: legacy.url,
71
- head: legacy.head,
72
- base: legacy.base
73
- };
74
- }
75
48
  function provenanceForSource(source) { return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source; }
76
- function parseFact(value, runtimeVersion = VERSION) {
49
+ function legacyFactError() {
50
+ return Object.assign(new Error('PLATFORM_IDENTITY_LEGACY_UNSUPPORTED: v1 PR delivery fact is unsupported; use the previous tool version to view this task or migrate its fact to version 2 before using this version'), { code: 'PLATFORM_IDENTITY_LEGACY_UNSUPPORTED' });
51
+ }
52
+ function parseFact(value) {
77
53
  if (!isRecord(value))
78
54
  throw factError('fact must be an object');
79
- if ((value.version !== 1 && value.version !== 2) || typeof value.state !== 'string')
55
+ if (value.version === 1)
56
+ throw legacyFactError();
57
+ if (value.version !== 2 || typeof value.state !== 'string')
80
58
  throw factError('version or state is invalid');
81
- if (value.version === 1 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
82
- throw legacyCompatibilityError('v1 PR delivery fact', runtimeVersion);
83
- }
84
59
  if (value.state === 'unbound') {
85
60
  exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
86
61
  if (value.reason !== 'initial')
@@ -95,25 +70,6 @@ function parseFact(value, runtimeVersion = VERSION) {
95
70
  }
96
71
  if (value.state !== 'bound')
97
72
  throw factError('state is invalid');
98
- if (value.version === 1) {
99
- // TODO(compat): Remove this v1-to-v2 shape conversion before the first stable v1.0.0 release.
100
- exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'legacy bound fact');
101
- if (!isRecord(value.binding) || !isRecord(value.provenance))
102
- throw factError('legacy binding or provenance is invalid');
103
- exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'legacy binding');
104
- const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'legacy binding.issueNumber');
105
- return parseFact({
106
- version: 2,
107
- state: 'bound',
108
- identity: decodeLegacyIdentity(value.identity),
109
- binding: {
110
- status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'),
111
- issueIdentity: issueNumber === null ? null : { kind: 'number', value: issueNumber }, remoteState: value.binding.remoteState,
112
- mergedAt: value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt'), mergeCommitSha: nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha')
113
- },
114
- provenance: value.provenance
115
- }, runtimeVersion);
116
- }
117
73
  exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
118
74
  if (!isRecord(value.binding) || !isRecord(value.provenance))
119
75
  throw factError('binding or provenance is invalid');
@@ -142,7 +98,7 @@ function parseFact(value, runtimeVersion = VERSION) {
142
98
  provenance: { establishedBy: value.provenance.establishedBy }
143
99
  };
144
100
  }
145
- function decodePrDeliveryFact(value, runtimeVersion = VERSION) {
101
+ function decodePrDeliveryFact(value, _runtimeVersion) {
146
102
  if (typeof value === 'string') {
147
103
  let decoded;
148
104
  try {
@@ -151,16 +107,16 @@ function decodePrDeliveryFact(value, runtimeVersion = VERSION) {
151
107
  catch {
152
108
  throw factError('value is not valid JSON');
153
109
  }
154
- return parseFact(decoded, runtimeVersion);
110
+ return parseFact(decoded);
155
111
  }
156
- return parseFact(value, runtimeVersion);
112
+ return parseFact(value);
157
113
  }
158
114
  function encodePrDeliveryFact(fact) { return JSON.stringify(parseFact(fact)); }
159
- function readPrDeliveryFact(metadata, runtimeVersion = VERSION) {
115
+ function readPrDeliveryFact(metadata, _runtimeVersion) {
160
116
  if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '')
161
117
  return { status: 'missing', fact: null };
162
118
  try {
163
- return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY], runtimeVersion) };
119
+ return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) };
164
120
  }
165
121
  catch (error) {
166
122
  return { status: 'invalid', fact: null, error: asFactError(error) };
@@ -0,0 +1,19 @@
1
+ function warningText(value) {
2
+ return typeof value === 'string' && value.length > 0 && value.trim() === value && !/[\r\n]/u.test(value);
3
+ }
4
+ export function isRecoveryWarning(value) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value))
6
+ return false;
7
+ const warning = value;
8
+ return Object.keys(warning).sort().join(',') === 'action,code,message'
9
+ && warningText(warning.code)
10
+ && warningText(warning.message)
11
+ && warningText(warning.action);
12
+ }
13
+ export function sameRecoveryWarning(left, right) {
14
+ return isRecoveryWarning(left) && isRecoveryWarning(right)
15
+ && left.code === right.code
16
+ && left.message === right.message
17
+ && left.action === right.action;
18
+ }
19
+ //# sourceMappingURL=recovery-warning.js.map
@@ -1,9 +1,10 @@
1
1
  export function parseReviewCommand(args) {
2
- if (!args[0] || args[1] !== 'finalize-summary')
3
- throw new Error('task ref and finalize-summary are required');
2
+ if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? ''))
3
+ throw new Error('task ref and a supported review operation are required');
4
4
  const values = {};
5
5
  const flags = {
6
6
  '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
7
+ '--recovery-id': 'recoveryId',
7
8
  '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
8
9
  };
9
10
  for (let index = 2; index < args.length; index += 1) {
@@ -1,6 +1,4 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
2
  import { parseArtifactName } from './artifact-name.js';
5
3
  import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.js';
6
4
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
@@ -12,18 +10,16 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
12
10
  import { allowsManualOverride } from './guard-override.js';
13
11
  import { getArtifactSchema } from './artifact-schema.js';
14
12
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
15
- import { readArtifactRepairIntent, writeArtifactRepairIntent } from './artifact-repair-intent.js';
13
+ import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
14
+ import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
15
+ import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
16
16
  const STAGES = {
17
17
  analysis: { family: 'review-analysis' },
18
18
  plan: { family: 'review-plan' },
19
19
  code: { family: 'review-code' }
20
20
  };
21
21
  const DEFAULT_FILE_SYSTEM = {
22
- readFileSync: (file) => fs.readFileSync(file, 'utf8'),
23
- statModeSync: (file) => fs.statSync(file).mode,
24
- writeFileSync: (file, content, mode) => fs.writeFileSync(file, content, { encoding: 'utf8', flag: 'wx', mode }),
25
- renameSync: (from, to) => fs.renameSync(from, to),
26
- unlinkSync: (file) => fs.unlinkSync(file)
22
+ readFileSync: (file) => fs.readFileSync(file, 'utf8')
27
23
  };
28
24
  function failed(request, code, message, taskId = null, stageStatus = null, extra = {}) {
29
25
  return {
@@ -37,21 +33,167 @@ function failed(request, code, message, taskId = null, stageStatus = null, extra
37
33
  stageStatus,
38
34
  artifactSha256: null,
39
35
  semanticDigest: null,
40
- repairable: false,
41
- operation: null,
42
36
  operations: [],
43
37
  error: { code, message },
44
38
  ...extra
45
39
  };
46
40
  }
47
- function cleanupTemp(fileSystem, tempPath) {
41
+ function preflightFailed(request, code, message, taskId = null, extra = {}) {
42
+ const { status: _status, intent: _intent, stageStatus: _stageStatus, ...finalizationExtra } = extra;
43
+ return {
44
+ ...failed(request, code, message, taskId, null, finalizationExtra),
45
+ status: 'failed',
46
+ intent: 'preflight',
47
+ stageStatus: null
48
+ };
49
+ }
50
+ /** Validate and seal a review draft before any finding-ledger write. */
51
+ function preflightReviewSummaryUnlocked(request, options = {}) {
52
+ const stage = request.stage;
53
+ const spec = STAGES[stage];
54
+ if (!spec)
55
+ return preflightFailed(request, 'REVIEW_STAGE_INVALID', `unsupported review stage '${request.stage}'`);
56
+ if (!request.taskRef || !request.artifact)
57
+ return preflightFailed(request, 'REVIEW_PAYLOAD_INVALID', 'taskRef, stage, and artifact are required');
58
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
59
+ if (!resolved.ok)
60
+ return preflightFailed(request, resolved.code, resolved.message, resolved.taskId ?? null);
61
+ if (resolved.state !== 'active')
62
+ return preflightFailed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
63
+ const parsed = parseArtifactName(request.artifact);
64
+ if (!parsed || parsed.family !== spec.family)
65
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${spec.family}`, resolved.taskId);
66
+ const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
67
+ if (publicationError)
68
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
69
+ let taskContent;
70
+ let content;
71
+ let recovery;
72
+ let existingIntent;
48
73
  try {
49
- fileSystem.unlinkSync(tempPath);
74
+ taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
75
+ if (request.recoveryId) {
76
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
77
+ if (!intent || intent.recoveryOperationId !== request.recoveryId)
78
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
79
+ if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
80
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
81
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
82
+ content = fs.existsSync(recovery.stagingPath)
83
+ ? fs.readFileSync(recovery.stagingPath, 'utf8')
84
+ : fs.readFileSync(recovery.formalPath, 'utf8');
85
+ }
86
+ else {
87
+ const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
88
+ if (!validated.ok)
89
+ return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
90
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
91
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
92
+ existingIntent = intent;
93
+ if (intent?.state === 'preflight-ready')
94
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
95
+ if (intent?.state === 'consumed')
96
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
97
+ }
50
98
  }
51
- catch {
52
- // Best-effort cleanup preserves the primary failure.
99
+ catch (error) {
100
+ return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
101
+ }
102
+ if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
103
+ return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
104
+ const execution = validateLifecycleExecution(request.taskRef, {
105
+ mode: request.orchestrated ? 'orchestrated' : 'standalone',
106
+ identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
107
+ dryRun: request.dryRun
108
+ }, { repoRoot: options.repoRoot });
109
+ if (!execution.ok)
110
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`, resolved.taskId);
111
+ const artifactSha256 = sha256Content(content);
112
+ const semanticDigest = canonicalSemanticDigest(content);
113
+ const detail = inspectDecisionDetailDuplicates(content);
114
+ const structure = inspectArtifactContract(content, getArtifactSchema(spec.family));
115
+ const expected = expectedQualificationRelations(taskContent, spec.family);
116
+ if (!expected.ok)
117
+ return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
118
+ const qualification = validateQualificationAudit(taskContent, content, {
119
+ family: spec.family,
120
+ artifact: request.artifact,
121
+ expectedUpstreamRelations: expected.relations
122
+ });
123
+ const qualificationError = qualification.ok ? null : qualification;
124
+ if (!detail.ok || !structure.ok || qualificationError) {
125
+ if (!recovery && !request.dryRun) {
126
+ try {
127
+ recovery = beginArtifactRecovery({
128
+ taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
129
+ requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
130
+ }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
131
+ }
132
+ catch (error) {
133
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
134
+ }
135
+ }
136
+ const diagnostics = [
137
+ ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
138
+ ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
139
+ ...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
140
+ ].join('; ');
141
+ return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
142
+ artifactSha256, semanticDigest,
143
+ ...(recovery ? { recovery: recoveryInfo(recovery) } : {})
144
+ });
145
+ }
146
+ if (!recovery && existingIntent?.state === 'passed') {
147
+ return {
148
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
149
+ status: 'passed', changed: false, error: null
150
+ };
151
+ }
152
+ if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
153
+ return {
154
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
155
+ status: 'passed', changed: false, error: null
156
+ };
157
+ }
158
+ try {
159
+ recovery ??= beginArtifactRecovery({
160
+ taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
161
+ requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
162
+ }, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
163
+ const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
164
+ prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
165
+ return {
166
+ ...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
167
+ status: 'passed', changed: false, error: null
168
+ };
169
+ }
170
+ catch (error) {
171
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
172
+ }
173
+ }
174
+ function preflightReviewSummary(request, options = {}) {
175
+ if (request.dryRun || options.lockAlreadyHeld)
176
+ return preflightReviewSummaryUnlocked(request, options);
177
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
178
+ if (!resolved.ok)
179
+ return preflightReviewSummaryUnlocked(request, options);
180
+ try {
181
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.preflight', () => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true }));
182
+ }
183
+ catch (error) {
184
+ if (!(error instanceof TaskExecutionLockError))
185
+ throw error;
186
+ return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
53
187
  }
54
188
  }
189
+ function recoveryInfo(recovery) {
190
+ return {
191
+ recoveryId: recovery.recoveryId,
192
+ candidatePath: recovery.stagingPath,
193
+ baselineSha256: recovery.baselineSha256,
194
+ baselineSemanticDigest: recovery.baselineSemanticDigest
195
+ };
196
+ }
55
197
  /** Prepare domain validation, summary bytes and provenance before any publication. */
56
198
  function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
57
199
  let taskId = null;
@@ -85,43 +227,41 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
85
227
  catch (error) {
86
228
  return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error));
87
229
  }
230
+ let recovery;
231
+ if (request.recoveryId) {
232
+ try {
233
+ const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
234
+ if (!intent || intent.recoveryOperationId !== request.recoveryId)
235
+ return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
236
+ if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
237
+ return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
238
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
239
+ artifactContent = fs.existsSync(recovery.stagingPath)
240
+ ? fs.readFileSync(recovery.stagingPath, 'utf8')
241
+ : fs.readFileSync(recovery.formalPath, 'utf8');
242
+ }
243
+ catch (error) {
244
+ return reject('REVIEW_PROVENANCE_INVALID', String(error));
245
+ }
246
+ }
88
247
  const artifactSha256 = sha256Content(artifactContent);
89
248
  const semanticDigest = canonicalSemanticDigest(artifactContent);
90
249
  const digests = { artifactSha256, semanticDigest };
91
250
  let repairIntent;
92
251
  try {
93
- repairIntent = readArtifactRepairIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
252
+ repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
94
253
  }
95
254
  catch (error) {
96
255
  return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
97
256
  }
98
- const schema = getArtifactSchema(spec.family);
99
- const structure = inspectArtifactContract(artifactContent, schema);
100
- if (!structure.ok) {
101
- const repairable = structure.repair !== null && structure.diagnostics.length === 1 && structure.diagnostics[0]?.repairable === true;
102
- if (repairable && repairIntent && (repairIntent.state !== 'awaiting-repair'
103
- || repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== structure.semanticDigest)) {
104
- return reject('REVIEW_PROVENANCE_INVALID', 'a different review repair baseline is already recorded for this artifact', digests);
257
+ if (repairIntent?.state === 'consumed') {
258
+ if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
259
+ return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
105
260
  }
106
- return {
107
- ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
108
- artifactSha256, semanticDigest: structure.semanticDigest, repairable, operation: structure.repair
109
- }),
110
- ...(repairable && !repairIntent ? { provenance: {
111
- version: 2, taskId: taskId, family: spec.family, artifact: request.artifact,
112
- state: 'awaiting-repair', baselineSemanticDigest: structure.semanticDigest,
113
- artifactSha256, semanticDigest: structure.semanticDigest,
114
- recoveryOperationId: null, phase: null, authorityDigest: null,
115
- requestId: `review-finalize:${taskId}`, createdAt: Date.now(), updatedAt: Date.now()
116
- }, expectedIntent: null } : {})
117
- };
118
- }
119
- if (repairIntent?.state === 'awaiting-repair' && repairIntent.baselineSemanticDigest !== semanticDigest) {
120
- return reject('REVIEW_PROVENANCE_INVALID', 'the repaired review artifact semantic digest does not match the recorded repair baseline', digests);
121
261
  }
122
- if ((repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')
123
- && (repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== semanticDigest)) {
124
- return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
262
+ if (repairIntent?.state === 'passed'
263
+ && (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
264
+ repairIntent = undefined;
125
265
  }
126
266
  if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
127
267
  return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
@@ -149,10 +289,68 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
149
289
  const detail = inspectDecisionDetailDuplicates(artifactContent);
150
290
  if (!detail.ok)
151
291
  return reject('REVIEW_DECISION_DETAIL_INVALID', `${detail.code}: ${detail.message}`);
292
+ const schema = getArtifactSchema(spec.family);
293
+ const structure = inspectArtifactContract(artifactContent, schema);
294
+ if (!structure.ok) {
295
+ if (!recovery && !request.dryRun && options.startRecovery) {
296
+ try {
297
+ recovery = beginArtifactRecovery({
298
+ taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
299
+ requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
300
+ }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
301
+ }
302
+ catch (error) {
303
+ return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
304
+ }
305
+ }
306
+ return {
307
+ ...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
308
+ artifactSha256, semanticDigest: structure.semanticDigest,
309
+ ...(recovery ? { recovery: {
310
+ recoveryId: recovery.recoveryId,
311
+ candidatePath: recovery.stagingPath,
312
+ baselineSha256: recovery.baselineSha256,
313
+ baselineSemanticDigest: recovery.baselineSemanticDigest
314
+ } } : {})
315
+ }),
316
+ ...(recovery ? { recovery } : {})
317
+ };
318
+ }
152
319
  const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
153
320
  if (!transformed.ok)
154
321
  return reject(transformed.code, transformed.message);
155
322
  const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
323
+ if (!recovery && repairIntent?.state === 'commit-started') {
324
+ if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
325
+ return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
326
+ }
327
+ recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
328
+ }
329
+ if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
330
+ if (transformed.changed)
331
+ return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
332
+ return {
333
+ content: transformed.content,
334
+ result: {
335
+ ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
336
+ status: 'no-op',
337
+ changed: false,
338
+ error: null
339
+ },
340
+ lockAlreadyHeld: options.lockAlreadyHeld
341
+ };
342
+ }
343
+ if (!recovery && !request.dryRun && options.startRecovery) {
344
+ try {
345
+ recovery = beginArtifactRecovery({
346
+ taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
347
+ requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
348
+ }, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
349
+ }
350
+ catch (error) {
351
+ return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests);
352
+ }
353
+ }
156
354
  return {
157
355
  content: transformed.content,
158
356
  result: {
@@ -162,22 +360,64 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
162
360
  operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
163
361
  error: null
164
362
  },
165
- ...(repairIntent?.state === 'awaiting-repair' ? {
166
- provenance: { ...repairIntent, state: 'passed', ...finalDigests },
167
- expectedIntent: repairIntent
168
- } : {})
363
+ ...(recovery ? { recovery } : {}),
364
+ lockAlreadyHeld: options.lockAlreadyHeld
169
365
  };
170
366
  }
171
- function commitReviewSummaryProvenance(prepared, repoRoot) {
367
+ function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
368
+ if (prepared.result.status === 'failed' || !prepared.recovery)
369
+ return prepared.result;
172
370
  try {
173
- if (prepared.provenance) {
174
- writeArtifactRepairIntent(repoRoot, prepared.provenance, { expected: prepared.expectedIntent ?? null });
371
+ const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
372
+ if (!intent)
373
+ throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
374
+ let committed;
375
+ if (intent.state === 'passed' || intent.state === 'consumed') {
376
+ return {
377
+ ...prepared.result,
378
+ status: 'no-op',
379
+ changed: false,
380
+ artifactSha256: intent.finalArtifactSha256,
381
+ semanticDigest: intent.finalSemanticDigest,
382
+ error: null
383
+ };
175
384
  }
176
- return prepared.result;
385
+ else if (intent.state === 'commit-started') {
386
+ const reconciled = reconcileArtifactRecovery(prepared.recovery, {
387
+ lockAlreadyHeld: prepared.lockAlreadyHeld,
388
+ afterPublish: options.afterPublish
389
+ });
390
+ committed = reconciled.intent;
391
+ }
392
+ else if (intent.state === 'awaiting-preflight-recovery') {
393
+ const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
394
+ prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
395
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
396
+ }
397
+ else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
398
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
399
+ }
400
+ if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
401
+ prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
402
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
403
+ }
404
+ else if (intent.state === 'full-finalizer-ready') {
405
+ committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
406
+ }
407
+ if (!committed || committed.state !== 'passed')
408
+ throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
409
+ return {
410
+ ...prepared.result,
411
+ artifactSha256: committed.finalArtifactSha256,
412
+ semanticDigest: committed.finalSemanticDigest,
413
+ error: null
414
+ };
177
415
  }
178
416
  catch (error) {
417
+ if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
418
+ throw error;
179
419
  return { ...prepared.result, status: 'failed', error: {
180
- code: 'REVIEW_PROVENANCE_INVALID', message: `cannot record review provenance: ${String(error)}`
420
+ code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
181
421
  } };
182
422
  }
183
423
  }
@@ -203,37 +443,17 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
203
443
  const prepared = prepareReviewSummaryCandidate(request, content, options);
204
444
  if (request.dryRun)
205
445
  return prepared.result;
206
- if (prepared.result.status !== 'applied')
207
- return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
208
- const tempPath = path.join(resolved.taskDir, `.${request.artifact}.tmp-${process.pid}-${(options.randomSuffix ?? randomUUID)()}`);
209
- try {
210
- fileSystem.writeFileSync(tempPath, prepared.content, fileSystem.statModeSync(validated.artifact.path));
211
- }
212
- catch (error) {
213
- cleanupTemp(fileSystem, tempPath);
214
- return failed(request, 'REVIEW_TEMP_WRITE_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
215
- }
216
- try {
217
- if (fileSystem.readFileSync(validated.artifact.path) !== content) {
218
- cleanupTemp(fileSystem, tempPath);
219
- return failed(request, 'REVIEW_ARTIFACT_CONFLICT', `${request.artifact} changed during finalization`, resolved.taskId, prepared.result.stageStatus);
220
- }
221
- fileSystem.renameSync(tempPath, validated.artifact.path);
222
- }
223
- catch (error) {
224
- cleanupTemp(fileSystem, tempPath);
225
- return failed(request, 'REVIEW_RENAME_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
226
- }
227
446
  return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
228
447
  }
229
448
  function finalizeReviewSummary(request, options = {}) {
449
+ const effectiveOptions = { ...options, startRecovery: !request.dryRun };
230
450
  if (request.dryRun || options.lockAlreadyHeld)
231
- return finalizeReviewSummaryUnlocked(request, options);
451
+ return finalizeReviewSummaryUnlocked(request, effectiveOptions);
232
452
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
233
453
  if (!resolved.ok)
234
- return finalizeReviewSummaryUnlocked(request, options);
454
+ return finalizeReviewSummaryUnlocked(request, effectiveOptions);
235
455
  try {
236
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => finalizeReviewSummaryUnlocked(request, options));
456
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => finalizeReviewSummaryUnlocked(request, { ...effectiveOptions, lockAlreadyHeld: true }));
237
457
  }
238
458
  catch (error) {
239
459
  if (!(error instanceof TaskExecutionLockError))
@@ -241,5 +461,5 @@ function finalizeReviewSummary(request, options = {}) {
241
461
  return failed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
242
462
  }
243
463
  }
244
- export { finalizeReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
464
+ export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
245
465
  //# sourceMappingURL=review-finalization.js.map