codex-workflow-v2 2.0.0-beta.13.8 → 2.0.0-beta.14

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 (281) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +96 -32
  3. package/dist/src/alpha6/adoption.d.ts +2 -0
  4. package/dist/src/alpha6/adoption.js +22 -0
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
  19. package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
  20. package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
  21. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  22. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  23. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  24. package/dist/src/alpha6/milestone.d.ts +2 -0
  25. package/dist/src/alpha6/milestone.js +10 -1
  26. package/dist/src/alpha6/milestone.js.map +1 -1
  27. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  28. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  29. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  30. package/dist/src/alpha6/plan-integrity.js +7 -5
  31. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  32. package/dist/src/alpha6/plan-risk.d.ts +2 -0
  33. package/dist/src/alpha6/plan-risk.js +10 -3
  34. package/dist/src/alpha6/plan-risk.js.map +1 -1
  35. package/dist/src/alpha6/remediation.d.ts +6 -21
  36. package/dist/src/alpha6/remediation.js +161 -476
  37. package/dist/src/alpha6/remediation.js.map +1 -1
  38. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  39. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  40. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  41. package/dist/src/beta1/project-transaction.d.ts +3 -1
  42. package/dist/src/beta1/project-transaction.js +6 -2
  43. package/dist/src/beta1/project-transaction.js.map +1 -1
  44. package/dist/src/checks/runner.d.ts +26 -0
  45. package/dist/src/checks/runner.js +223 -0
  46. package/dist/src/checks/runner.js.map +1 -0
  47. package/dist/src/checks/task-sync.d.ts +10 -0
  48. package/dist/src/checks/task-sync.js +41 -0
  49. package/dist/src/checks/task-sync.js.map +1 -0
  50. package/dist/src/checks/worker.d.ts +1 -0
  51. package/dist/src/checks/worker.js +154 -0
  52. package/dist/src/checks/worker.js.map +1 -0
  53. package/dist/src/cli-actions.d.ts +2 -2
  54. package/dist/src/cli-actions.js +3 -15
  55. package/dist/src/cli-actions.js.map +1 -1
  56. package/dist/src/cli.js +14 -98
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/contracts.d.ts +35 -32
  59. package/dist/src/dependency-provenance.d.ts +2 -2
  60. package/dist/src/dependency-provenance.js +12 -69
  61. package/dist/src/dependency-provenance.js.map +1 -1
  62. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  63. package/dist/src/domain/base-sync-conflict.js +32 -0
  64. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  65. package/dist/src/domain/step-start-admission.d.ts +4 -0
  66. package/dist/src/domain/step-start-admission.js +9 -0
  67. package/dist/src/domain/step-start-admission.js.map +1 -0
  68. package/dist/src/domain/virgin-registration.d.ts +3 -0
  69. package/dist/src/domain/virgin-registration.js +65 -0
  70. package/dist/src/domain/virgin-registration.js.map +1 -0
  71. package/dist/src/errors.d.ts +1 -1
  72. package/dist/src/errors.js.map +1 -1
  73. package/dist/src/gateway-handshake.js +0 -8
  74. package/dist/src/gateway-handshake.js.map +1 -1
  75. package/dist/src/git.d.ts +3 -1
  76. package/dist/src/git.js +41 -14
  77. package/dist/src/git.js.map +1 -1
  78. package/dist/src/graph.js +25 -3
  79. package/dist/src/graph.js.map +1 -1
  80. package/dist/src/index.d.ts +1 -0
  81. package/dist/src/lifecycle/corrective-replan.js +3 -3
  82. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  83. package/dist/src/navigation-actions.d.ts +9 -0
  84. package/dist/src/navigation-actions.js +72 -0
  85. package/dist/src/navigation-actions.js.map +1 -0
  86. package/dist/src/navigation-update-artifact.d.ts +5 -0
  87. package/dist/src/navigation-update-artifact.js +227 -0
  88. package/dist/src/navigation-update-artifact.js.map +1 -0
  89. package/dist/src/navigation-update.d.ts +39 -0
  90. package/dist/src/navigation-update.js +82 -0
  91. package/dist/src/navigation-update.js.map +1 -0
  92. package/dist/src/observation.js +52 -29
  93. package/dist/src/observation.js.map +1 -1
  94. package/dist/src/observed-routes.js +5 -6
  95. package/dist/src/observed-routes.js.map +1 -1
  96. package/dist/src/pending-review-update.d.ts +0 -13
  97. package/dist/src/pending-review-update.js +1 -6
  98. package/dist/src/pending-review-update.js.map +1 -1
  99. package/dist/src/reviewer.d.ts +1 -1
  100. package/dist/src/reviewer.js +20 -17
  101. package/dist/src/reviewer.js.map +1 -1
  102. package/dist/src/runtime.d.ts +3 -0
  103. package/dist/src/runtime.js +6 -0
  104. package/dist/src/runtime.js.map +1 -0
  105. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  106. package/dist/src/state/corrective-replan-executor.js +37 -1
  107. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  108. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  109. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  110. package/dist/src/state/corrective-replan-public.js +6 -5
  111. package/dist/src/state/corrective-replan-public.js.map +1 -1
  112. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  113. package/dist/src/state/corrective-replan-transaction.js +12 -11
  114. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  115. package/dist/src/state/corrective-yield-executor.js +2 -2
  116. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  117. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  118. package/dist/src/state/corrective-yield-transaction.js +11 -10
  119. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  120. package/dist/src/state/lock.d.ts +4 -0
  121. package/dist/src/state/lock.js +21 -0
  122. package/dist/src/state/lock.js.map +1 -1
  123. package/dist/src/version.d.ts +1 -1
  124. package/dist/src/version.js +1 -1
  125. package/dist/src/version.js.map +1 -1
  126. package/dist/src/workflow-blocker-route.d.ts +8 -0
  127. package/dist/src/workflow-blocker-route.js +106 -0
  128. package/dist/src/workflow-blocker-route.js.map +1 -0
  129. package/dist/src/workflow.d.ts +48 -63
  130. package/dist/src/workflow.js +698 -1470
  131. package/dist/src/workflow.js.map +1 -1
  132. package/docs/autonomy-guardrails.md +26 -304
  133. package/docs/decisions.md +14 -112
  134. package/docs/development-flow.md +35 -238
  135. package/docs/project-memory.md +31 -50
  136. package/docs/release-app-evidence.md +190 -0
  137. package/docs/release.md +95 -363
  138. package/docs/updating-existing-project.md +19 -717
  139. package/package.json +11 -13
  140. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  141. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  142. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  143. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  144. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  145. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  146. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  147. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -740
  148. package/references/state-machine.md +4 -4
  149. package/roles/technical-planner.md +1 -1
  150. package/schemas/corrective-decision-event.schema.json +3 -1
  151. package/schemas/project-knowledge-map.schema.json +3 -1
  152. package/schemas/remediation-event.schema.json +10 -1
  153. package/schemas/task.schema.json +46 -2
  154. package/schemas/transition-payloads.schema.json +16 -1
  155. package/src/alpha6/adoption.ts +1123 -0
  156. package/src/alpha6/captured-check-evidence.ts +132 -0
  157. package/src/alpha6/check-support-anchor.ts +128 -0
  158. package/src/alpha6/component-owner.ts +168 -0
  159. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  160. package/src/alpha6/downstream-proof.ts +520 -0
  161. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  162. package/src/alpha6/handoff.ts +1443 -0
  163. package/src/alpha6/journal.ts +473 -0
  164. package/src/alpha6/literal-test-invocation.ts +84 -0
  165. package/src/alpha6/mechanical-feasibility.ts +488 -0
  166. package/src/alpha6/milestone.ts +2192 -0
  167. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  168. package/src/alpha6/npm-check-contract.ts +47 -0
  169. package/src/alpha6/plan-integrity.ts +298 -0
  170. package/src/alpha6/plan-risk.ts +1480 -0
  171. package/src/alpha6/preexecution-replan.ts +187 -0
  172. package/src/alpha6/remediation-cause.ts +98 -0
  173. package/src/alpha6/remediation.ts +2438 -0
  174. package/src/alpha6/review.ts +1198 -0
  175. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  176. package/src/alpha6/store-sidecars.ts +335 -0
  177. package/src/alpha7/autonomy.ts +411 -0
  178. package/src/alpha7/corrective-recovery.ts +1332 -0
  179. package/src/artifacts.ts +130 -0
  180. package/src/beta1/project-transaction.ts +355 -0
  181. package/src/change-explanation.ts +153 -0
  182. package/src/checks/runner.ts +245 -0
  183. package/src/checks/task-sync.ts +41 -0
  184. package/src/checks/worker.ts +149 -0
  185. package/src/cli-actions.ts +107 -0
  186. package/src/cli.ts +1457 -0
  187. package/src/contracts.ts +1494 -0
  188. package/src/credential-output.ts +89 -0
  189. package/src/credential-transport.ts +215 -0
  190. package/src/delegation.ts +190 -0
  191. package/src/dependency-provenance.ts +472 -0
  192. package/src/diagnostics.ts +93 -0
  193. package/src/domain/base-sync-conflict.ts +35 -0
  194. package/src/domain/completed-step-carryover.ts +76 -0
  195. package/src/domain/discovery.ts +27 -0
  196. package/src/domain/plan-semantics.ts +58 -0
  197. package/src/domain/step-start-admission.ts +10 -0
  198. package/src/domain/validation.ts +177 -0
  199. package/src/domain/virgin-registration.ts +46 -0
  200. package/src/errors.ts +24 -0
  201. package/src/fs-utils.ts +61 -0
  202. package/src/gateway-handshake.ts +95 -0
  203. package/src/git.ts +183 -0
  204. package/src/graph.ts +342 -0
  205. package/src/historical-step-provenance.ts +136 -0
  206. package/src/index.ts +23 -0
  207. package/src/lifecycle/canonical-hash.ts +28 -0
  208. package/src/lifecycle/catalog.ts +202 -0
  209. package/src/lifecycle/compiler-inspection.ts +29 -0
  210. package/src/lifecycle/core-static-readiness.ts +132 -0
  211. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  212. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  213. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  214. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  215. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  216. package/src/lifecycle/corrective-replan.ts +843 -0
  217. package/src/lifecycle/evaluator.ts +48 -0
  218. package/src/lifecycle/fingerprint.ts +488 -0
  219. package/src/lifecycle/immutable.ts +8 -0
  220. package/src/lifecycle/implementation-table.ts +118 -0
  221. package/src/lifecycle/index.ts +8 -0
  222. package/src/lifecycle/schema-artifact.ts +263 -0
  223. package/src/lifecycle/semantic-registry.ts +572 -0
  224. package/src/lifecycle/types.ts +838 -0
  225. package/src/memory.ts +273 -0
  226. package/src/migration.ts +161 -0
  227. package/src/navigation-actions.ts +70 -0
  228. package/src/navigation-update-artifact.ts +198 -0
  229. package/src/navigation-update.ts +121 -0
  230. package/src/observation.ts +225 -0
  231. package/src/observed-routes.ts +660 -0
  232. package/src/operational-contract.ts +125 -0
  233. package/src/pending-review-update.ts +175 -0
  234. package/src/repository.ts +99 -0
  235. package/src/reviewer.ts +1879 -0
  236. package/src/runtime.ts +6 -0
  237. package/src/state/corrective-replan-executor.ts +818 -0
  238. package/src/state/corrective-replan-public-schema.ts +83 -0
  239. package/src/state/corrective-replan-public.ts +908 -0
  240. package/src/state/corrective-replan-transaction.ts +949 -0
  241. package/src/state/corrective-yield-executor.ts +327 -0
  242. package/src/state/corrective-yield-transaction.ts +730 -0
  243. package/src/state/lock.ts +902 -0
  244. package/src/state/store.ts +567 -0
  245. package/src/transition-core.ts +330 -0
  246. package/src/ulid.ts +24 -0
  247. package/src/version.ts +2 -0
  248. package/src/workflow-blocker-route.ts +109 -0
  249. package/src/workflow.ts +10172 -0
  250. package/docs/alpha7.1-implementation-brief.md +0 -268
  251. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  252. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  253. package/docs/beta1-stabilization-brief.md +0 -165
  254. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  255. package/docs/beta13.2-signal-review-recovery.md +0 -38
  256. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  257. package/docs/change-model.md +0 -118
  258. package/docs/delegated-approval.md +0 -254
  259. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  260. package/docs/pdf/README.md +0 -24
  261. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  262. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  263. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  264. package/docs/pdf/requirements.txt +0 -1
  265. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  266. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -508
  267. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  268. package/docs/pending-review-update.md +0 -15
  269. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  270. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  271. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  272. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  273. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  274. package/docs/problem-briefs/README.md +0 -120
  275. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  276. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  277. package/docs/split-required-recovery.md +0 -47
  278. package/docs/stable-release-defect-register.md +0 -730
  279. package/docs/validation-report.md +0 -182
  280. package/scripts/generate-pdf-docs.py +0 -524
  281. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,949 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ existsSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ readdirSync,
12
+ renameSync,
13
+ rmSync,
14
+ writeSync,
15
+ } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { Ajv2020 } from 'ajv/dist/2020.js';
18
+ import type {
19
+ CorruptObservedTaskTransaction,
20
+ ObservedTaskTransaction,
21
+ } from '../contracts.js';
22
+ import { WorkflowError } from '../errors.js';
23
+ import { assertNoSymlinkPathComponents } from '../alpha6/store-sidecars.js';
24
+ import { deepFreeze } from '../lifecycle/immutable.js';
25
+ import {
26
+ correctiveReplanBindingManifest,
27
+ historicalCompletedCorrectiveReplanBindingIdentities,
28
+ } from '../lifecycle/corrective-replan-binding-manifest.js';
29
+ import {
30
+ assertCoreTaskOperationHandle,
31
+ type CoreTaskOperationHandle,
32
+ } from './lock.js';
33
+
34
+ const HASH = /^[a-f0-9]{64}$/;
35
+ const PROJECT = /^[a-f0-9]{24}$/;
36
+ const TASK = /^TASK-[0-9A-HJKMNP-TV-Z]{26}$/;
37
+ const MAX_TARGET_BYTES = 256 * 1024 * 1024;
38
+ const schema = JSON.parse(readFileSync(
39
+ new URL('../../../schemas/corrective-replan-task-transaction.private.schema.json', import.meta.url),
40
+ 'utf8',
41
+ )) as object;
42
+ const ajv = new Ajv2020({ allErrors: true, strict: true, formats: { 'date-time': true } });
43
+ if (!ajv.validateSchema(schema)) throw new TypeError(`Invalid private Task transaction schema: ${ajv.errorsText(ajv.errors)}`);
44
+ const validateSchema = ajv.compile(schema);
45
+
46
+ export const CORRECTIVE_REPLAN_TARGETS = deepFreeze([
47
+ { targetId: 'task-state', relativePath: 'state.json', writeMode: 'replace-exact' },
48
+ { targetId: 'plan', relativePath: 'plan.md', writeMode: 'replace-exact' },
49
+ { targetId: 'plan-validation', relativePath: 'plan-validations.jsonl', writeMode: 'append-exact' },
50
+ { targetId: 'plan-risk-audit', relativePath: 'plan-risk-audits.jsonl', writeMode: 'append-exact' },
51
+ { targetId: 'execution-evidence', relativePath: 'corrective-replan-executions.jsonl', writeMode: 'append-exact' },
52
+ ] as const);
53
+
54
+ export type CorrectiveReplanTransactionPhase = 'intent' | 'applying' | 'published-pending-readback' | 'completed';
55
+ type TargetId = typeof CORRECTIVE_REPLAN_TARGETS[number]['targetId'];
56
+
57
+ export interface CorrectiveReplanTargetRecord {
58
+ targetId: TargetId;
59
+ ordinal: number;
60
+ relativePath: string;
61
+ writeMode: 'replace-exact' | 'append-exact';
62
+ preimage: { exists: boolean; contentSha256: string | null; byteLength: number; contentPath: string | null };
63
+ postimage: { contentSha256: string; byteLength: number; contentPath: string };
64
+ sidecarAnchors: null | {
65
+ previousEventHash: string | null;
66
+ resultEventHash: string;
67
+ previousLineCount: number;
68
+ resultLineCount: number;
69
+ };
70
+ taskCas: null | { expectedRevision: number; resultRevision: number };
71
+ }
72
+
73
+ export interface CorrectiveReplanTaskTransactionJournal {
74
+ schemaVersion: 1;
75
+ kind: 'corrective-replan-task-transaction';
76
+ transactionId: string;
77
+ projectId: string;
78
+ taskId: string;
79
+ transitionId: 'task.plan.corrective-replan';
80
+ executionKeyHash: string;
81
+ runtimeFingerprint: string;
82
+ catalogHash: string;
83
+ lifecycleEpoch: number;
84
+ bindingManifest: { id: 'binding-manifest.corrective-replan.exact-v1'; version: 1; manifestSha256: string };
85
+ bindingSha256: string;
86
+ receiptHash: string;
87
+ actorIdentityHash: string;
88
+ humanPrincipalIdentityHash: string;
89
+ admittedAt: string;
90
+ intentSha256: string;
91
+ phase: CorrectiveReplanTransactionPhase;
92
+ phaseRevision: number;
93
+ nextTargetOrdinal: number;
94
+ targets: CorrectiveReplanTargetRecord[];
95
+ phaseHistory: Array<{
96
+ sequence: number;
97
+ phase: CorrectiveReplanTransactionPhase;
98
+ nextTargetOrdinal: number;
99
+ recordedAt: string;
100
+ previousEntryHash: string | null;
101
+ entryHash: string;
102
+ }>;
103
+ completedResult: null | { resultSha256: string };
104
+ updatedAt: string;
105
+ }
106
+
107
+ export interface CreateCorrectiveReplanIntentInput {
108
+ projectId: string;
109
+ taskId: string;
110
+ transitionId: 'task.plan.corrective-replan';
111
+ executionKeyHash: string;
112
+ runtimeFingerprint: string;
113
+ catalogHash: string;
114
+ lifecycleEpoch: number;
115
+ bindingManifest: { id: 'binding-manifest.corrective-replan.exact-v1'; version: 1; manifestSha256: string };
116
+ bindingSha256: string;
117
+ receiptHash: string;
118
+ actorIdentityHash: string;
119
+ humanPrincipalIdentityHash: string;
120
+ expectedTaskRevision: number;
121
+ resultTaskRevision: number;
122
+ postimages: {
123
+ taskState: Uint8Array;
124
+ plan: Uint8Array;
125
+ planValidation: Uint8Array;
126
+ planRiskAudit: Uint8Array;
127
+ executionEvidence: Uint8Array;
128
+ };
129
+ }
130
+
131
+ interface StoreOptions {
132
+ taskRoot: string;
133
+ locksRoot: string;
134
+ trustedRoot: string;
135
+ now?: () => Date;
136
+ }
137
+
138
+ export class CorrectiveReplanTaskTransactionStore {
139
+ private readonly taskRoot: string;
140
+ private readonly locksRoot: string;
141
+ private readonly trustedRoot: string;
142
+ private readonly now: () => Date;
143
+
144
+ constructor(options: StoreOptions) {
145
+ this.taskRoot = path.resolve(options.taskRoot);
146
+ this.locksRoot = path.resolve(options.locksRoot);
147
+ this.trustedRoot = path.resolve(options.trustedRoot);
148
+ this.now = options.now ?? (() => new Date());
149
+ assertTaskRoot(this.taskRoot, this.trustedRoot);
150
+ }
151
+
152
+ createIntent(handle: unknown, input: CreateCorrectiveReplanIntentInput): CorrectiveReplanTaskTransactionJournal {
153
+ assertPlainExact(input, [
154
+ 'actorIdentityHash', 'bindingManifest', 'bindingSha256', 'catalogHash', 'executionKeyHash',
155
+ 'expectedTaskRevision', 'humanPrincipalIdentityHash', 'lifecycleEpoch', 'postimages', 'projectId', 'receiptHash',
156
+ 'resultTaskRevision', 'runtimeFingerprint', 'taskId', 'transitionId',
157
+ ], 'Corrective-replan intent');
158
+ assertHeader(input);
159
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
160
+ entityId: input.taskId,
161
+ operationId: input.executionKeyHash,
162
+ transitionId: input.transitionId,
163
+ });
164
+ const postimages = normalizePostimages(input.postimages);
165
+ const preimages = CORRECTIVE_REPLAN_TARGETS.map((target) => readTarget(this.taskRoot, target.relativePath));
166
+ validateTaskStatePair(
167
+ preimages[0]!.content,
168
+ postimages[0]!,
169
+ input.projectId,
170
+ input.taskId,
171
+ input.expectedTaskRevision,
172
+ input.resultTaskRevision,
173
+ );
174
+ if (postimages[1]!.byteLength === 0) throw new WorkflowError('INVALID_ARGUMENT', 'Replacement Plan bytes must not be empty.');
175
+ const sidecars = [2, 3, 4].map((ordinal) => sidecarAnchors(preimages[ordinal]!.content, postimages[ordinal]!));
176
+ const transactionRoot = this.transactionRoot(input.executionKeyHash);
177
+ if (existsSync(transactionRoot)) {
178
+ throw new WorkflowError('STATE_CONFLICT', 'Corrective-replan transaction already exists.', {
179
+ transactionId: input.executionKeyHash,
180
+ });
181
+ }
182
+ const targets: CorrectiveReplanTargetRecord[] = CORRECTIVE_REPLAN_TARGETS.map((target, ordinal) => ({
183
+ ...target,
184
+ ordinal,
185
+ preimage: snapshot(preimages[ordinal]!.content, ordinal),
186
+ postimage: {
187
+ contentSha256: sha256(postimages[ordinal]!),
188
+ byteLength: postimages[ordinal]!.byteLength,
189
+ contentPath: `targets/${ordinal}.bin`,
190
+ },
191
+ sidecarAnchors: ordinal < 2 ? null : sidecars[ordinal - 2]!,
192
+ taskCas: ordinal === 0
193
+ ? { expectedRevision: input.expectedTaskRevision, resultRevision: input.resultTaskRevision }
194
+ : null,
195
+ }));
196
+ const admittedAt = this.now().toISOString();
197
+ const journal: CorrectiveReplanTaskTransactionJournal = {
198
+ schemaVersion: 1,
199
+ kind: 'corrective-replan-task-transaction',
200
+ transactionId: input.executionKeyHash,
201
+ projectId: input.projectId,
202
+ taskId: input.taskId,
203
+ transitionId: input.transitionId,
204
+ executionKeyHash: input.executionKeyHash,
205
+ runtimeFingerprint: input.runtimeFingerprint,
206
+ catalogHash: input.catalogHash,
207
+ lifecycleEpoch: input.lifecycleEpoch,
208
+ bindingManifest: {
209
+ id: correctiveReplanBindingManifest.id,
210
+ version: correctiveReplanBindingManifest.version,
211
+ manifestSha256: correctiveReplanBindingManifest.manifestSha256,
212
+ },
213
+ bindingSha256: input.bindingSha256,
214
+ receiptHash: input.receiptHash,
215
+ actorIdentityHash: input.actorIdentityHash,
216
+ humanPrincipalIdentityHash: input.humanPrincipalIdentityHash,
217
+ admittedAt,
218
+ intentSha256: '0'.repeat(64),
219
+ phase: 'intent',
220
+ phaseRevision: 1,
221
+ nextTargetOrdinal: 0,
222
+ targets,
223
+ phaseHistory: [],
224
+ completedResult: null,
225
+ updatedAt: admittedAt,
226
+ };
227
+ journal.intentSha256 = intentHash(journal);
228
+ journal.phaseHistory = [phaseEntry(0, 'intent', 0, admittedAt, null, journal.intentSha256)];
229
+ validateJournal(journal, this.taskRoot, false, input.executionKeyHash);
230
+ ensureDirectorySafe(path.dirname(transactionRoot), this.trustedRoot);
231
+ let createdTransactionRoot = false;
232
+ try {
233
+ mkdirSync(transactionRoot, { mode: 0o700 });
234
+ createdTransactionRoot = true;
235
+ mkdirSync(path.join(transactionRoot, 'preimages'), { mode: 0o700 });
236
+ mkdirSync(path.join(transactionRoot, 'targets'), { mode: 0o700 });
237
+ preimages.forEach(({ content }, ordinal) => {
238
+ if (content !== null) writeNewDurable(path.join(transactionRoot, 'preimages', `${ordinal}.bin`), content);
239
+ });
240
+ postimages.forEach((content, ordinal) => writeNewDurable(path.join(transactionRoot, 'targets', `${ordinal}.bin`), content));
241
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
242
+ entityId: input.taskId, operationId: input.executionKeyHash, transitionId: input.transitionId,
243
+ });
244
+ writeNewDurable(this.journalPath(input.executionKeyHash), jsonBuffer(journal));
245
+ fsyncDirectory(transactionRoot);
246
+ return this.read(input.executionKeyHash);
247
+ } catch (error) {
248
+ if (createdTransactionRoot && !existsSync(this.journalPath(input.executionKeyHash))) {
249
+ rmSync(transactionRoot, { recursive: true, force: true });
250
+ }
251
+ if (isNodeError(error) && error.code === 'EEXIST') {
252
+ throw new WorkflowError('STATE_CONFLICT', 'Corrective-replan transaction already exists.', {
253
+ transactionId: input.executionKeyHash,
254
+ });
255
+ }
256
+ throw error;
257
+ }
258
+ }
259
+
260
+ read(transactionId: string): CorrectiveReplanTaskTransactionJournal {
261
+ return this.readValidated(transactionId, true);
262
+ }
263
+
264
+ readForObservation(transactionId: string): CorrectiveReplanTaskTransactionJournal {
265
+ return this.readValidated(transactionId, false);
266
+ }
267
+
268
+ private readValidated(
269
+ transactionId: string,
270
+ requireCanonicalTargets: boolean,
271
+ ): CorrectiveReplanTaskTransactionJournal {
272
+ assertHash(transactionId, 'Transaction id');
273
+ const file = this.journalPath(transactionId);
274
+ assertNoSymlinkPathComponents(file, { label: 'Task transaction journal', trustedRoot: this.trustedRoot, requireLeaf: true });
275
+ if (!existsSync(file)) throw new WorkflowError('NOT_FOUND', `Task transaction journal not found: ${transactionId}`);
276
+ let journal: unknown;
277
+ try {
278
+ journal = JSON.parse(readFileSync(file, 'utf8'));
279
+ } catch {
280
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction journal is not valid JSON.');
281
+ }
282
+ validateJournal(journal, this.taskRoot, true, transactionId, requireCanonicalTargets);
283
+ return deepFreeze(structuredClone(journal as CorrectiveReplanTaskTransactionJournal));
284
+ }
285
+
286
+ journalPath(transactionId: string): string {
287
+ assertHash(transactionId, 'Transaction id');
288
+ return path.join(this.transactionRoot(transactionId), 'journal.json');
289
+ }
290
+
291
+ readOwnedPostimage(transactionId: string, requested: Pick<CorrectiveReplanTargetRecord, 'targetId'>): Buffer {
292
+ const journal = this.read(transactionId);
293
+ const target = journal.targets.find((row) => row.targetId === requested.targetId);
294
+ if (!target) throw new WorkflowError('INVALID_ARGUMENT', 'Unknown corrective-replan target.');
295
+ return readOwnedContent(this.taskRoot, transactionId, target);
296
+ }
297
+
298
+ readOwnedPreimage(
299
+ transactionId: string,
300
+ requested: Pick<CorrectiveReplanTargetRecord, 'targetId'>,
301
+ ): Buffer | null {
302
+ const journal = this.read(transactionId);
303
+ const target = journal.targets.find((row) => row.targetId === requested.targetId);
304
+ if (!target) throw new WorkflowError('INVALID_ARGUMENT', 'Unknown corrective-replan target.');
305
+ return readOwnedPreimage(this.taskRoot, transactionId, target);
306
+ }
307
+
308
+ captureCurrentTarget(
309
+ handle: unknown,
310
+ input: { taskId: string; operationId: string; targetId: TargetId },
311
+ ): Buffer | null {
312
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
313
+ entityId: input.taskId,
314
+ operationId: input.operationId,
315
+ transitionId: 'task.plan.corrective-replan',
316
+ });
317
+ const target = CORRECTIVE_REPLAN_TARGETS.find((candidate) => candidate.targetId === input.targetId);
318
+ if (!target) throw new WorkflowError('INVALID_ARGUMENT', 'Unknown corrective-replan target.');
319
+ const content = readTarget(this.taskRoot, target.relativePath).content;
320
+ return content === null ? null : Buffer.from(content);
321
+ }
322
+
323
+ compareAndSetPhase(
324
+ handle: unknown,
325
+ transactionId: string,
326
+ input: {
327
+ expectedPhase: CorrectiveReplanTransactionPhase;
328
+ expectedRevision: number;
329
+ nextPhase: CorrectiveReplanTransactionPhase;
330
+ nextTargetOrdinal: number;
331
+ completedResultSha256?: string;
332
+ },
333
+ ): CorrectiveReplanTaskTransactionJournal {
334
+ const current = this.read(transactionId);
335
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
336
+ entityId: current.taskId, operationId: current.executionKeyHash, transitionId: current.transitionId,
337
+ });
338
+ if (current.phase !== input.expectedPhase || current.phaseRevision !== input.expectedRevision) {
339
+ throw new WorkflowError('STATE_CONFLICT', 'Task transaction phase compare-and-set conflict.');
340
+ }
341
+ assertPhaseTransition(current, input.nextPhase, input.nextTargetOrdinal);
342
+ if (current.phase === 'applying' && input.nextPhase === 'applying' && input.nextTargetOrdinal > 0) {
343
+ assertCanonicalTargetMatches(this.taskRoot, current.targets[input.nextTargetOrdinal - 1]!);
344
+ }
345
+ if (input.nextPhase === 'completed') {
346
+ assertHash(input.completedResultSha256, 'Completed Task transaction result hash');
347
+ assertCanonicalTargetsMatch(this.taskRoot, current);
348
+ } else if (input.completedResultSha256 !== undefined) {
349
+ throw new WorkflowError('INVALID_ARGUMENT', 'Only a completed Task transaction may bind a result hash.');
350
+ }
351
+ const previous = current.phaseHistory.at(-1)!;
352
+ const recordedAt = this.now().toISOString();
353
+ const nextEntry = phaseEntry(
354
+ current.phaseRevision,
355
+ input.nextPhase,
356
+ input.nextTargetOrdinal,
357
+ recordedAt,
358
+ previous.entryHash,
359
+ current.intentSha256,
360
+ );
361
+ const next: CorrectiveReplanTaskTransactionJournal = {
362
+ ...structuredClone(current),
363
+ phase: input.nextPhase,
364
+ phaseRevision: current.phaseRevision + 1,
365
+ nextTargetOrdinal: input.nextTargetOrdinal,
366
+ phaseHistory: [...current.phaseHistory, nextEntry],
367
+ completedResult: input.nextPhase === 'completed'
368
+ ? { resultSha256: input.completedResultSha256! }
369
+ : current.completedResult,
370
+ updatedAt: recordedAt,
371
+ };
372
+ validateJournal(next, this.taskRoot, true, transactionId);
373
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
374
+ entityId: current.taskId, operationId: current.executionKeyHash, transitionId: current.transitionId,
375
+ });
376
+ writeAtomicDurable(this.journalPath(transactionId), jsonBuffer(next));
377
+ return this.read(transactionId);
378
+ }
379
+
380
+ applyTarget(
381
+ handle: unknown,
382
+ transactionId: string,
383
+ targetId: TargetId,
384
+ ): 'applied' | 'confirmed' {
385
+ const journal = this.read(transactionId);
386
+ if (journal.phase !== 'applying') {
387
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Target application requires an applying transaction.');
388
+ }
389
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
390
+ entityId: journal.taskId, operationId: journal.executionKeyHash, transitionId: journal.transitionId,
391
+ });
392
+ const target = journal.targets.find((candidate) => candidate.targetId === targetId);
393
+ if (!target || target.ordinal !== journal.nextTargetOrdinal) {
394
+ throw new WorkflowError('INVALID_ARGUMENT', 'Task transaction target is not the exact next target.');
395
+ }
396
+ const file = path.join(this.taskRoot, target.relativePath);
397
+ const current = readTarget(this.taskRoot, target.relativePath).content;
398
+ const currentHash = current === null ? null : sha256(current);
399
+ if (currentHash === target.postimage.contentSha256) return 'confirmed';
400
+ if (currentHash !== target.preimage.contentSha256) {
401
+ throw new WorkflowError('STATE_CONFLICT', 'Task target found neither the exact preimage nor exact result.', {
402
+ targetId,
403
+ });
404
+ }
405
+ const result = readOwnedContent(this.taskRoot, transactionId, target);
406
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
407
+ entityId: journal.taskId, operationId: journal.executionKeyHash, transitionId: journal.transitionId,
408
+ });
409
+ writeAtomicDurable(file, result);
410
+ if (sha256(readFileSync(file)) !== target.postimage.contentSha256) {
411
+ throw new WorkflowError('STATE_CORRUPT', 'Task target readback mismatch.', { targetId });
412
+ }
413
+ return 'applied';
414
+ }
415
+
416
+ applyTaskStateCas(handle: unknown, transactionId: string): 'applied' | 'confirmed' {
417
+ return this.applyTarget(handle, transactionId, 'task-state');
418
+ }
419
+
420
+ readCanonicalTarget(handle: unknown, transactionId: string, targetId: TargetId): Buffer {
421
+ const journal = this.read(transactionId);
422
+ assertCoreTaskOperationHandle(this.locksRoot, this.trustedRoot, handle, this.now(), {
423
+ entityId: journal.taskId,
424
+ operationId: journal.executionKeyHash,
425
+ transitionId: journal.transitionId,
426
+ });
427
+ const target = journal.targets.find((candidate) => candidate.targetId === targetId);
428
+ if (!target) throw new WorkflowError('INVALID_ARGUMENT', 'Unknown corrective-replan target.');
429
+ assertCanonicalTargetMatches(this.taskRoot, target);
430
+ return Buffer.from(readTarget(this.taskRoot, target.relativePath).content!);
431
+ }
432
+
433
+ readCompletedTarget(transactionId: string, targetId: TargetId): Buffer {
434
+ const journal = this.read(transactionId);
435
+ if (journal.phase !== 'completed') {
436
+ throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'Task transaction is not completed.');
437
+ }
438
+ const target = journal.targets.find((candidate) => candidate.targetId === targetId);
439
+ if (!target) throw new WorkflowError('INVALID_ARGUMENT', 'Unknown corrective-replan target.');
440
+ return Buffer.from(readTarget(this.taskRoot, target.relativePath).content!);
441
+ }
442
+
443
+ private transactionRoot(transactionId: string): string {
444
+ assertHash(transactionId, 'Transaction id');
445
+ return path.join(this.taskRoot, '.transactions', transactionId);
446
+ }
447
+ }
448
+
449
+ export function inspectCorrectiveReplanTaskTransactions(
450
+ projectRoot: string,
451
+ trustedRoot: string,
452
+ ): { pending: ObservedTaskTransaction[]; completed: ObservedTaskTransaction[]; corrupt: CorruptObservedTaskTransaction[] } {
453
+ const resolvedProject = path.resolve(projectRoot);
454
+ const resolvedTrusted = path.resolve(trustedRoot);
455
+ if (!existsSync(resolvedTrusted) || !existsSync(resolvedProject)) {
456
+ return deepFreeze({ pending: [], completed: [], corrupt: [] });
457
+ }
458
+ assertNoSymlinkPathComponents(resolvedProject, { label: 'Project transaction observation root', trustedRoot: resolvedTrusted });
459
+ const tasksRoot = path.join(resolvedProject, 'tasks');
460
+ if (!existsSync(tasksRoot)) return deepFreeze({ pending: [], completed: [], corrupt: [] });
461
+ assertNoSymlinkPathComponents(tasksRoot, { label: 'Task transaction observation root', trustedRoot: resolvedTrusted, requireLeaf: true });
462
+ const pending: ObservedTaskTransaction[] = [];
463
+ const completed: ObservedTaskTransaction[] = [];
464
+ const corrupt: CorruptObservedTaskTransaction[] = [];
465
+ for (const taskEntry of readdirSync(tasksRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
466
+ const taskPath = path.join(tasksRoot, taskEntry.name);
467
+ if (taskEntry.isSymbolicLink()) throw new WorkflowError('STATE_CORRUPT', 'Task observation root must not be a symlink.');
468
+ if (!taskEntry.isDirectory() || !TASK.test(taskEntry.name)) continue;
469
+ const transactionRoot = path.join(taskPath, '.transactions');
470
+ if (!existsSync(transactionRoot)) continue;
471
+ assertNoSymlinkPathComponents(transactionRoot, { label: 'Task transaction directory', trustedRoot: resolvedTrusted, requireLeaf: true });
472
+ for (const entry of readdirSync(transactionRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
473
+ if (entry.isSymbolicLink()) throw new WorkflowError('STATE_CORRUPT', 'Task transaction entry must not be a symlink.');
474
+ if (!entry.isDirectory()) continue;
475
+ const transactionId = HASH.test(entry.name) ? entry.name : 'invalid-transaction-id';
476
+ if (!HASH.test(entry.name)) {
477
+ corrupt.push({ taskId: taskEntry.name, transactionId, reason: 'journal-invalid' });
478
+ continue;
479
+ }
480
+ try {
481
+ const store = new CorrectiveReplanTaskTransactionStore({
482
+ taskRoot: taskPath,
483
+ locksRoot: path.join(resolvedProject, 'locks'),
484
+ trustedRoot: resolvedTrusted,
485
+ });
486
+ const journal = store.readForObservation(entry.name);
487
+ (journal.phase === 'completed' ? completed : pending).push({
488
+ transitionId: journal.transitionId,
489
+ taskId: journal.taskId,
490
+ transactionId: journal.transactionId,
491
+ phase: journal.phase,
492
+ phaseRevision: journal.phaseRevision,
493
+ updatedAt: journal.updatedAt,
494
+ });
495
+ } catch {
496
+ corrupt.push({ taskId: taskEntry.name, transactionId, reason: 'journal-invalid' });
497
+ }
498
+ }
499
+ }
500
+ return deepFreeze({
501
+ completed: completed.sort((a, b) => a.taskId.localeCompare(b.taskId) || a.transactionId.localeCompare(b.transactionId)),
502
+ pending: pending.sort((a, b) => a.taskId.localeCompare(b.taskId) || a.transactionId.localeCompare(b.transactionId)),
503
+ corrupt: corrupt.sort((a, b) => a.taskId.localeCompare(b.taskId) || a.transactionId.localeCompare(b.transactionId)),
504
+ });
505
+ }
506
+
507
+ export function assertTaskTransactionBoundary(
508
+ projectRoot: string,
509
+ trustedRoot: string,
510
+ taskId?: string,
511
+ ): void {
512
+ const observed = inspectCorrectiveReplanTaskTransactions(projectRoot, trustedRoot);
513
+ const corrupt = taskId === undefined
514
+ ? observed.corrupt
515
+ : observed.corrupt.filter((transaction) => transaction.taskId === taskId);
516
+ if (corrupt.length > 0) {
517
+ throw new WorkflowError('STATE_CORRUPT', 'A Task transaction journal failed integrity validation.', {
518
+ transactions: corrupt,
519
+ });
520
+ }
521
+ const pending = taskId === undefined
522
+ ? observed.pending
523
+ : observed.pending.filter((transaction) => transaction.taskId === taskId);
524
+ if (pending.length > 0) {
525
+ throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'A Task transaction requires explicit recovery.', {
526
+ taskIds: [...new Set(pending.map((transaction) => transaction.taskId))].sort(),
527
+ transactionIds: pending.map((transaction) => transaction.transactionId).sort(),
528
+ });
529
+ }
530
+ }
531
+
532
+ function assertHeader(input: CreateCorrectiveReplanIntentInput): void {
533
+ if (!PROJECT.test(input.projectId) || !TASK.test(input.taskId) || input.transitionId !== 'task.plan.corrective-replan') {
534
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective-replan intent identity is invalid.');
535
+ }
536
+ [input.executionKeyHash, input.runtimeFingerprint, input.catalogHash, input.bindingManifest?.manifestSha256,
537
+ input.bindingSha256, input.receiptHash, input.actorIdentityHash, input.humanPrincipalIdentityHash]
538
+ .forEach((value) => assertHash(value, 'Corrective-replan binding hash'));
539
+ if (input.bindingManifest?.id !== correctiveReplanBindingManifest.id
540
+ || input.bindingManifest.version !== correctiveReplanBindingManifest.version
541
+ || input.bindingManifest.manifestSha256 !== correctiveReplanBindingManifest.manifestSha256) {
542
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective-replan binding manifest identity is invalid.');
543
+ }
544
+ if (!Number.isSafeInteger(input.lifecycleEpoch) || input.lifecycleEpoch < 1
545
+ || !Number.isSafeInteger(input.expectedTaskRevision) || input.expectedTaskRevision < 1
546
+ || input.resultTaskRevision !== input.expectedTaskRevision + 1) {
547
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective-replan revision or epoch is invalid.');
548
+ }
549
+ }
550
+
551
+ function normalizePostimages(value: CreateCorrectiveReplanIntentInput['postimages']): Buffer[] {
552
+ assertPlainExact(value, ['executionEvidence', 'plan', 'planRiskAudit', 'planValidation', 'taskState'], 'Postimages');
553
+ return [value.taskState, value.plan, value.planValidation, value.planRiskAudit, value.executionEvidence].map((item) => {
554
+ if (!(item instanceof Uint8Array) || item.byteLength === 0 || item.byteLength > MAX_TARGET_BYTES) {
555
+ throw new WorkflowError('INVALID_ARGUMENT', 'Each corrective-replan postimage must contain bounded owned bytes.');
556
+ }
557
+ return Buffer.from(item);
558
+ });
559
+ }
560
+
561
+ function validateTaskStatePair(
562
+ preimage: Buffer | null,
563
+ postimage: Buffer,
564
+ projectId: string,
565
+ taskId: string,
566
+ expectedTaskRevision: number,
567
+ resultTaskRevision: number,
568
+ ): void {
569
+ if (!preimage) throw new WorkflowError('STATE_CONFLICT', 'Corrective-replan Task preimage is missing.');
570
+ const before = parseJsonRecord(preimage, 'Task preimage');
571
+ const after = parseJsonRecord(postimage, 'Task postimage');
572
+ for (const value of [before, after]) {
573
+ if (value.kind !== 'task' || value.projectId !== projectId || value.id !== taskId) {
574
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective-replan Task bytes have the wrong identity.');
575
+ }
576
+ }
577
+ if (before.revision !== expectedTaskRevision || after.revision !== resultTaskRevision) {
578
+ throw new WorkflowError('STATE_CONFLICT', 'Corrective-replan Task bytes have the wrong revision CAS.');
579
+ }
580
+ }
581
+
582
+ function sidecarAnchors(preimage: Buffer | null, result: Buffer): NonNullable<CorrectiveReplanTargetRecord['sidecarAnchors']> {
583
+ const before = jsonLines(preimage);
584
+ const after = jsonLines(result);
585
+ if (after.length !== before.length + 1 || (preimage && !result.subarray(0, preimage.byteLength).equals(preimage))) {
586
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective-replan sidecar postimage must append exactly one event.');
587
+ }
588
+ const previousEventHash = before.length === 0 ? null : eventHash(before.at(-1)!);
589
+ const appended = after.at(-1)!;
590
+ const resultEventHash = eventHash(appended);
591
+ if (appended.previousEventHash !== previousEventHash) {
592
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective-replan sidecar event does not bind the current tail.');
593
+ }
594
+ return { previousEventHash, resultEventHash, previousLineCount: before.length, resultLineCount: after.length };
595
+ }
596
+
597
+ function jsonLines(content: Buffer | null): Array<Record<string, unknown>> {
598
+ if (!content || content.byteLength === 0) return [];
599
+ const text = content.toString('utf8');
600
+ if (!text.endsWith('\n')) throw new WorkflowError('INVALID_ARGUMENT', 'Sidecar bytes must end with a newline.');
601
+ return text.slice(0, -1).split('\n').map((line) => parseJsonRecord(Buffer.from(line), 'Sidecar event'));
602
+ }
603
+
604
+ function eventHash(event: Record<string, unknown>): string {
605
+ assertHash(event.eventHash, 'Sidecar eventHash');
606
+ return event.eventHash;
607
+ }
608
+
609
+ function validateJournal(
610
+ value: unknown,
611
+ taskRoot: string,
612
+ verifyOwnedContent: boolean,
613
+ expectedTransactionId: string,
614
+ requireCanonicalTargets = true,
615
+ ): asserts value is CorrectiveReplanTaskTransactionJournal {
616
+ if (validateSchema(value) !== true) throw new WorkflowError('STATE_CORRUPT', 'Task transaction journal schema is invalid.');
617
+ const journal = value as CorrectiveReplanTaskTransactionJournal;
618
+ assertStoredIso(journal.admittedAt);
619
+ assertStoredIso(journal.updatedAt);
620
+ if (journal.transactionId !== journal.executionKeyHash || journal.transactionId !== expectedTransactionId
621
+ || journal.taskId !== path.basename(taskRoot)) {
622
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction path identity mismatch.');
623
+ }
624
+ if (!hasSupportedBindingIdentity(journal)) {
625
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction binding manifest mismatch.');
626
+ }
627
+ if (journal.intentSha256 !== intentHash(journal)) throw new WorkflowError('STATE_CORRUPT', 'Task transaction intent hash mismatch.');
628
+ CORRECTIVE_REPLAN_TARGETS.forEach((expected, ordinal) => {
629
+ const actual = journal.targets[ordinal];
630
+ if (!actual || actual.targetId !== expected.targetId || actual.ordinal !== ordinal
631
+ || actual.relativePath !== expected.relativePath || actual.writeMode !== expected.writeMode
632
+ || (ordinal === 0) !== (actual.taskCas !== null) || (ordinal >= 2) !== (actual.sidecarAnchors !== null)) {
633
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction target inventory mismatch.');
634
+ }
635
+ if (actual.preimage.exists !== (actual.preimage.contentPath !== null)
636
+ || actual.preimage.exists !== (actual.preimage.contentSha256 !== null)
637
+ || actual.preimage.contentPath !== (actual.preimage.exists ? `preimages/${ordinal}.bin` : null)) {
638
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction preimage ownership mismatch.');
639
+ }
640
+ if (verifyOwnedContent) {
641
+ const preimage = readOwnedPreimage(taskRoot, journal.transactionId, actual);
642
+ const postimage = readOwnedContent(taskRoot, journal.transactionId, actual);
643
+ if (ordinal === 0) {
644
+ validateTaskStatePair(
645
+ preimage,
646
+ postimage,
647
+ journal.projectId,
648
+ journal.taskId,
649
+ actual.taskCas!.expectedRevision,
650
+ actual.taskCas!.resultRevision,
651
+ );
652
+ }
653
+ if (ordinal >= 2) {
654
+ const anchors = sidecarAnchors(preimage, postimage);
655
+ if (JSON.stringify(anchors) !== JSON.stringify(actual.sidecarAnchors)) {
656
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction sidecar anchors mismatch.');
657
+ }
658
+ }
659
+ }
660
+ });
661
+ if (journal.phaseHistory.length !== journal.phaseRevision) throw new WorkflowError('STATE_CORRUPT', 'Task transaction phase revision mismatch.');
662
+ journal.phaseHistory.forEach((entry, index) => {
663
+ assertStoredIso(entry.recordedAt);
664
+ const previous = index === 0 ? null : journal.phaseHistory[index - 1]!.entryHash;
665
+ if (index > 0 && entry.recordedAt < journal.phaseHistory[index - 1]!.recordedAt) {
666
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction phase time moved backwards.');
667
+ }
668
+ if (entry.sequence !== index || entry.previousEntryHash !== previous
669
+ || entry.entryHash !== phaseEntryHash(
670
+ entry.sequence,
671
+ entry.phase,
672
+ entry.nextTargetOrdinal,
673
+ entry.recordedAt,
674
+ previous,
675
+ journal.intentSha256,
676
+ )) {
677
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction phase hash chain mismatch.');
678
+ }
679
+ if (index === 0 && (entry.phase !== 'intent' || entry.nextTargetOrdinal !== 0 || entry.recordedAt !== journal.admittedAt)) {
680
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction phase history must start at intent.');
681
+ }
682
+ if (index > 0 && !isStoredPhaseTransition(journal.phaseHistory[index - 1]!, entry)) {
683
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction phase topology mismatch.');
684
+ }
685
+ });
686
+ const last = journal.phaseHistory.at(-1)!;
687
+ if (last.phase !== journal.phase || last.nextTargetOrdinal !== journal.nextTargetOrdinal || last.recordedAt !== journal.updatedAt) {
688
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction current phase projection mismatch.');
689
+ }
690
+ if ((journal.phase === 'completed') !== (journal.completedResult !== null)) {
691
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction completion result mismatch.');
692
+ }
693
+ if (journal.phaseRevision > 9) throw new WorkflowError('STATE_CORRUPT', 'Task transaction phase cardinality mismatch.');
694
+ if (verifyOwnedContent && requireCanonicalTargets && journal.phase === 'completed') {
695
+ assertCanonicalTargetsMatch(taskRoot, journal);
696
+ }
697
+ }
698
+
699
+ function hasSupportedBindingIdentity(journal: CorrectiveReplanTaskTransactionJournal): boolean {
700
+ if (journal.bindingManifest.id !== correctiveReplanBindingManifest.id
701
+ || journal.bindingManifest.version !== correctiveReplanBindingManifest.version) return false;
702
+ if (journal.bindingManifest.manifestSha256 === correctiveReplanBindingManifest.manifestSha256) return true;
703
+ return journal.phase === 'completed' && historicalCompletedCorrectiveReplanBindingIdentities.some((identity) =>
704
+ identity.manifestSha256 === journal.bindingManifest.manifestSha256
705
+ && identity.runtimeFingerprint === journal.runtimeFingerprint
706
+ && identity.catalogHash === journal.catalogHash);
707
+ }
708
+
709
+ function isStoredPhaseTransition(
710
+ previous: CorrectiveReplanTaskTransactionJournal['phaseHistory'][number],
711
+ next: CorrectiveReplanTaskTransactionJournal['phaseHistory'][number],
712
+ ): boolean {
713
+ return previous.phase === 'intent' && previous.nextTargetOrdinal === 0
714
+ && next.phase === 'applying' && next.nextTargetOrdinal === 0
715
+ || previous.phase === 'applying' && next.phase === 'applying'
716
+ && next.nextTargetOrdinal === previous.nextTargetOrdinal + 1
717
+ && next.nextTargetOrdinal <= CORRECTIVE_REPLAN_TARGETS.length
718
+ || previous.phase === 'applying' && previous.nextTargetOrdinal === CORRECTIVE_REPLAN_TARGETS.length
719
+ && next.phase === 'published-pending-readback' && next.nextTargetOrdinal === CORRECTIVE_REPLAN_TARGETS.length
720
+ || previous.phase === 'published-pending-readback' && next.phase === 'completed'
721
+ && next.nextTargetOrdinal === CORRECTIVE_REPLAN_TARGETS.length;
722
+ }
723
+
724
+ function assertPhaseTransition(
725
+ current: CorrectiveReplanTaskTransactionJournal,
726
+ next: CorrectiveReplanTransactionPhase,
727
+ ordinal: number,
728
+ ): void {
729
+ const allowed = current.phase === 'intent' && next === 'applying' && ordinal === 0
730
+ || current.phase === 'applying' && next === 'applying'
731
+ && ordinal === current.nextTargetOrdinal + 1 && ordinal <= CORRECTIVE_REPLAN_TARGETS.length
732
+ || current.phase === 'applying' && next === 'published-pending-readback'
733
+ && current.nextTargetOrdinal === CORRECTIVE_REPLAN_TARGETS.length
734
+ && ordinal === CORRECTIVE_REPLAN_TARGETS.length
735
+ || current.phase === 'published-pending-readback' && next === 'completed'
736
+ && ordinal === CORRECTIVE_REPLAN_TARGETS.length;
737
+ if (!allowed) throw new WorkflowError('INVALID_ARGUMENT', 'Invalid Task transaction phase transition.');
738
+ }
739
+
740
+ function phaseEntry(
741
+ sequence: number,
742
+ phase: CorrectiveReplanTransactionPhase,
743
+ nextTargetOrdinal: number,
744
+ recordedAt: string,
745
+ previousEntryHash: string | null,
746
+ intentSha256: string,
747
+ ) {
748
+ assertIso(recordedAt, 'phase recordedAt');
749
+ return {
750
+ sequence, phase, nextTargetOrdinal, recordedAt, previousEntryHash,
751
+ entryHash: phaseEntryHash(sequence, phase, nextTargetOrdinal, recordedAt, previousEntryHash, intentSha256),
752
+ };
753
+ }
754
+
755
+ function phaseEntryHash(
756
+ sequence: number,
757
+ phase: CorrectiveReplanTransactionPhase,
758
+ nextTargetOrdinal: number,
759
+ recordedAt: string,
760
+ previousEntryHash: string | null,
761
+ intentSha256: string,
762
+ ): string {
763
+ return sha256(Buffer.from(JSON.stringify({
764
+ domain: 'codex-workflow/corrective-replan-task-transaction-phase/v1',
765
+ intentSha256, sequence, phase, nextTargetOrdinal, recordedAt, previousEntryHash,
766
+ })));
767
+ }
768
+
769
+ function intentHash(journal: CorrectiveReplanTaskTransactionJournal): string {
770
+ return sha256(Buffer.from(JSON.stringify({
771
+ domain: 'codex-workflow/corrective-replan-task-transaction-intent/v1',
772
+ schemaVersion: journal.schemaVersion,
773
+ kind: journal.kind,
774
+ transactionId: journal.transactionId,
775
+ projectId: journal.projectId,
776
+ taskId: journal.taskId,
777
+ transitionId: journal.transitionId,
778
+ executionKeyHash: journal.executionKeyHash,
779
+ runtimeFingerprint: journal.runtimeFingerprint,
780
+ catalogHash: journal.catalogHash,
781
+ lifecycleEpoch: journal.lifecycleEpoch,
782
+ bindingManifest: journal.bindingManifest,
783
+ bindingSha256: journal.bindingSha256,
784
+ receiptHash: journal.receiptHash,
785
+ actorIdentityHash: journal.actorIdentityHash,
786
+ humanPrincipalIdentityHash: journal.humanPrincipalIdentityHash,
787
+ admittedAt: journal.admittedAt,
788
+ targets: journal.targets,
789
+ })));
790
+ }
791
+
792
+ function readTarget(taskRoot: string, relativePath: string): { content: Buffer | null } {
793
+ const file = path.join(taskRoot, relativePath);
794
+ assertNoSymlinkPathComponents(file, { label: 'Corrective-replan target', trustedRoot: taskRoot });
795
+ if (!existsSync(file)) return { content: null };
796
+ const stat = lstatSync(file);
797
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new WorkflowError('STATE_CORRUPT', 'Corrective-replan target must be a regular file.');
798
+ return { content: readFileSync(file) };
799
+ }
800
+
801
+ function snapshot(content: Buffer | null, ordinal: number): CorrectiveReplanTargetRecord['preimage'] {
802
+ return content === null
803
+ ? { exists: false, contentSha256: null, byteLength: 0, contentPath: null }
804
+ : { exists: true, contentSha256: sha256(content), byteLength: content.byteLength, contentPath: `preimages/${ordinal}.bin` };
805
+ }
806
+
807
+ function readOwnedPreimage(
808
+ taskRoot: string,
809
+ transactionId: string,
810
+ target: CorrectiveReplanTargetRecord,
811
+ ): Buffer | null {
812
+ if (!target.preimage.exists) return null;
813
+ if (target.preimage.contentPath !== `preimages/${target.ordinal}.bin`) {
814
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction preimage path mismatch.');
815
+ }
816
+ const file = path.join(taskRoot, '.transactions', transactionId, target.preimage.contentPath);
817
+ assertNoSymlinkPathComponents(file, { label: 'Task transaction owned preimage', trustedRoot: taskRoot, requireLeaf: true });
818
+ if (!existsSync(file)) throw new WorkflowError('STATE_CORRUPT', 'Task transaction owned preimage is missing.');
819
+ const content = readFileSync(file);
820
+ if (content.byteLength !== target.preimage.byteLength || sha256(content) !== target.preimage.contentSha256) {
821
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction owned preimage digest mismatch.');
822
+ }
823
+ return content;
824
+ }
825
+
826
+ function assertCanonicalTargetsMatch(taskRoot: string, journal: CorrectiveReplanTaskTransactionJournal): void {
827
+ for (const target of journal.targets) {
828
+ assertCanonicalTargetMatches(taskRoot, target);
829
+ }
830
+ }
831
+
832
+ function assertCanonicalTargetMatches(taskRoot: string, target: CorrectiveReplanTargetRecord): void {
833
+ const current = readTarget(taskRoot, target.relativePath).content;
834
+ if (current === null || sha256(current) !== target.postimage.contentSha256) {
835
+ throw new WorkflowError('STATE_CONFLICT', 'Task transaction target readback mismatch.', {
836
+ targetId: target.targetId,
837
+ });
838
+ }
839
+ }
840
+
841
+ function readOwnedContent(taskRoot: string, transactionId: string, target: CorrectiveReplanTargetRecord): Buffer {
842
+ if (target.postimage.contentPath !== `targets/${target.ordinal}.bin`) {
843
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction owned-content path mismatch.');
844
+ }
845
+ const file = path.join(taskRoot, '.transactions', transactionId, target.postimage.contentPath);
846
+ assertNoSymlinkPathComponents(file, { label: 'Task transaction owned content', trustedRoot: taskRoot, requireLeaf: true });
847
+ if (!existsSync(file)) throw new WorkflowError('STATE_CORRUPT', 'Task transaction owned content is missing.');
848
+ const content = readFileSync(file);
849
+ if (content.byteLength !== target.postimage.byteLength || sha256(content) !== target.postimage.contentSha256) {
850
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction owned content digest mismatch.');
851
+ }
852
+ return content;
853
+ }
854
+
855
+ function parseJsonRecord(content: Buffer, label: string): Record<string, unknown> {
856
+ try {
857
+ const value = JSON.parse(content.toString('utf8')) as unknown;
858
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('not an object');
859
+ return value as Record<string, unknown>;
860
+ } catch {
861
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} must contain one JSON object.`);
862
+ }
863
+ }
864
+
865
+ function assertPlainExact(value: unknown, keys: string[], label: string): asserts value is Record<string, unknown> {
866
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
867
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} must be a plain object.`);
868
+ }
869
+ const actual = Object.keys(value).sort();
870
+ const expected = [...keys].sort();
871
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
872
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} has unknown or missing fields.`);
873
+ }
874
+ }
875
+
876
+ function assertTaskRoot(taskRoot: string, trustedRoot: string): void {
877
+ if (!path.resolve(taskRoot).startsWith(`${path.resolve(trustedRoot)}${path.sep}`)) {
878
+ throw new WorkflowError('INVALID_ARGUMENT', 'Task transaction root is outside the trusted state root.');
879
+ }
880
+ assertNoSymlinkPathComponents(taskRoot, { label: 'Task transaction root', trustedRoot, requireLeaf: true });
881
+ }
882
+
883
+ function ensureDirectorySafe(directory: string, trustedRoot: string): void {
884
+ if (!existsSync(directory)) mkdirSync(directory, { recursive: true, mode: 0o700 });
885
+ assertNoSymlinkPathComponents(directory, { label: 'Task transaction directory', trustedRoot, requireLeaf: true });
886
+ }
887
+
888
+ function writeNewDurable(file: string, content: Buffer): void {
889
+ let descriptor: number | null = null;
890
+ try {
891
+ descriptor = openSync(file, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
892
+ writeSync(descriptor, content);
893
+ fsyncSync(descriptor);
894
+ } finally {
895
+ if (descriptor !== null) closeSync(descriptor);
896
+ }
897
+ }
898
+
899
+ function writeAtomicDurable(file: string, content: Buffer): void {
900
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomUUID()}.tmp`);
901
+ let descriptor: number | null = null;
902
+ try {
903
+ descriptor = openSync(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
904
+ writeSync(descriptor, content);
905
+ fsyncSync(descriptor);
906
+ closeSync(descriptor);
907
+ descriptor = null;
908
+ renameSync(temporary, file);
909
+ fsyncDirectory(path.dirname(file));
910
+ } finally {
911
+ if (descriptor !== null) closeSync(descriptor);
912
+ rmSync(temporary, { force: true });
913
+ }
914
+ }
915
+
916
+ function fsyncDirectory(directory: string): void {
917
+ const descriptor = openSync(directory, 'r');
918
+ try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
919
+ }
920
+
921
+ function jsonBuffer(value: unknown): Buffer {
922
+ return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
923
+ }
924
+
925
+ function sha256(value: Buffer): string {
926
+ return createHash('sha256').update(value).digest('hex');
927
+ }
928
+
929
+ function assertHash(value: unknown, label: string): asserts value is string {
930
+ if (typeof value !== 'string' || !HASH.test(value)) throw new WorkflowError('INVALID_ARGUMENT', `${label} must be lowercase SHA-256.`);
931
+ }
932
+
933
+ function assertIso(value: string, label: string): void {
934
+ const parsed = Date.parse(value);
935
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
936
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} must be a canonical UTC timestamp.`);
937
+ }
938
+ }
939
+
940
+ function assertStoredIso(value: string): void {
941
+ const parsed = Date.parse(value);
942
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
943
+ throw new WorkflowError('STATE_CORRUPT', 'Task transaction contains a noncanonical timestamp.');
944
+ }
945
+ }
946
+
947
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
948
+ return error instanceof Error && 'code' in error;
949
+ }