@fitlab-ai/agent-infra 0.9.14 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -3,7 +3,8 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
5
  import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
6
- import { artifactFamilyCatalog, inspectArtifactDirectory, parseArtifactName } from './artifact-lifecycle.ts';
6
+ import { artifactFamilyCatalog, inspectArtifactDirectory } from './artifact-lifecycle.ts';
7
+ import { parseArtifactName } from './artifact-name.ts';
7
8
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
8
9
  import { locateHotTaskDirs, resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
9
10
  import {
@@ -337,6 +338,21 @@ function parseJournal(content: string): LifecycleJournal {
337
338
  return value as LifecycleJournal;
338
339
  }
339
340
 
341
+ /** Recovery reads the same journal contract as the lifecycle writer. */
342
+ export function readLifecycleJournalEvidence(repoRoot: string, taskId: string): Readonly<{
343
+ exists: boolean; completedSteps: readonly string[]; failure: string | null;
344
+ }> {
345
+ const journalPath = locateHotTaskDirs(repoRoot, taskId)
346
+ .map((entry) => path.join(entry.taskDir, '.task-lifecycle.json'))
347
+ .find((candidate) => fs.existsSync(candidate));
348
+ if (!journalPath) return { exists: false, completedSteps: [], failure: null };
349
+ try {
350
+ const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
351
+ if (journal.taskId !== taskId) throw new Error('journal task identity mismatch');
352
+ return { exists: true, completedSteps: journal.completedSteps, failure: journal.failure?.code ?? null };
353
+ } catch { return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' }; }
354
+ }
355
+
340
356
  type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
341
357
 
342
358
  function directoryManifest(root: string): DirectoryEntry[] {
@@ -1,8 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
 
3
- import { inspectDecisionDetailDuplicates, scanVisibleMarkdown } from './decision-details.ts';
3
+ import { inspectDecisionDetailDuplicates } from './decision-details.ts';
4
+ import { scanVisibleMarkdown } from './markdown.ts';
5
+ import { parseArtifactName } from './artifact-name.ts';
4
6
  import {
5
- parseArtifactName,
7
+ hasOpenArtifactRound,
8
+ validateArtifactPublication,
6
9
  validateCompletedArtifact
7
10
  } from './artifact-lifecycle.ts';
8
11
  import { resolveTaskRef } from './resolve-ref.ts';
@@ -11,6 +14,12 @@ import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStruct
11
14
  import { getArtifactSchema } from './artifact-schema.ts';
12
15
  import { readArtifactRepairIntent, writeArtifactRepairIntent } from './artifact-repair-intent.ts';
13
16
  import type { ArtifactRepairIntent } from './artifact-repair-intent.ts';
17
+ import {
18
+ consumeLifecycleRecoveryAttestation,
19
+ lifecycleRecoveryAttestationDigest,
20
+ validateLifecycleRecoveryAttestation
21
+ } from './control-authority.ts';
22
+ import type { LifecycleRecoveryAttestationV1 } from './control-authority.ts';
14
23
 
15
24
  type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
16
25
 
@@ -82,17 +91,26 @@ type LocalArtifactFinalizationResult = {
82
91
 
83
92
  type LocalArtifactFinalizationIntent = ArtifactRepairIntent;
84
93
 
85
- function readLocalArtifactFinalizationIntent(
86
- repoRoot: string,
87
- taskId: string,
88
- family: LocalArtifactFamily,
89
- artifact: string
90
- ): LocalArtifactFinalizationIntent | null {
91
- return readArtifactRepairIntent(repoRoot, taskId, family, artifact);
94
+ function authorityDigest(authority: LifecycleRecoveryAttestationV1): string {
95
+ return lifecycleRecoveryAttestationDigest(authority);
92
96
  }
93
97
 
94
- function writeLocalArtifactFinalizationIntent(repoRoot: string, value: LocalArtifactFinalizationIntent): void {
95
- writeArtifactRepairIntent(repoRoot, value);
98
+ function validFinalizerAuthority(
99
+ authority: LifecycleRecoveryAttestationV1 | undefined,
100
+ request: LocalArtifactFinalizationRequest,
101
+ taskId: string
102
+ ): boolean {
103
+ if (!authority) return false;
104
+ try { validateLifecycleRecoveryAttestation(authority); }
105
+ catch { return false; }
106
+ return authority.version === 1
107
+ && authority.phase === 'artifact.finalize-local'
108
+ && authority.taskId === taskId
109
+ && authority.family === request.family
110
+ && authority.artifact === request.artifact
111
+ && authority.round === (parseArtifactName(request.artifact)?.round ?? 0)
112
+ && authority.requestId.length > 0
113
+ && authority.operationId.length > 0;
96
114
  }
97
115
 
98
116
  function consumeLocalArtifactFinalizationIntent(
@@ -101,8 +119,8 @@ function consumeLocalArtifactFinalizationIntent(
101
119
  ): LocalArtifactFinalizationIntent {
102
120
  if (intent.state === 'consumed') return intent;
103
121
  if (intent.state !== 'passed') throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: only passed provenance can be consumed');
104
- const consumed = { ...intent, state: 'consumed' as const };
105
- writeLocalArtifactFinalizationIntent(repoRoot, consumed);
122
+ const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
123
+ writeArtifactRepairIntent(repoRoot, consumed, { expected: intent });
106
124
  return consumed;
107
125
  }
108
126
 
@@ -222,200 +240,147 @@ function failedFinalization(
222
240
  };
223
241
  }
224
242
 
225
- function provenanceFailure(
243
+ type LocalArtifactPreparation = Readonly<{
244
+ result: LocalArtifactFinalizationResult;
245
+ content: string;
246
+ repoRoot?: string;
247
+ provenance?: ArtifactRepairIntent;
248
+ expectedIntent?: ArtifactRepairIntent | null;
249
+ }>;
250
+
251
+ /** Read and validate once; callers publish this content before committing provenance. */
252
+ function prepareLocalArtifact(
226
253
  request: LocalArtifactFinalizationRequest,
227
- resolved: { taskId: string; taskDir: string },
228
- content: string,
229
- artifactSha256: string,
230
- semanticDigestValue: string,
231
- code: LocalArtifactDiagnosticCode,
232
- message: string
233
- ): LocalArtifactFinalizationResult {
234
- return failedFinalization(request, { code, message }, {
235
- taskId: resolved.taskId,
236
- taskDir: resolved.taskDir,
237
- artifactSha256,
238
- semanticDigest: semanticDigestValue,
239
- diagnostics: [{ code, message, repairable: false, line: null }]
254
+ candidate?: string,
255
+ authority?: LifecycleRecoveryAttestationV1
256
+ ): LocalArtifactPreparation {
257
+ const failed = (code: string, message: string): LocalArtifactPreparation => ({
258
+ result: failedFinalization(request, { code, message }), content: candidate ?? ''
240
259
  });
241
- }
242
-
243
- function finalizeLocalArtifact(request: LocalArtifactFinalizationRequest): LocalArtifactFinalizationResult {
244
260
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
245
- if (!resolved.ok) return failedFinalization(request, { code: resolved.code, message: resolved.message });
261
+ if (!resolved.ok) return failed(resolved.code, resolved.message);
246
262
  const parsed = parseArtifactName(request.artifact);
247
- if (!parsed || parsed.family !== request.family) {
248
- return failedFinalization(request, {
249
- code: 'ARTIFACT_IDENTITY_INVALID',
250
- message: `artifact '${request.artifact}' does not match ${request.family}`
251
- }, { taskId: resolved.taskId, taskDir: resolved.taskDir });
252
- }
253
- const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
254
- if (!validated.ok) {
255
- return failedFinalization(request, validated.error, { taskId: resolved.taskId, taskDir: resolved.taskDir });
256
- }
263
+ if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
257
264
  let content: string;
258
- try { content = fs.readFileSync(validated.artifact.path, 'utf8'); }
259
- catch (error) {
260
- return failedFinalization(request, { code: 'ARTIFACT_NOT_READABLE', message: String(error) }, {
261
- taskId: resolved.taskId, taskDir: resolved.taskDir
262
- });
263
- }
264
265
  let taskContent: string;
265
- try { taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
266
- catch (error) {
267
- return failedFinalization(request, { code: 'TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) }, {
268
- taskId: resolved.taskId, taskDir: resolved.taskDir
269
- });
270
- }
271
- const result = validateLocalArtifact(content, {
272
- family: request.family,
273
- requiredSections: request.requiredSections,
274
- taskContent,
275
- artifact: request.artifact
276
- });
277
- const artifactSha256 = sha256Content(content);
278
- let intent: LocalArtifactFinalizationIntent | null;
279
266
  try {
280
- intent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
281
- } catch (error) {
282
- return failedFinalization(request, {
283
- code: 'LOCAL_FINALIZATION_INTENT_INVALID',
284
- message: error instanceof Error ? error.message : String(error)
285
- }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
286
- }
287
- if (result.repairable) {
288
- if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== result.semanticDigest)) {
289
- return provenanceFailure(
290
- request, resolved, content, artifactSha256, result.semanticDigest,
291
- 'LOCAL_REPAIR_PROVENANCE_CONFLICT',
292
- 'a different local repair baseline is already recorded for this artifact'
293
- );
294
- }
295
- if (!intent) {
296
- try {
297
- writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
298
- version: 1,
299
- taskId: resolved.taskId,
300
- family: request.family,
301
- artifact: request.artifact,
302
- state: 'awaiting-repair',
303
- baselineSemanticDigest: result.semanticDigest,
304
- artifactSha256,
305
- semanticDigest: result.semanticDigest
306
- });
307
- } catch (error) {
308
- return failedFinalization(request, {
309
- code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
310
- message: error instanceof Error ? error.message : String(error)
311
- }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
267
+ 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');
312
273
  }
274
+ content = candidate;
275
+ } else {
276
+ const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
277
+ if (!validated.ok) return failed(validated.error.code, validated.error.message);
278
+ content = fs.readFileSync(validated.artifact.path, 'utf8');
313
279
  }
280
+ } catch (error) { return failed('ARTIFACT_NOT_READABLE', String(error)); }
281
+ const validation = validateLocalArtifact(content, {
282
+ family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
283
+ });
284
+ const artifactSha256 = sha256Content(content);
285
+ const { semanticDigest, repairable, diagnostics } = validation;
286
+ 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)) {
314
291
  return {
315
- status: 'failed',
316
- changed: false,
317
- taskId: resolved.taskId,
318
- taskDir: resolved.taskDir,
319
- family: request.family,
320
- artifact: request.artifact,
321
- artifactSha256,
322
- semanticDigest: result.semanticDigest,
323
- repairable: true,
324
- diagnostics: result.diagnostics,
325
- error: {
326
- code: 'LOCAL_ARTIFACT_INVALID',
327
- message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
328
- }
329
- };
330
- }
331
- if (!result.ok) {
332
- return {
333
- status: 'failed',
334
- changed: false,
335
- taskId: resolved.taskId,
336
- taskDir: resolved.taskDir,
337
- family: request.family,
338
- artifact: request.artifact,
339
- artifactSha256,
340
- semanticDigest: result.semanticDigest,
341
- repairable: false,
342
- diagnostics: result.diagnostics,
343
- error: {
344
- code: 'LOCAL_ARTIFACT_INVALID',
345
- message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
346
- }
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: [] }
347
297
  };
348
298
  }
349
- if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== result.semanticDigest) {
350
- return provenanceFailure(
351
- request, resolved, content, artifactSha256, result.semanticDigest,
352
- 'LOCAL_REPAIR_BASELINE_MISMATCH',
353
- 'the repaired artifact semantic digest does not match the recorded repair baseline'
354
- );
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) } } };
355
308
  }
356
- if ((intent?.state === 'passed' || intent?.state === 'consumed')
357
- && (intent.artifactSha256 !== artifactSha256 || intent.semanticDigest !== result.semanticDigest)) {
358
- return provenanceFailure(
359
- request, resolved, content, artifactSha256, result.semanticDigest,
360
- 'LOCAL_REPAIR_PROVENANCE_CONFLICT',
361
- 'the artifact changed after its finalization provenance was recorded'
362
- );
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');
363
313
  }
364
- if (intent?.state === 'consumed') {
365
- return {
366
- status: 'passed',
367
- changed: false,
368
- taskId: resolved.taskId,
369
- taskDir: resolved.taskDir,
370
- family: request.family,
371
- artifact: request.artifact,
372
- artifactSha256,
373
- semanticDigest: result.semanticDigest,
374
- repairable: false,
375
- diagnostics: result.diagnostics,
376
- error: null
377
- };
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
+ 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');
327
+ }
328
+ prepared.result = { ...result, status: 'passed', error: null };
329
+ if (intent?.state === 'consumed') return prepared;
378
330
  }
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
+ }
345
+
346
+ function commitLocalArtifactProvenance(prepared: LocalArtifactPreparation): LocalArtifactFinalizationResult {
379
347
  try {
380
- writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
381
- version: 1,
382
- taskId: resolved.taskId,
383
- family: request.family,
384
- artifact: request.artifact,
385
- state: 'passed',
386
- baselineSemanticDigest: intent?.baselineSemanticDigest ?? null,
387
- artifactSha256,
388
- semanticDigest: result.semanticDigest
389
- });
348
+ if (prepared.provenance) {
349
+ writeArtifactRepairIntent(prepared.repoRoot!, prepared.provenance, { expected: prepared.expectedIntent ?? null });
350
+ }
351
+ return prepared.result;
390
352
  } catch (error) {
391
- return failedFinalization(request, {
392
- code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
393
- message: error instanceof Error ? error.message : String(error)
394
- }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
353
+ return { ...prepared.result, status: 'failed', error: {
354
+ code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED', message: String(error)
355
+ } };
395
356
  }
396
- return {
397
- status: result.ok ? 'passed' : 'failed',
398
- changed: false,
399
- taskId: resolved.taskId,
400
- taskDir: resolved.taskDir,
401
- family: request.family,
402
- artifact: request.artifact,
403
- artifactSha256,
404
- semanticDigest: result.semanticDigest,
405
- repairable: result.repairable,
406
- diagnostics: result.diagnostics,
407
- error: result.ok ? null : {
408
- code: 'LOCAL_ARTIFACT_INVALID',
409
- message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
357
+ }
358
+
359
+ function finalizeLocalArtifact(
360
+ request: LocalArtifactFinalizationRequest,
361
+ authority?: LifecycleRecoveryAttestationV1,
362
+ options: Readonly<{ deferLifecycleRecoveryConsumption?: boolean }> = {}
363
+ ): LocalArtifactFinalizationResult {
364
+ const result = commitLocalArtifactProvenance(prepareLocalArtifact(request, undefined, authority));
365
+ if (authority && !options.deferLifecycleRecoveryConsumption && result.status === 'passed') {
366
+ try { consumeLifecycleRecoveryAttestation(authority); }
367
+ catch (error) {
368
+ return { ...result, status: 'failed', error: {
369
+ code: 'LOCAL_EXECUTION_AUTHORITY_CONSUME_FAILED',
370
+ message: error instanceof Error ? error.message : String(error)
371
+ } };
410
372
  }
411
- };
373
+ }
374
+ return result;
412
375
  }
413
376
 
414
377
  export {
415
378
  LOCAL_ARTIFACT_REQUIRED_SECTIONS,
416
379
  consumeLocalArtifactFinalizationIntent,
417
380
  finalizeLocalArtifact,
418
- readLocalArtifactFinalizationIntent,
381
+ prepareLocalArtifact,
382
+ commitLocalArtifactProvenance,
383
+ readArtifactRepairIntent as readLocalArtifactFinalizationIntent,
419
384
  canonicalSemanticDigest as semanticDigest,
420
385
  sha256Content,
421
386
  validateLocalArtifact
@@ -427,6 +392,7 @@ export type {
427
392
  LocalArtifactFinalizationRequest,
428
393
  LocalArtifactFinalizationResult,
429
394
  LocalArtifactFinalizationIntent,
395
+ LocalArtifactPreparation,
430
396
  LocalArtifactValidationOptions,
431
397
  LocalArtifactValidationResult
432
398
  };
@@ -0,0 +1,40 @@
1
+ import { readManualValidationReceipt } from './manual-validation-receipt.ts';
2
+ import type { ManualValidationReceipt, ManualValidationReceiptError, ManualValidationReceiptIdentity } from './manual-validation-receipt.ts';
3
+ import { readManualValidationTransaction } from './manual-validation-transaction.ts';
4
+ import type { ManualValidationTransaction, ManualValidationTransactionError } from './manual-validation-transaction.ts';
5
+
6
+ type ManualValidationCompletionIdentity = ManualValidationReceiptIdentity & Readonly<{
7
+ receiptDigest?: string;
8
+ }>;
9
+ type ManualValidationCompletion = Readonly<{
10
+ receipt: ManualValidationReceipt;
11
+ transaction: ManualValidationTransaction;
12
+ }>;
13
+ type ManualValidationCompletionError = ManualValidationReceiptError | ManualValidationTransactionError;
14
+ type ManualValidationCompletionResult =
15
+ | { ok: true; value: ManualValidationCompletion }
16
+ | { ok: false; error: ManualValidationCompletionError };
17
+
18
+ function readManualValidationCompletion(taskDir: string, expected: ManualValidationCompletionIdentity = {}): ManualValidationCompletionResult {
19
+ const { receiptDigest: expectedReceiptDigest, ...receiptIdentity } = expected;
20
+ const receipt = readManualValidationReceipt(taskDir, receiptIdentity);
21
+ if (!receipt.ok) return receipt;
22
+ if (expectedReceiptDigest !== undefined && receipt.value.receiptDigest !== expectedReceiptDigest) {
23
+ return { ok: false, error: { code: 'MANUAL_VALIDATION_RECEIPT_IDENTITY_MISMATCH', message: 'receipt digest does not match the expected completion' } };
24
+ }
25
+ const transaction = readManualValidationTransaction(taskDir, {
26
+ transactionId: receipt.value.transactionId,
27
+ taskId: receipt.value.taskId,
28
+ prNumber: receipt.value.prNumber,
29
+ prHeadSha: receipt.value.prHeadSha,
30
+ artifact: receipt.value.artifact
31
+ });
32
+ if (!transaction.ok) return transaction;
33
+ if (transaction.value.committedReceipt !== receipt.value.receiptDigest) {
34
+ return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'completion transaction is not bound to the canonical receipt' } };
35
+ }
36
+ return { ok: true, value: { receipt: receipt.value, transaction: transaction.value } };
37
+ }
38
+
39
+ export { readManualValidationCompletion };
40
+ export type { ManualValidationCompletion, ManualValidationCompletionError, ManualValidationCompletionIdentity, ManualValidationCompletionResult };