codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14.1

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 (297) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +98 -34
  3. package/dist/src/alpha6/adoption.d.ts +3 -1
  4. package/dist/src/alpha6/adoption.js +48 -8
  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 +53 -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/literal-test-invocation.d.ts +2 -0
  19. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  20. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  21. package/dist/src/alpha6/milestone.d.ts +2 -0
  22. package/dist/src/alpha6/milestone.js +10 -1
  23. package/dist/src/alpha6/milestone.js.map +1 -1
  24. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  25. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  26. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  27. package/dist/src/alpha6/plan-integrity.js +7 -5
  28. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  29. package/dist/src/alpha6/remediation.d.ts +8 -21
  30. package/dist/src/alpha6/remediation.js +225 -474
  31. package/dist/src/alpha6/remediation.js.map +1 -1
  32. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  33. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  34. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  35. package/dist/src/alpha7/autonomy.d.ts +9 -0
  36. package/dist/src/alpha7/autonomy.js +40 -1
  37. package/dist/src/alpha7/autonomy.js.map +1 -1
  38. package/dist/src/beta1/project-transaction.d.ts +3 -1
  39. package/dist/src/beta1/project-transaction.js +20 -6
  40. package/dist/src/beta1/project-transaction.js.map +1 -1
  41. package/dist/src/checks/runner.d.ts +26 -0
  42. package/dist/src/checks/runner.js +223 -0
  43. package/dist/src/checks/runner.js.map +1 -0
  44. package/dist/src/checks/task-sync.d.ts +10 -0
  45. package/dist/src/checks/task-sync.js +41 -0
  46. package/dist/src/checks/task-sync.js.map +1 -0
  47. package/dist/src/checks/worker.d.ts +1 -0
  48. package/dist/src/checks/worker.js +154 -0
  49. package/dist/src/checks/worker.js.map +1 -0
  50. package/dist/src/cli-actions.d.ts +3 -3
  51. package/dist/src/cli-actions.js +8 -15
  52. package/dist/src/cli-actions.js.map +1 -1
  53. package/dist/src/cli.js +43 -97
  54. package/dist/src/cli.js.map +1 -1
  55. package/dist/src/contracts.d.ts +54 -34
  56. package/dist/src/delegation.d.ts +5 -1
  57. package/dist/src/delegation.js +4 -0
  58. package/dist/src/delegation.js.map +1 -1
  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/failed-checkpoint-update.d.ts +69 -0
  74. package/dist/src/failed-checkpoint-update.js +171 -0
  75. package/dist/src/failed-checkpoint-update.js.map +1 -0
  76. package/dist/src/gateway-handshake.js +0 -8
  77. package/dist/src/gateway-handshake.js.map +1 -1
  78. package/dist/src/git.d.ts +3 -1
  79. package/dist/src/git.js +41 -14
  80. package/dist/src/git.js.map +1 -1
  81. package/dist/src/graph.js +25 -3
  82. package/dist/src/graph.js.map +1 -1
  83. package/dist/src/index.d.ts +1 -0
  84. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +2 -0
  85. package/dist/src/lifecycle/corrective-replan-credential-core.js +45 -9
  86. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -1
  87. package/dist/src/lifecycle/corrective-replan.js +12 -8
  88. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  89. package/dist/src/lifecycle/semantic-registry.js +4 -2
  90. package/dist/src/lifecycle/semantic-registry.js.map +1 -1
  91. package/dist/src/navigation-actions.d.ts +9 -0
  92. package/dist/src/navigation-actions.js +72 -0
  93. package/dist/src/navigation-actions.js.map +1 -0
  94. package/dist/src/navigation-update-artifact.d.ts +5 -0
  95. package/dist/src/navigation-update-artifact.js +227 -0
  96. package/dist/src/navigation-update-artifact.js.map +1 -0
  97. package/dist/src/navigation-update.d.ts +39 -0
  98. package/dist/src/navigation-update.js +82 -0
  99. package/dist/src/navigation-update.js.map +1 -0
  100. package/dist/src/observation.js +52 -29
  101. package/dist/src/observation.js.map +1 -1
  102. package/dist/src/observed-routes.js +10 -6
  103. package/dist/src/observed-routes.js.map +1 -1
  104. package/dist/src/pending-review-update.d.ts +0 -13
  105. package/dist/src/pending-review-update.js +1 -6
  106. package/dist/src/pending-review-update.js.map +1 -1
  107. package/dist/src/reviewer.d.ts +1 -1
  108. package/dist/src/reviewer.js +20 -17
  109. package/dist/src/reviewer.js.map +1 -1
  110. package/dist/src/runtime.d.ts +3 -0
  111. package/dist/src/runtime.js +6 -0
  112. package/dist/src/runtime.js.map +1 -0
  113. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  114. package/dist/src/state/corrective-replan-executor.js +38 -1
  115. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  116. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  117. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  118. package/dist/src/state/corrective-replan-public.d.ts +9 -1
  119. package/dist/src/state/corrective-replan-public.js +50 -8
  120. package/dist/src/state/corrective-replan-public.js.map +1 -1
  121. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  122. package/dist/src/state/corrective-replan-transaction.js +12 -11
  123. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  124. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  125. package/dist/src/state/corrective-yield-transaction.js +11 -10
  126. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  127. package/dist/src/state/lock.d.ts +4 -0
  128. package/dist/src/state/lock.js +21 -0
  129. package/dist/src/state/lock.js.map +1 -1
  130. package/dist/src/version.d.ts +1 -1
  131. package/dist/src/version.js +1 -1
  132. package/dist/src/workflow-blocker-route.d.ts +8 -0
  133. package/dist/src/workflow-blocker-route.js +106 -0
  134. package/dist/src/workflow-blocker-route.js.map +1 -0
  135. package/dist/src/workflow.d.ts +88 -64
  136. package/dist/src/workflow.js +832 -1456
  137. package/dist/src/workflow.js.map +1 -1
  138. package/docs/autonomy-guardrails.md +26 -304
  139. package/docs/decisions.md +14 -112
  140. package/docs/development-flow.md +36 -237
  141. package/docs/project-memory.md +31 -50
  142. package/docs/release-app-evidence.md +190 -0
  143. package/docs/release.md +101 -388
  144. package/docs/updating-existing-project.md +65 -726
  145. package/package.json +11 -13
  146. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  147. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +88 -200
  148. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  149. package/plugins/codex-workflow-gateway/references/protocol.md +51 -458
  150. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +20 -4
  151. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +21 -34
  152. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  153. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +92 -778
  154. package/references/state-machine.md +7 -5
  155. package/roles/delivery-coordinator.md +31 -11
  156. package/roles/scope-lead.md +1 -1
  157. package/roles/technical-planner.md +5 -2
  158. package/schemas/adoption-posture-event.schema.json +11 -0
  159. package/schemas/authorization-event.schema.json +1 -1
  160. package/schemas/corrective-decision-event.schema.json +26 -4
  161. package/schemas/corrective-replan-credentials.private.schema.json +8 -5
  162. package/schemas/corrective-replan-execution-event.private.schema.json +9 -0
  163. package/schemas/corrective-replan-public.schema.json +2 -1
  164. package/schemas/delegation-grant.schema.json +1 -1
  165. package/schemas/project-knowledge-map.schema.json +3 -1
  166. package/schemas/remediation-event.schema.json +10 -1
  167. package/schemas/task.schema.json +64 -2
  168. package/schemas/transition-definition.schema.json +4 -0
  169. package/schemas/transition-payloads.schema.json +16 -1
  170. package/src/alpha6/adoption.ts +1146 -0
  171. package/src/alpha6/captured-check-evidence.ts +132 -0
  172. package/src/alpha6/check-support-anchor.ts +128 -0
  173. package/src/alpha6/component-owner.ts +168 -0
  174. package/src/alpha6/corrective-decision-boundary.ts +45 -0
  175. package/src/alpha6/downstream-proof.ts +520 -0
  176. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  177. package/src/alpha6/handoff.ts +1443 -0
  178. package/src/alpha6/journal.ts +473 -0
  179. package/src/alpha6/literal-test-invocation.ts +84 -0
  180. package/src/alpha6/mechanical-feasibility.ts +488 -0
  181. package/src/alpha6/milestone.ts +2192 -0
  182. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  183. package/src/alpha6/npm-check-contract.ts +47 -0
  184. package/src/alpha6/plan-integrity.ts +298 -0
  185. package/src/alpha6/plan-risk.ts +1480 -0
  186. package/src/alpha6/preexecution-replan.ts +187 -0
  187. package/src/alpha6/remediation-cause.ts +98 -0
  188. package/src/alpha6/remediation.ts +2505 -0
  189. package/src/alpha6/review.ts +1198 -0
  190. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  191. package/src/alpha6/store-sidecars.ts +335 -0
  192. package/src/alpha7/autonomy.ts +449 -0
  193. package/src/alpha7/corrective-recovery.ts +1332 -0
  194. package/src/artifacts.ts +130 -0
  195. package/src/beta1/project-transaction.ts +364 -0
  196. package/src/change-explanation.ts +153 -0
  197. package/src/checks/runner.ts +245 -0
  198. package/src/checks/task-sync.ts +41 -0
  199. package/src/checks/worker.ts +149 -0
  200. package/src/cli-actions.ts +112 -0
  201. package/src/cli.ts +1490 -0
  202. package/src/contracts.ts +1512 -0
  203. package/src/credential-output.ts +89 -0
  204. package/src/credential-transport.ts +215 -0
  205. package/src/delegation.ts +194 -0
  206. package/src/dependency-provenance.ts +472 -0
  207. package/src/diagnostics.ts +93 -0
  208. package/src/domain/base-sync-conflict.ts +35 -0
  209. package/src/domain/completed-step-carryover.ts +76 -0
  210. package/src/domain/discovery.ts +27 -0
  211. package/src/domain/plan-semantics.ts +58 -0
  212. package/src/domain/step-start-admission.ts +10 -0
  213. package/src/domain/validation.ts +177 -0
  214. package/src/domain/virgin-registration.ts +46 -0
  215. package/src/errors.ts +24 -0
  216. package/src/failed-checkpoint-update.ts +203 -0
  217. package/src/fs-utils.ts +61 -0
  218. package/src/gateway-handshake.ts +95 -0
  219. package/src/git.ts +183 -0
  220. package/src/graph.ts +342 -0
  221. package/src/historical-step-provenance.ts +136 -0
  222. package/src/index.ts +23 -0
  223. package/src/lifecycle/canonical-hash.ts +28 -0
  224. package/src/lifecycle/catalog.ts +202 -0
  225. package/src/lifecycle/compiler-inspection.ts +29 -0
  226. package/src/lifecycle/core-static-readiness.ts +132 -0
  227. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  228. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  229. package/src/lifecycle/corrective-replan-credential-core.ts +437 -0
  230. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  231. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  232. package/src/lifecycle/corrective-replan.ts +847 -0
  233. package/src/lifecycle/evaluator.ts +48 -0
  234. package/src/lifecycle/fingerprint.ts +488 -0
  235. package/src/lifecycle/immutable.ts +8 -0
  236. package/src/lifecycle/implementation-table.ts +118 -0
  237. package/src/lifecycle/index.ts +8 -0
  238. package/src/lifecycle/schema-artifact.ts +263 -0
  239. package/src/lifecycle/semantic-registry.ts +574 -0
  240. package/src/lifecycle/types.ts +838 -0
  241. package/src/memory.ts +273 -0
  242. package/src/migration.ts +161 -0
  243. package/src/navigation-actions.ts +70 -0
  244. package/src/navigation-update-artifact.ts +198 -0
  245. package/src/navigation-update.ts +121 -0
  246. package/src/observation.ts +225 -0
  247. package/src/observed-routes.ts +665 -0
  248. package/src/operational-contract.ts +125 -0
  249. package/src/pending-review-update.ts +175 -0
  250. package/src/repository.ts +99 -0
  251. package/src/reviewer.ts +1879 -0
  252. package/src/runtime.ts +6 -0
  253. package/src/state/corrective-replan-executor.ts +819 -0
  254. package/src/state/corrective-replan-public-schema.ts +83 -0
  255. package/src/state/corrective-replan-public.ts +952 -0
  256. package/src/state/corrective-replan-transaction.ts +949 -0
  257. package/src/state/corrective-yield-executor.ts +327 -0
  258. package/src/state/corrective-yield-transaction.ts +730 -0
  259. package/src/state/lock.ts +902 -0
  260. package/src/state/store.ts +567 -0
  261. package/src/transition-core.ts +330 -0
  262. package/src/ulid.ts +24 -0
  263. package/src/version.ts +2 -0
  264. package/src/workflow-blocker-route.ts +109 -0
  265. package/src/workflow.ts +10342 -0
  266. package/docs/alpha7.1-implementation-brief.md +0 -268
  267. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  268. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  269. package/docs/beta1-stabilization-brief.md +0 -165
  270. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  271. package/docs/beta13.2-signal-review-recovery.md +0 -38
  272. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  273. package/docs/change-model.md +0 -118
  274. package/docs/delegated-approval.md +0 -254
  275. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  276. package/docs/pdf/README.md +0 -24
  277. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  278. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  279. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  280. package/docs/pdf/requirements.txt +0 -1
  281. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  282. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  283. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  284. package/docs/pending-review-update.md +0 -15
  285. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  286. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  287. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  288. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  289. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  290. package/docs/problem-briefs/README.md +0 -120
  291. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  292. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  293. package/docs/split-required-recovery.md +0 -47
  294. package/docs/stable-release-defect-register.md +0 -730
  295. package/docs/validation-report.md +0 -182
  296. package/scripts/generate-pdf-docs.py +0 -524
  297. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,27 @@
1
+ import type { DiscoveryState } from '../contracts.js';
2
+
3
+ export interface DiscoveryReadiness {
4
+ ready: boolean;
5
+ status: 'needs_discovery' | 'needs_clarification' | 'ready_to_materialize';
6
+ missing: string[];
7
+ }
8
+
9
+ export function assessDiscovery(discovery: DiscoveryState): DiscoveryReadiness {
10
+ const missing: string[] = [];
11
+ if (!discovery.goal.trim()) missing.push('goal');
12
+ if (!discovery.outcome?.trim()) missing.push('observable outcome');
13
+ if (discovery.scope.length === 0) missing.push('scope');
14
+ if (discovery.acceptance.length === 0) missing.push('acceptance scenario');
15
+ if (discovery.blockingUnknowns.length > 0) missing.push('resolved blocking unknowns');
16
+ if (missing.length === 0) return { ready: true, status: 'ready_to_materialize', missing };
17
+ const hasInitialUnderstanding = Boolean(discovery.goal.trim() && (discovery.outcome || discovery.scope.length > 0));
18
+ return {
19
+ ready: false,
20
+ status: hasInitialUnderstanding ? 'needs_clarification' : 'needs_discovery',
21
+ missing,
22
+ };
23
+ }
24
+
25
+ export function mergeUnique(current: string[], additions: string[]): string[] {
26
+ return [...new Set([...current, ...additions].map((value) => value.trim()).filter(Boolean))];
27
+ }
@@ -0,0 +1,58 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { WorkflowError } from '../errors.js';
5
+
6
+ const HASH64 = /^[a-f0-9]{64}$/;
7
+ const KNOWLEDGE_LINE = /^- Knowledge map: revision ([1-9]\d*), hash ([a-f0-9]{64})$/gm;
8
+ const KNOWLEDGE_SENTINEL = '- Knowledge map: revision <context>, hash <context>';
9
+
10
+ export interface NormalizedPlanSemantics {
11
+ normalized: string;
12
+ semanticHash: string;
13
+ binding: { revision: number; hash: string };
14
+ }
15
+
16
+ export function normalizePlanKnowledgeBinding(content: string, label: string): NormalizedPlanSemantics {
17
+ const matches = [...content.matchAll(KNOWLEDGE_LINE)];
18
+ if (matches.length !== 1) {
19
+ throw new WorkflowError('STATE_CORRUPT', `${label} Plan must contain exactly one canonical Knowledge Map binding line.`, {
20
+ matchCount: matches.length,
21
+ });
22
+ }
23
+ const match = matches[0]!;
24
+ const normalized = content.slice(0, match.index) + KNOWLEDGE_SENTINEL + content.slice(match.index! + match[0].length);
25
+ return {
26
+ normalized,
27
+ semanticHash: sha256Hex(normalized),
28
+ binding: { revision: Number(match[1]), hash: match[2]! },
29
+ };
30
+ }
31
+
32
+ export function assertKnowledgeBindingOnlyPlanChange(historical: string, current: string): string {
33
+ const source = normalizePlanKnowledgeBinding(historical, 'historical');
34
+ const target = normalizePlanKnowledgeBinding(current, 'current');
35
+ if (source.normalized !== target.normalized) {
36
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery permits only the Knowledge Map binding line to change.');
37
+ }
38
+ return source.semanticHash;
39
+ }
40
+
41
+ export function readVerifiedPlanArtifact(taskRoot: string, planHash: string, currentPlanHash: string): string {
42
+ if (!HASH64.test(planHash)) throw new WorkflowError('STATE_CORRUPT', 'Plan hash is malformed.', { planHash });
43
+ const file = planHash === currentPlanHash
44
+ ? path.join(taskRoot, 'plan.md')
45
+ : path.join(taskRoot, '.versions', `plan.md.${planHash}`);
46
+ if (!existsSync(file)) {
47
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact required by lifecycle evidence is missing.', { planHash, file });
48
+ }
49
+ const content = readFileSync(file, 'utf8');
50
+ if (sha256Hex(content) !== planHash) {
51
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its recorded hash.', { planHash, file });
52
+ }
53
+ return content;
54
+ }
55
+
56
+ function sha256Hex(value: string): string {
57
+ return createHash('sha256').update(value).digest('hex');
58
+ }
@@ -0,0 +1,10 @@
1
+ import type { StepStatus } from '../contracts.js';
2
+
3
+ export type StepStartPhase = 'planned-clean' | 'planned-dirty' | 'failed-retry' | 'not-startable';
4
+
5
+ /** Replacement carryover admits a first start; a retry uses its current write scope. */
6
+ export function stepStartPhase(status: StepStatus | undefined, hasDirtyFiles: boolean): StepStartPhase {
7
+ if (status === 'planned') return hasDirtyFiles ? 'planned-dirty' : 'planned-clean';
8
+ if (status === 'failed') return 'failed-retry';
9
+ return 'not-startable';
10
+ }
@@ -0,0 +1,177 @@
1
+ import type {
2
+ PlanRiskAuditStepClassification,
3
+ TaskPlanInput,
4
+ TaskState,
5
+ } from '../contracts.js';
6
+ import { WorkflowError } from '../errors.js';
7
+
8
+ export function validatePlan(plan: TaskPlanInput): void {
9
+ const issues: string[] = [];
10
+ if (!plan.objective.trim()) issues.push('Plan objective is required.');
11
+ if (plan.requirements.length === 0) issues.push('At least one requirement is required.');
12
+ if (plan.acceptance.length === 0) issues.push('At least one acceptance scenario is required.');
13
+ if (plan.steps.length === 0) issues.push('At least one STEP-* is required.');
14
+ if (!plan.knowledgeImpactReason.trim()) issues.push('knowledgeImpactReason is required.');
15
+ if (!Number.isInteger(plan.knowledgeMapRevision) || plan.knowledgeMapRevision < 1) {
16
+ issues.push('knowledgeMapRevision must be a positive integer.');
17
+ }
18
+ if (!/^[a-f0-9]{64}$/.test(plan.knowledgeMapHash)) issues.push('knowledgeMapHash must be a SHA-256 hash.');
19
+ if (plan.knowledgeImpact === 'none' && plan.knowledgeTargets.length > 0) {
20
+ issues.push('knowledgeImpact none cannot declare knowledgeTargets.');
21
+ }
22
+ if (plan.knowledgeImpact !== 'none' && plan.knowledgeTargets.length === 0) {
23
+ issues.push(`knowledgeImpact ${plan.knowledgeImpact} requires at least one knowledgeTarget.`);
24
+ }
25
+ for (const target of plan.knowledgeTargets) {
26
+ if (!target.reason.trim()) issues.push(`Knowledge target ${target.path} requires a reason.`);
27
+ if (target.requirementIds.length === 0) issues.push(`Knowledge target ${target.path} requires requirementIds.`);
28
+ }
29
+
30
+ const ids = new Set<string>();
31
+ for (const step of plan.steps) {
32
+ if (!/^STEP-[0-9]{3}(?:-[A-Z0-9-]+)?$/.test(step.id)) issues.push(`Invalid Step id: ${step.id}`);
33
+ if (ids.has(step.id)) issues.push(`Duplicate Step id: ${step.id}`);
34
+ ids.add(step.id);
35
+ if (!step.title.trim() || !step.objective.trim()) issues.push(`${step.id} requires title and objective.`);
36
+ if (step.requirements.length === 0) issues.push(`${step.id} must cover at least one requirement.`);
37
+ if (step.allowedWrites.length === 0) issues.push(`${step.id} must declare allowedWrites.`);
38
+ if (step.expectedOutputs.length === 0) issues.push(`${step.id} must declare expectedOutputs.`);
39
+ if (step.checks.length === 0) issues.push(`${step.id} must declare at least one executable check.`);
40
+ }
41
+
42
+ for (const step of plan.steps) {
43
+ for (const dependency of step.dependencies) {
44
+ if (!ids.has(dependency)) issues.push(`${step.id} depends on missing ${dependency}.`);
45
+ if (dependency === step.id) issues.push(`${step.id} cannot depend on itself.`);
46
+ }
47
+ }
48
+
49
+ const covered = new Set(plan.steps.flatMap((step) => step.requirements));
50
+ for (const requirement of plan.requirements) {
51
+ if (!covered.has(requirement)) issues.push(`Requirement is not covered by any Step: ${requirement}`);
52
+ }
53
+
54
+ const visiting = new Set<string>();
55
+ const visited = new Set<string>();
56
+ const byId = new Map(plan.steps.map((step) => [step.id, step]));
57
+ const visit = (stepId: string): void => {
58
+ if (visited.has(stepId)) return;
59
+ if (visiting.has(stepId)) {
60
+ issues.push(`Step dependency cycle includes ${stepId}.`);
61
+ return;
62
+ }
63
+ visiting.add(stepId);
64
+ for (const dependency of byId.get(stepId)?.dependencies ?? []) visit(dependency);
65
+ visiting.delete(stepId);
66
+ visited.add(stepId);
67
+ };
68
+ for (const step of plan.steps) visit(step.id);
69
+
70
+ const productionEdges = plan.productionEdges ?? [];
71
+ if (plan.requiresProductionEdges && productionEdges.length === 0) {
72
+ issues.push('Plan requiresProductionEdges but declares no explicit production edge.');
73
+ }
74
+ if (!plan.requiresProductionEdges && productionEdges.length > 0) {
75
+ issues.push('Plan declares productionEdges without the explicit requiresProductionEdges boundary.');
76
+ }
77
+ const edgeKeys = new Set<string>();
78
+ for (const edge of productionEdges) {
79
+ const key = `${edge.ownerStepId}\0${edge.consumerStepId}\0${edge.checkRef}`;
80
+ if (edgeKeys.has(key)) issues.push(`Duplicate production edge: ${edge.ownerStepId} -> ${edge.consumerStepId}.`);
81
+ edgeKeys.add(key);
82
+ const owner = byId.get(edge.ownerStepId);
83
+ const consumer = byId.get(edge.consumerStepId);
84
+ if (!owner) issues.push(`Production edge owner is missing: ${edge.ownerStepId}.`);
85
+ if (!consumer) issues.push(`Production edge consumer is missing: ${edge.consumerStepId}.`);
86
+ if (owner && consumer && owner.id === consumer.id) issues.push(`Production edge cannot self-reference ${owner.id}.`);
87
+ if (owner && consumer && !transitiveDependencies(consumer.id, byId).has(owner.id)) {
88
+ issues.push(`Production edge owner ${owner.id} is not a transitive dependency of consumer ${consumer.id}.`);
89
+ }
90
+ if (consumer && !consumer.checks.includes(edge.checkRef)) {
91
+ issues.push(`Production edge ${edge.ownerStepId} -> ${edge.consumerStepId} names a check not executed by its consumer.`);
92
+ }
93
+ }
94
+
95
+ if (issues.length > 0) throw new WorkflowError('TRANSITION_BLOCKED', 'Plan is not execution-ready.', { issues });
96
+ }
97
+
98
+ /**
99
+ * Derive the narrow beta.13 structural integration obligation from the independent
100
+ * Plan Risk Audit rather than trusting the planner-owned opt-in flag. This is not
101
+ * semantic output-to-evidence authority (P02): it only proves that a multi-Step
102
+ * runtime-composition Plan names an inbound production hand-off and executes its
103
+ * exact check in the consumer Step.
104
+ */
105
+ export function validateStructuralProductionEdgeAuthority(
106
+ task: Pick<TaskState, 'id' | 'steps' | 'productionEdges' | 'requiresProductionEdges'>,
107
+ classifications: readonly PlanRiskAuditStepClassification[],
108
+ ): void {
109
+ if (task.steps.length < 2) return;
110
+ const integrationConsumerIds = classifications
111
+ .filter((classification) => classification.categories.includes('runtime-composition'))
112
+ .map((classification) => classification.stepId)
113
+ .sort();
114
+ if (integrationConsumerIds.length === 0) return;
115
+
116
+ const edges = task.productionEdges ?? [];
117
+ const byId = new Map(task.steps.map((step) => [step.id, step]));
118
+ const missingConsumerEdgeIds = integrationConsumerIds.filter((consumerStepId) =>
119
+ !edges.some((edge) => edge.consumerStepId === consumerStepId));
120
+ const blockingGuardIds = [
121
+ ...(task.requiresProductionEdges === true ? [] : ['task.production-edge.explicit-boundary']),
122
+ ...(edges.length > 0 ? [] : ['task.production-edge.non-empty']),
123
+ ...(missingConsumerEdgeIds.length > 0 ? ['task.production-edge.runtime-consumer-covered'] : []),
124
+ ];
125
+ for (const edge of edges) {
126
+ const consumer = byId.get(edge.consumerStepId);
127
+ const owner = byId.get(edge.ownerStepId);
128
+ if (!consumer || !owner || !transitiveDependencies(consumer.id, byId).has(owner.id)
129
+ || !consumer.checks.includes(edge.checkRef)) {
130
+ blockingGuardIds.push('task.production-edge.executable-predecessor-binding');
131
+ break;
132
+ }
133
+ }
134
+ if (blockingGuardIds.length === 0) return;
135
+ throw new WorkflowError(
136
+ 'TRANSITION_BLOCKED',
137
+ `Runtime-composition authority for Task ${task.id} requires explicit production-edge coverage.`,
138
+ {
139
+ diagnosticCode: 'STRUCTURAL_PRODUCTION_EDGE_REQUIRED',
140
+ triggerAuthority: 'plan-risk-audit.runtime-composition',
141
+ integrationConsumerStepIds: integrationConsumerIds,
142
+ missingConsumerEdgeIds,
143
+ blockingGuardIds: [...new Set(blockingGuardIds)],
144
+ semanticAuthorityClaimed: false,
145
+ requiredAction: 'Declare requiresProductionEdges=true and bind each runtime-composition consumer to a transitive owner and one exact consumer check.',
146
+ },
147
+ );
148
+ }
149
+
150
+ function transitiveDependencies(
151
+ stepId: string,
152
+ byId: ReadonlyMap<string, TaskPlanInput['steps'][number]>,
153
+ ): Set<string> {
154
+ const result = new Set<string>();
155
+ const visit = (candidateId: string): void => {
156
+ for (const dependency of byId.get(candidateId)?.dependencies ?? []) {
157
+ if (result.has(dependency)) continue;
158
+ result.add(dependency);
159
+ visit(dependency);
160
+ }
161
+ };
162
+ visit(stepId);
163
+ return result;
164
+ }
165
+
166
+ export function pathAllowed(file: string, patterns: string[]): boolean {
167
+ const normalized = file.replaceAll('\\', '/').replace(/^\.\//, '');
168
+ return patterns.some((pattern) => {
169
+ const candidate = pattern.replaceAll('\\', '/').replace(/^\.\//, '');
170
+ if (candidate.endsWith('/**')) return normalized === candidate.slice(0, -3) || normalized.startsWith(candidate.slice(0, -2));
171
+ if (candidate.endsWith('/*')) {
172
+ const prefix = candidate.slice(0, -1);
173
+ return normalized.startsWith(prefix) && !normalized.slice(prefix.length).includes('/');
174
+ }
175
+ return normalized === candidate;
176
+ });
177
+ }
@@ -0,0 +1,46 @@
1
+ import { lstatSync, readdirSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import type { RepositoryIdentity } from '../repository.js';
5
+ import { isVirginProjectRegistrationContent } from '../alpha6/adoption.js';
6
+ import { assertNoSymlinkPathComponents } from '../alpha6/store-sidecars.js';
7
+
8
+ /** Read-only bootstrap admission. Retained or unrecognized state is never bootstrap. */
9
+ export function isVirginRegistrationRoot(projectRoot: string, trustedRoot: string, identity: RepositoryIdentity, packageVersion: string): boolean {
10
+ try {
11
+ const relative = path.relative(path.resolve(trustedRoot), path.resolve(projectRoot));
12
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return false;
13
+ // The trusted state home itself can be absent on first use. Do not create it
14
+ // during observation, and do not confuse that case with invalid retained data.
15
+ try { lstatSync(trustedRoot); } catch (error) {
16
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true;
17
+ throw error;
18
+ }
19
+ assertNoSymlinkPathComponents(projectRoot, { label: 'Bootstrap project root', trustedRoot });
20
+ let root;
21
+ try { root = lstatSync(projectRoot); } catch (error) {
22
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true;
23
+ throw error;
24
+ }
25
+ if (!root.isDirectory() || root.isSymbolicLink()) return false;
26
+ const validFile = (file: string, hash?: string) => {
27
+ const stat = lstatSync(file);
28
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) return false;
29
+ const bytes = readFileSync(file);
30
+ return (!hash || createHash('sha256').update(bytes).digest('hex') === hash)
31
+ && isVirginProjectRegistrationContent(bytes.toString('utf8'), identity, packageVersion);
32
+ };
33
+ for (const entry of readdirSync(projectRoot, { withFileTypes: true })) {
34
+ if (entry.isSymbolicLink()) return false;
35
+ if (entry.name === 'adoption-posture.jsonl') {
36
+ if (!validFile(path.join(projectRoot, entry.name))) return false;
37
+ } else if (entry.name === '.versions' && entry.isDirectory()) {
38
+ for (const snapshot of readdirSync(path.join(projectRoot, '.versions'))) {
39
+ const match = /^adoption-posture\.jsonl\.([a-f0-9]{64})$/.exec(snapshot);
40
+ if (!match || !validFile(path.join(projectRoot, '.versions', snapshot), match[1])) return false;
41
+ }
42
+ } else return false;
43
+ }
44
+ return true;
45
+ } catch { return false; }
46
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,24 @@
1
+ export type WorkflowErrorCode =
2
+ | 'INVALID_ARGUMENT'
3
+ | 'NOT_FOUND'
4
+ | 'STATE_CONFLICT'
5
+ | 'STATE_CORRUPT'
6
+ | 'LOCKED'
7
+ | 'LOCK_TOKEN_INVALID'
8
+ | 'TASK_RECOVERY_REQUIRED'
9
+ | 'TRANSITION_BLOCKED'
10
+ | 'GIT_PRECONDITION_FAILED'
11
+ | 'COMMAND_FAILED'
12
+ | 'CHECK_INFRASTRUCTURE_FAILED';
13
+
14
+ export class WorkflowError extends Error {
15
+ readonly code: WorkflowErrorCode;
16
+ readonly details: Record<string, unknown>;
17
+
18
+ constructor(code: WorkflowErrorCode, message: string, details: Record<string, unknown> = {}) {
19
+ super(message);
20
+ this.name = 'WorkflowError';
21
+ this.code = code;
22
+ this.details = details;
23
+ }
24
+ }
@@ -0,0 +1,203 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import type { TaskState } from './contracts.js';
5
+ import type { FileStateStore } from './state/store.js';
6
+ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
7
+ import { readCorrectiveDecisionEvents, readRemediationEvents } from './alpha6/remediation.js';
8
+ import { readTaskC1Posture, readTaskHandoffEvents } from './alpha6/handoff.js';
9
+ import { readPlanRiskAuditEvents } from './alpha6/plan-risk.js';
10
+ import { verifiedHistoricalStepReviewBindings } from './alpha6/review.js';
11
+ import { readVerifiedPlanArtifact } from './domain/plan-semantics.js';
12
+ import { pathAllowed } from './domain/validation.js';
13
+ import { validateTaskHistory } from './git.js';
14
+ import { inspectBoundedWorkflowDependencyCommit, workflowDependencyVersionAt } from './dependency-provenance.js';
15
+ import { inspectWorkflowVersions } from './diagnostics.js';
16
+ import { changedFiles, currentBranch, headCommit, runGit } from './repository.js';
17
+ import { PACKAGE_VERSION } from './version.js';
18
+
19
+ export const FAILED_CHECKPOINT_SOURCE = '2.0.0-beta.14';
20
+ export const FAILED_CHECKPOINT_TARGET = '2.0.0-beta.14.1';
21
+ const hash = (value: unknown) => sha256Hex(canonicalJsonStringify(value));
22
+ function require(condition: unknown, message: string): asserts condition {
23
+ if (!condition) throw new Error(`FAILED_CHECKPOINT_UPDATE_BLOCKED: ${message}`);
24
+ }
25
+ export interface FailedCheckpointUpdateReceipt {
26
+ protocol: 'failed-checkpoint-update-source-v1';
27
+ projectId: string; taskId: string; revision: number; issuedAt: string;
28
+ taskHash: string; artifactsHash: string; knowledgeHash: string; milestoneHash: string | null;
29
+ sourceHead: string; sourceBaseHead: string; taskBranch: string; baseBranch: string;
30
+ sourceVersion: string; targetVersion: string; actor: string; stepId: string;
31
+ dirtyFiles: string[]; sourceDirtyHash: string; remediationEventId: string; remediationEventHash: string;
32
+ artifact: { sha256: string; runtimeHash: string };
33
+ }
34
+ export interface FailedCheckpointUpdateBinding {
35
+ protocol: 'failed-checkpoint-update-v1';
36
+ sourceHead: string; sourcePlanHash: string; stepId: string;
37
+ sourceDirtyHash: string; targetDirtyHash: string; dirtyFiles: string[];
38
+ remediationEventId: string; remediationEventHash: string;
39
+ sourceReceiptHash: string; artifactHash: string;
40
+ }
41
+ function filesHash(root: string): string {
42
+ if (!existsSync(root)) return hash([]);
43
+ const visit = (directory: string): Array<[string, string]> => readdirSync(directory).sort().flatMap(name => {
44
+ const file = path.join(directory, name); const info = lstatSync(file);
45
+ require(!info.isSymbolicLink() && (info.isFile() || info.isDirectory()), 'State artifacts must be regular files.');
46
+ return info.isDirectory() ? visit(file) : [[path.relative(root, file), sha256Hex(readFileSync(file))]];
47
+ });
48
+ return hash(visit(root));
49
+ }
50
+ /** Reverse only the dependency string, retaining every other byte (including whitespace and product scripts). */
51
+ function sourceManifestBytes(content: Buffer): Buffer {
52
+ const text = content.toString('utf8'); const manifest = JSON.parse(text);
53
+ require(manifest.devDependencies?.['codex-workflow-v2'] === FAILED_CHECKPOINT_TARGET
54
+ && !manifest.dependencies?.['codex-workflow-v2'], 'Target working manifest must have one exact dev dependency.');
55
+ const key = /("codex-workflow-v2"\s*:\s*)"2\.0\.0-beta\.14\.1"/g;
56
+ require([...text.matchAll(key)].length === 1, 'Working manifest must have one unambiguous literal target pin.');
57
+ return Buffer.from(text.replace(key, `$1"${FAILED_CHECKPOINT_SOURCE}"`));
58
+ }
59
+ /** Same byte framing as native failure evidence; no normalization of product content. */
60
+ export function failedCheckpointDirtyHash(repository: string, files: readonly string[], transported = false): string {
61
+ const digest = createHash('sha256');
62
+ for (const file of [...files].sort()) {
63
+ const absolute = path.resolve(repository, file); const relative = path.relative(repository, absolute);
64
+ require(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'Dirty path escapes checkout.');
65
+ require(existsSync(absolute) && lstatSync(absolute).isFile() && !lstatSync(absolute).isSymbolicLink(),
66
+ 'This checkpoint supports retained regular files only.');
67
+ let bytes: Buffer = readFileSync(absolute);
68
+ if (transported && file === 'package.json') bytes = sourceManifestBytes(bytes);
69
+ digest.update(`path\0${file}\0file\0`); digest.update(bytes); digest.update('\0');
70
+ }
71
+ return digest.digest('hex');
72
+ }
73
+ function assertPins(repository: string, commit: string, version: string): void {
74
+ const manifest = JSON.parse(runGit(repository, ['show', `${commit}:package.json`]));
75
+ const lock = JSON.parse(runGit(repository, ['show', `${commit}:package-lock.json`]));
76
+ require(manifest.devDependencies?.['codex-workflow-v2'] === version && !manifest.dependencies?.['codex-workflow-v2']
77
+ && workflowDependencyVersionAt(repository, commit) === version
78
+ && lock.packages?.['']?.devDependencies?.['codex-workflow-v2'] === version
79
+ && !lock.packages?.['']?.dependencies?.['codex-workflow-v2']
80
+ && lock.packages?.['node_modules/codex-workflow-v2']?.version === version,
81
+ 'Exact source manifest and lock bindings are required.');
82
+ }
83
+ export function assertFailedCheckpointInstalled(repository: string, task: TaskState, version: string): void {
84
+ const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
85
+ require([versions.declared, versions.locked, versions.installed, versions.currentBranch,
86
+ ...versions.milestoneBases.map(item => item.version)].every(value => value === version),
87
+ `Every installed and Git version surface must equal ${version}.`);
88
+ }
89
+ export function failedCheckpointUpdateSource(repository: string, store: FileStateStore, task: TaskState,
90
+ artifact: FailedCheckpointUpdateReceipt['artifact'],
91
+ sourceHead = headCommit(repository), sourceBaseHead = runGit(repository, ['rev-parse', task.baseBranch]),
92
+ transported = false): FailedCheckpointUpdateReceipt {
93
+ require(PACKAGE_VERSION === FAILED_CHECKPOINT_TARGET, 'Only the beta.14 to beta.14.1 repair is supported.');
94
+ require(task.status === 'awaiting_execution_authorization' && task.workspaceOwner === 'external',
95
+ 'Expected an external failed Task awaiting its new Plan audit.');
96
+ require(readCorrectiveDecisionEvents(store, task).length === 0,
97
+ 'A checkpoint with corrective decision history requires its own supported boundary.');
98
+ require(task.taskBranch && currentBranch(repository) === task.taskBranch, 'Expected the recorded Task branch.');
99
+ const failed = task.steps.filter(step => step.status === 'failed');
100
+ require(failed.length === 1 && !task.steps.some(step => step.status === 'in_progress'), 'Exactly one paused failed Step is required.');
101
+ const step = failed[0]!;
102
+ require(step.failurePause && step.evidence === null, 'Native failed-check evidence is required.');
103
+ require(task.planHash && task.briefHash, 'Current Brief and Plan are required.');
104
+ const taskRoot = store.taskRoot(task.projectId, task.id);
105
+ require(store.hashArtifact(taskRoot, 'brief.md') === task.briefHash && store.hashArtifact(taskRoot, 'plan.md') === task.planHash,
106
+ 'Brief and Plan artifacts must match state.');
107
+ readVerifiedPlanArtifact(taskRoot, task.planHash, task.planHash);
108
+ const currentAudit = readPlanRiskAuditEvents(store, task).at(-1);
109
+ require(!currentAudit || currentAudit.planHash !== task.planHash
110
+ || currentAudit.decision === 'stop-escalate', 'The checkpoint must await its audit or retain a planning obstruction.');
111
+ assertPins(repository, sourceHead, FAILED_CHECKPOINT_SOURCE); assertPins(repository, sourceBaseHead, FAILED_CHECKPOINT_SOURCE);
112
+ validateTaskHistory(repository, task, sourceHead);
113
+ const completed = task.steps.filter(item => item.status === 'completed');
114
+ const reviews = verifiedHistoricalStepReviewBindings(store, task);
115
+ require(completed.length > 0 && completed.every(item => item.evidence
116
+ && reviews.some(review => review.stepId === item.id && review.commitSha === item.evidence!.commitSha)),
117
+ 'Every retained completed Step must have immutable execution and independent review evidence.');
118
+ require(step.dependencies.every(id => completed.some(item => item.id === id)), 'Failed Step dependencies must be complete.');
119
+ const events = readRemediationEvents(store, task).filter(event => event.stepId === step.id && event.failureKind === 'checks-failed');
120
+ require(events.length === 1, 'This checkpoint supports exactly the first native check failure.');
121
+ const terminal = events.at(-1);
122
+ require(terminal && terminal.failureEvidence.completionCommit === sourceHead
123
+ && terminal.failureEvidence.dirtyWorktreeHash === step.failurePause.dirtyWorktreeHash,
124
+ 'Native terminal failure must bind source HEAD and retained dirty bytes.');
125
+ const dirtyFiles = changedFiles(repository).sort();
126
+ require(dirtyFiles.length > 0 && !dirtyFiles.includes('package-lock.json')
127
+ && JSON.stringify(dirtyFiles) === JSON.stringify([...step.failurePause.dirtyFiles].sort()),
128
+ 'Dirty paths must equal the native failure; dirty lockfile transport is unsupported.');
129
+ require(dirtyFiles.every(file => pathAllowed(file, step.allowedWrites) && !pathAllowed(file, step.forbiddenScope)),
130
+ 'Retained dirty files must be inside the failed Step scope.');
131
+ const sourceDirtyHash = failedCheckpointDirtyHash(repository, dirtyFiles, transported);
132
+ require(sourceDirtyHash === terminal.failureEvidence.dirtyWorktreeHash, 'Product bytes changed since native failure.');
133
+ const posture = readTaskC1Posture(store, task);
134
+ const claim = readTaskHandoffEvents(store, task).filter(event => event.eventType === 'claim').at(-1);
135
+ require(posture.state === 'claimed' && claim && posture.claimant === claim.actor && claim.briefHash === task.briefHash,
136
+ 'Original immutable Worker claim is required.');
137
+ return { protocol: 'failed-checkpoint-update-source-v1', projectId: task.projectId, taskId: task.id,
138
+ revision: task.revision, issuedAt: task.updatedAt, taskHash: hash(task), artifactsHash: filesHash(taskRoot),
139
+ knowledgeHash: hash(store.readKnowledgeMap(task.projectId)),
140
+ milestoneHash: task.milestoneId ? hash(store.readMilestone(task.projectId, task.milestoneId)) : null,
141
+ sourceHead, sourceBaseHead, taskBranch: task.taskBranch, baseBranch: task.baseBranch,
142
+ sourceVersion: FAILED_CHECKPOINT_SOURCE, targetVersion: FAILED_CHECKPOINT_TARGET, actor: claim.actor,
143
+ stepId: step.id, dirtyFiles, sourceDirtyHash, remediationEventId: terminal.eventId,
144
+ remediationEventHash: terminal.eventHash, artifact };
145
+ }
146
+ export function inspectFailedCheckpointTransport(repository: string, store: FileStateStore, task: TaskState,
147
+ receipt: FailedCheckpointUpdateReceipt, artifact: FailedCheckpointUpdateReceipt['artifact']) {
148
+ require(receipt?.protocol === 'failed-checkpoint-update-source-v1', 'Missing or foreign source receipt.');
149
+ const targetHead = headCommit(repository); const base = runGit(repository, ['rev-parse', task.baseBranch]);
150
+ const target = inspectBoundedWorkflowDependencyCommit(repository, targetHead);
151
+ const baseTransport = inspectBoundedWorkflowDependencyCommit(repository, base);
152
+ require([targetHead, base].every(commit => runGit(repository, ['rev-list', '--parents', '-n', '1', commit]).split(' ').length === 2),
153
+ 'Transport commits must have exactly one parent.');
154
+ require(target?.parentCommitSha === receipt.sourceHead && target.packageVersion === FAILED_CHECKPOINT_TARGET
155
+ && baseTransport?.parentCommitSha === receipt.sourceBaseHead && baseTransport.packageVersion === FAILED_CHECKPOINT_TARGET,
156
+ 'Exactly one dependency-only transport is required on each bound branch.');
157
+ const reconstructed = failedCheckpointUpdateSource(repository, store, task, artifact,
158
+ target.parentCommitSha, baseTransport.parentCommitSha, true);
159
+ require(canonicalJsonStringify(receipt) === canonicalJsonStringify(reconstructed),
160
+ 'Source receipt no longer matches state, artifacts, product bytes, history or tarball.');
161
+ assertFailedCheckpointInstalled(repository, task, FAILED_CHECKPOINT_TARGET);
162
+ const binding: FailedCheckpointUpdateBinding = { protocol: 'failed-checkpoint-update-v1',
163
+ sourceHead: receipt.sourceHead, sourcePlanHash: task.planHash!, stepId: receipt.stepId,
164
+ sourceDirtyHash: receipt.sourceDirtyHash, targetDirtyHash: failedCheckpointDirtyHash(repository, receipt.dirtyFiles),
165
+ dirtyFiles: receipt.dirtyFiles, remediationEventId: receipt.remediationEventId,
166
+ remediationEventHash: receipt.remediationEventHash, sourceReceiptHash: hash(receipt), artifactHash: artifact.sha256 };
167
+ return { receipt: reconstructed, target, baseTransport: base, targetHead, binding };
168
+ }
169
+
170
+ /** Revalidate the registered byte-preserving transport when a later corrective gate reads native failure evidence.
171
+ * Returns no authority and never changes the original failure record or its hash. */
172
+ export function readFailedCheckpointDirtyTransport(store: FileStateStore, repository: string, task: TaskState,
173
+ stepId: string, sourceHead: string, sourceDirtyHash: string) {
174
+ const records = (task.dependencyProvenanceRecoveries ?? []).filter(record => {
175
+ const binding = (record as typeof record & { failedCheckpoint?: FailedCheckpointUpdateBinding }).failedCheckpoint;
176
+ return binding?.stepId === stepId && binding.sourceHead === sourceHead && binding.sourceDirtyHash === sourceDirtyHash;
177
+ });
178
+ if (records.length === 0) return null;
179
+ require(records.length === 1, 'Registered failed checkpoint is ambiguous.');
180
+ const record = records[0]!;
181
+ const binding = (record as typeof record & { failedCheckpoint: FailedCheckpointUpdateBinding }).failedCheckpoint;
182
+ require(task.planHash, 'Current Plan is required to validate the historical checkpoint.');
183
+ readVerifiedPlanArtifact(store.taskRoot(task.projectId, task.id), binding.sourcePlanHash, task.planHash);
184
+ require(binding.protocol === 'failed-checkpoint-update-v1' && task.systemCommits.includes(record.commitSha)
185
+ && record.packageVersion === FAILED_CHECKPOINT_TARGET && record.packageName === 'codex-workflow-v2'
186
+ && record.parentCommitSha === sourceHead && JSON.stringify(record.files) === JSON.stringify(['package-lock.json','package.json']),
187
+ 'Registered failed checkpoint dependency record is invalid.');
188
+ const inspected = inspectBoundedWorkflowDependencyCommit(repository, record.commitSha);
189
+ require(inspected?.parentCommitSha === sourceHead && inspected.packageVersion === FAILED_CHECKPOINT_TARGET,
190
+ 'Registered failed checkpoint no longer identifies a dependency-only commit.');
191
+ require(headCommit(repository) === record.commitSha, 'Dirty checkpoint requires its exact transported HEAD.');
192
+ const terminal = readRemediationEvents(store, task).filter(event => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
193
+ require(terminal?.eventId === binding.remediationEventId && terminal.eventHash === binding.remediationEventHash
194
+ && terminal.failureEvidence.completionCommit === sourceHead && terminal.failureEvidence.dirtyWorktreeHash === sourceDirtyHash,
195
+ 'Registered failed checkpoint does not bind the terminal native failure.');
196
+ const dirty = changedFiles(repository).sort();
197
+ require(JSON.stringify(dirty) === JSON.stringify(binding.dirtyFiles)
198
+ && failedCheckpointDirtyHash(repository, dirty, true) === sourceDirtyHash
199
+ && failedCheckpointDirtyHash(repository, dirty) === binding.targetDirtyHash,
200
+ 'Retained product bytes changed after the registered dependency transport.');
201
+ return { sourceHead, sourceDirtyHash, targetHead: record.commitSha, targetDirtyHash: binding.targetDirtyHash,
202
+ dirtyFiles: [...dirty], sourceReceiptHash: binding.sourceReceiptHash };
203
+ }
@@ -0,0 +1,61 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
11
+ import path from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { WorkflowError } from './errors.js';
14
+
15
+ export function ensureDirectory(directory: string): void {
16
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
17
+ if (lstatSync(directory).isSymbolicLink()) {
18
+ throw new WorkflowError('STATE_CORRUPT', `State directory must not be a symlink: ${directory}`);
19
+ }
20
+ }
21
+
22
+ export function readJson<T>(file: string): T {
23
+ try {
24
+ return JSON.parse(readFileSync(file, 'utf8')) as T;
25
+ } catch (error) {
26
+ throw new WorkflowError('STATE_CORRUPT', `Cannot read JSON state: ${file}`, {
27
+ cause: error instanceof Error ? error.message : String(error),
28
+ });
29
+ }
30
+ }
31
+
32
+ export function writeJsonAtomic(file: string, value: unknown): void {
33
+ const directory = path.dirname(file);
34
+ ensureDirectory(directory);
35
+ if (existsSync(file) && lstatSync(file).isSymbolicLink()) {
36
+ throw new WorkflowError('STATE_CORRUPT', `Refusing to replace state symlink: ${file}`);
37
+ }
38
+ const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
39
+ try {
40
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
41
+ chmodSync(temporary, 0o600);
42
+ renameSync(temporary, file);
43
+ } finally {
44
+ rmSync(temporary, { force: true });
45
+ }
46
+ }
47
+
48
+ export function writeTextAtomic(file: string, content: string): void {
49
+ const directory = path.dirname(file);
50
+ ensureDirectory(directory);
51
+ const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
52
+ try {
53
+ writeFileSync(temporary, content.endsWith('\n') ? content : `${content}\n`, {
54
+ encoding: 'utf8',
55
+ mode: 0o600,
56
+ });
57
+ renameSync(temporary, file);
58
+ } finally {
59
+ rmSync(temporary, { force: true });
60
+ }
61
+ }