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,1480 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type {
4
+ AdoptionPostureEvent,
5
+ AdoptionTaskBaseline,
6
+ GuardedRiskCategory,
7
+ PlanRiskAuditDecision,
8
+ PlanRiskAuditEvent,
9
+ PlanRiskAuditStepClassification,
10
+ TaskState,
11
+ TaskStatus,
12
+ } from '../contracts.js';
13
+ import { WorkflowError } from '../errors.js';
14
+ import { validateStructuralProductionEdgeAuthority } from '../domain/validation.js';
15
+ import type { FileStateStore } from '../state/store.js';
16
+ import { assertHash64, canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
17
+ import { createUlid } from '../ulid.js';
18
+
19
+ const PLAN_RISK_AUDIT_SIDECAR = 'plan-risk-audits.jsonl';
20
+ const ADOPTION_POSTURE_SIDECAR = 'adoption-posture.jsonl';
21
+ const PLAN_RISK_DECISIONS = new Set<PlanRiskAuditDecision>([
22
+ 'approved',
23
+ 'approved-with-rationale',
24
+ 'split-required',
25
+ 'stop-escalate',
26
+ ]);
27
+ const ADOPTION_POSTURE_EVENT_TYPES = new Set<AdoptionPostureEvent['eventType']>([
28
+ 'project-registration',
29
+ 'adoption-apply',
30
+ ]);
31
+ const ACTIVE_ADOPTION_TASK_STATUSES = new Set<TaskStatus>([
32
+ 'planning',
33
+ 'awaiting_execution_authorization',
34
+ 'ready',
35
+ 'in_progress',
36
+ 'validating',
37
+ 'needs_fix',
38
+ 'awaiting_final_acceptance',
39
+ 'blocked',
40
+ ]);
41
+ const PLAN_RISK_RECORDABLE_TASK_STATUSES = new Set<TaskStatus>([
42
+ 'awaiting_execution_authorization',
43
+ 'ready',
44
+ 'in_progress',
45
+ 'validating',
46
+ 'needs_fix',
47
+ 'awaiting_final_acceptance',
48
+ 'blocked',
49
+ ]);
50
+ const GUARDED_RISK_CATEGORY_ORDER: readonly GuardedRiskCategory[] = [
51
+ 'migration',
52
+ 'concurrency',
53
+ 'restart-replay',
54
+ 'crash-window',
55
+ 'external-provider',
56
+ 'runtime-composition',
57
+ 'security-boundary',
58
+ 'auth-secrets',
59
+ 'irreversible-execution',
60
+ ];
61
+ const GUARDED_RISK_CATEGORY_SET = new Set<GuardedRiskCategory>(GUARDED_RISK_CATEGORY_ORDER);
62
+ const GUARDED_RISK_CATEGORY_INDEX = new Map(
63
+ GUARDED_RISK_CATEGORY_ORDER.map((category, index) => [category, index]),
64
+ );
65
+ export const STRUCTURAL_PRODUCTION_EDGE_AUTHORITY = 'required-v1' as const;
66
+
67
+ export interface PlanRiskAuditBoundary {
68
+ scope: 'current-task-steps' | 'legacy-remaining-scope';
69
+ postureEventType: AdoptionPostureEvent['eventType'];
70
+ adoptionBoundaryAt: string;
71
+ expectedStepIds: string[];
72
+ }
73
+
74
+ export interface CurrentPlanRiskAudit extends PlanRiskAuditBoundary {
75
+ audit: PlanRiskAuditEvent;
76
+ reviewRequiredStepIds: string[];
77
+ }
78
+
79
+ export interface RecordPlanRiskAuditInput {
80
+ planner: string;
81
+ auditor: string;
82
+ summary: string;
83
+ decision: PlanRiskAuditDecision;
84
+ splitRationale: string | null;
85
+ stepClassifications: PlanRiskAuditStepClassification[];
86
+ }
87
+
88
+ export function validatePlanRiskAuditCandidate(
89
+ store: FileStateStore,
90
+ task: TaskState,
91
+ input: RecordPlanRiskAuditInput,
92
+ ): void {
93
+ const boundary = readPlanRiskAuditBoundary(store, task);
94
+ if (!boundary) {
95
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit requires an active adoption posture boundary.', {
96
+ taskId: task.id,
97
+ });
98
+ }
99
+ assertRecordableTask(task);
100
+ if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
101
+ throw new WorkflowError('STATE_CORRUPT', 'Candidate Task is missing Plan or Knowledge bindings for risk audit.', {
102
+ taskId: task.id,
103
+ });
104
+ }
105
+ validatePlanRiskAuditPayload(task, {
106
+ taskId: task.id,
107
+ taskRevision: task.revision + 1,
108
+ briefHash: task.briefHash,
109
+ planHash: task.planHash,
110
+ knowledgeMapRevision: task.knowledgeMapRevision,
111
+ knowledgeMapHash: task.knowledgeMapHash,
112
+ planner: input.planner,
113
+ auditor: input.auditor,
114
+ summary: input.summary,
115
+ decision: input.decision,
116
+ splitRationale: input.splitRationale,
117
+ stepClassifications: input.stepClassifications,
118
+ }, boundary.expectedStepIds, 'INVALID_ARGUMENT', {
119
+ taskId: task.id,
120
+ expectedStepIds: boundary.expectedStepIds,
121
+ });
122
+ }
123
+
124
+ export function readPlanRiskAuditBoundary(
125
+ store: FileStateStore,
126
+ task: TaskState,
127
+ ): PlanRiskAuditBoundary | null {
128
+ const posture = readCurrentAdoptionPosture(store, task.projectId);
129
+ if (!posture) return null;
130
+ const scope = derivePlanRiskScope(task, posture);
131
+ return {
132
+ scope: scope.scope,
133
+ postureEventType: posture.eventType,
134
+ adoptionBoundaryAt: posture.adoptionBoundaryAt,
135
+ expectedStepIds: scope.expectedStepIds,
136
+ };
137
+ }
138
+
139
+ export function readCurrentPlanRiskAudit(
140
+ store: FileStateStore,
141
+ task: TaskState,
142
+ options: { requireCurrentTaskRevision?: boolean } = {},
143
+ ): CurrentPlanRiskAudit | null {
144
+ return readCurrentAudit(store, task, options, false);
145
+ }
146
+
147
+ /** Planning evidence only: never use as execution or authorization authority. */
148
+ export function readCurrentPlanningObstructionAudit(store: FileStateStore, task: TaskState): CurrentPlanRiskAudit | null {
149
+ return readCurrentAudit(store, task, {}, true);
150
+ }
151
+
152
+ function readCurrentAudit(store: FileStateStore, task: TaskState,
153
+ options: { requireCurrentTaskRevision?: boolean }, allowBlocking: boolean): CurrentPlanRiskAudit | null {
154
+ const boundary = readPlanRiskAuditBoundary(store, task);
155
+ if (!boundary) return null;
156
+ const taskRoot = store.taskRoot(task.projectId, task.id);
157
+ const events = store.readSidecarEvents<PlanRiskAuditEvent>(
158
+ taskRoot,
159
+ PLAN_RISK_AUDIT_SIDECAR,
160
+ {
161
+ validateEvent: (event, context) => validateStoredPlanRiskAuditEvent(event, task, context.file, context.line),
162
+ },
163
+ );
164
+ const audit = events.at(-1);
165
+ if (!audit) {
166
+ throw new WorkflowError(
167
+ 'TRANSITION_BLOCKED',
168
+ `Task ${task.id} requires a current Plan Risk Audit before execution can proceed.`,
169
+ {
170
+ taskId: task.id,
171
+ scope: boundary.scope,
172
+ expectedStepIds: boundary.expectedStepIds,
173
+ sidecar: PLAN_RISK_AUDIT_SIDECAR,
174
+ },
175
+ );
176
+ }
177
+ const reviewRequiredStepIds = validateCurrentPlanRiskAudit(
178
+ store,
179
+ task,
180
+ audit,
181
+ boundary.expectedStepIds,
182
+ options,
183
+ allowBlocking,
184
+ );
185
+ return {
186
+ ...boundary,
187
+ audit,
188
+ reviewRequiredStepIds,
189
+ };
190
+ }
191
+
192
+ export function buildPlanRiskAuditEvent(
193
+ store: FileStateStore,
194
+ task: TaskState,
195
+ input: RecordPlanRiskAuditInput,
196
+ recordedAt: string,
197
+ now = Date.now(),
198
+ ): {
199
+ boundary: PlanRiskAuditBoundary;
200
+ entityRoot: string;
201
+ event: Omit<PlanRiskAuditEvent, 'previousEventHash' | 'eventHash'>;
202
+ } {
203
+ return buildPlanRiskAuditEventWithAuthority(store, task, input, recordedAt, now, true, true);
204
+ }
205
+
206
+ export function buildCandidatePlanRiskAuditEvent(
207
+ store: FileStateStore,
208
+ task: TaskState,
209
+ input: RecordPlanRiskAuditInput,
210
+ recordedAt: string,
211
+ now = Date.now(),
212
+ ): ReturnType<typeof buildPlanRiskAuditEvent> {
213
+ return buildPlanRiskAuditEventWithAuthority(store, task, input, recordedAt, now, true, false);
214
+ }
215
+
216
+ function buildPlanRiskAuditEventWithAuthority(
217
+ store: FileStateStore,
218
+ task: TaskState,
219
+ input: RecordPlanRiskAuditInput,
220
+ recordedAt: string,
221
+ now: number,
222
+ bindStructuralProductionEdgeAuthority: boolean,
223
+ requireStoredPlan: boolean,
224
+ ): {
225
+ boundary: PlanRiskAuditBoundary;
226
+ entityRoot: string;
227
+ event: Omit<PlanRiskAuditEvent, 'previousEventHash' | 'eventHash'>;
228
+ } {
229
+ const boundary = readPlanRiskAuditBoundary(store, task);
230
+ if (!boundary) {
231
+ throw new WorkflowError(
232
+ 'TRANSITION_BLOCKED',
233
+ 'Plan Risk Audit recording requires an active adoption posture boundary.',
234
+ { taskId: task.id, sidecar: ADOPTION_POSTURE_SIDECAR },
235
+ );
236
+ }
237
+ assertRecordableTask(task);
238
+ const entityRoot = store.taskRoot(task.projectId, task.id);
239
+ const briefHash = store.hashArtifact(entityRoot, 'brief.md');
240
+ const planHash = requireStoredPlan ? store.hashArtifact(entityRoot, 'plan.md') : task.planHash;
241
+ if (!planHash || task.briefHash !== briefHash || (requireStoredPlan && task.planHash !== planHash)) {
242
+ throw new WorkflowError(
243
+ 'STATE_CORRUPT',
244
+ 'Task state does not match the current Brief or Plan artifacts required for Plan Risk Audit recording.',
245
+ {
246
+ taskId: task.id,
247
+ stateBriefHash: task.briefHash,
248
+ currentBriefHash: briefHash,
249
+ statePlanHash: task.planHash,
250
+ currentPlanHash: planHash ?? null,
251
+ },
252
+ );
253
+ }
254
+ if (task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
255
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no current knowledge binding for Plan Risk Audit.`);
256
+ }
257
+ const event: Omit<PlanRiskAuditEvent, 'previousEventHash' | 'eventHash'> = {
258
+ eventId: `PRA-${createUlid(now)}`,
259
+ recordedAt,
260
+ taskId: task.id,
261
+ taskRevision: task.revision,
262
+ briefHash,
263
+ planHash,
264
+ knowledgeMapRevision: task.knowledgeMapRevision,
265
+ knowledgeMapHash: task.knowledgeMapHash,
266
+ planner: normalizeNonEmptyString(input.planner, 'Plan Risk Audit planner'),
267
+ auditor: normalizeNonEmptyString(input.auditor, 'Plan Risk Audit auditor'),
268
+ summary: normalizeNonEmptyString(input.summary, 'Plan Risk Audit summary'),
269
+ decision: normalizePlanRiskDecision(input.decision, 'INVALID_ARGUMENT', {
270
+ taskId: task.id,
271
+ value: input.decision,
272
+ }),
273
+ splitRationale: normalizeOptionalNonEmptyString(input.splitRationale, 'Plan Risk Audit splitRationale'),
274
+ ...(bindStructuralProductionEdgeAuthority
275
+ ? { structuralProductionEdgeAuthority: STRUCTURAL_PRODUCTION_EDGE_AUTHORITY }
276
+ : {}),
277
+ stepClassifications: normalizePlanRiskClassifications(
278
+ input.stepClassifications,
279
+ 'INVALID_ARGUMENT',
280
+ {
281
+ taskId: task.id,
282
+ },
283
+ ),
284
+ };
285
+ validatePlanRiskAuditPayload(
286
+ task,
287
+ event,
288
+ boundary.expectedStepIds,
289
+ 'INVALID_ARGUMENT',
290
+ {
291
+ taskId: task.id,
292
+ expectedStepIds: boundary.expectedStepIds,
293
+ },
294
+ true,
295
+ bindStructuralProductionEdgeAuthority,
296
+ );
297
+ return {
298
+ boundary,
299
+ entityRoot,
300
+ event,
301
+ };
302
+ }
303
+
304
+ export function appendCurrentPlanRiskAudit(
305
+ store: FileStateStore,
306
+ task: TaskState,
307
+ input: RecordPlanRiskAuditInput,
308
+ now: Date,
309
+ ): PlanRiskAuditEvent {
310
+ return appendPlanRiskAuditWithAuthority(store, task, input, now, true);
311
+ }
312
+
313
+ function appendPlanRiskAuditWithAuthority(
314
+ store: FileStateStore,
315
+ task: TaskState,
316
+ input: RecordPlanRiskAuditInput,
317
+ now: Date,
318
+ bindStructuralProductionEdgeAuthority: boolean,
319
+ ): PlanRiskAuditEvent {
320
+ const prepared = buildPlanRiskAuditEventWithAuthority(
321
+ store,
322
+ task,
323
+ input,
324
+ now.toISOString(),
325
+ now.getTime(),
326
+ bindStructuralProductionEdgeAuthority,
327
+ true,
328
+ );
329
+ const existingEvents = readPlanRiskAuditEvents(store, task);
330
+ assertPlanRiskAuditAppendAllowed(task, prepared.boundary, existingEvents);
331
+ return store.appendSidecarEvent<PlanRiskAuditEvent>(
332
+ prepared.entityRoot,
333
+ PLAN_RISK_AUDIT_SIDECAR,
334
+ prepared.event,
335
+ {
336
+ validateEvent: (event, context) => validateStoredPlanRiskAuditEvent(event, task, context.file, context.line),
337
+ },
338
+ );
339
+ }
340
+
341
+ export function validateCorrectiveReplanRiskAuditPayload(
342
+ task: TaskState,
343
+ event: Pick<PlanRiskAuditEvent,
344
+ 'taskId' | 'taskRevision' | 'briefHash' | 'planHash' | 'knowledgeMapRevision' | 'knowledgeMapHash'
345
+ | 'planner' | 'auditor' | 'summary' | 'decision' | 'splitRationale' | 'stepClassifications'>,
346
+ expectedStepIds: readonly string[],
347
+ ): void {
348
+ validatePlanRiskAuditPayload(task, event, expectedStepIds, 'INVALID_ARGUMENT', { taskId: task.id });
349
+ if (event.decision === 'split-required' || event.decision === 'stop-escalate') {
350
+ throw new WorkflowError('TRANSITION_BLOCKED', `Plan Risk Audit decision ${event.decision} blocks corrective replan.`);
351
+ }
352
+ }
353
+
354
+ export function appendReboundPlanRiskAudit(
355
+ store: FileStateStore,
356
+ sourceTask: TaskState,
357
+ reboundTask: TaskState,
358
+ sourceAudit: CurrentPlanRiskAudit,
359
+ now: Date,
360
+ ): PlanRiskAuditEvent {
361
+ assertReboundPlanRiskAuditAllowed(store, sourceTask, reboundTask, sourceAudit);
362
+ const sourceHasStructuralProductionEdgeAuthority =
363
+ sourceAudit.audit.structuralProductionEdgeAuthority !== undefined
364
+ || Object.hasOwn(sourceTask, 'requiresProductionEdges')
365
+ || Object.hasOwn(sourceTask, 'productionEdges')
366
+ || Object.hasOwn(reboundTask, 'requiresProductionEdges')
367
+ || Object.hasOwn(reboundTask, 'productionEdges');
368
+ return appendPlanRiskAuditWithAuthority(
369
+ store,
370
+ reboundTask,
371
+ {
372
+ planner: sourceAudit.audit.planner,
373
+ auditor: sourceAudit.audit.auditor,
374
+ summary: `${sourceAudit.audit.summary} Rebound automatically after content-only Task context refresh.`,
375
+ decision: sourceAudit.audit.decision,
376
+ splitRationale: sourceAudit.audit.splitRationale,
377
+ stepClassifications: sourceAudit.audit.stepClassifications.map((classification) => ({
378
+ ...classification,
379
+ categories: [...classification.categories],
380
+ failureModes: [...classification.failureModes],
381
+ requiredEvidence: [...classification.requiredEvidence],
382
+ ...(classification.proofObligations
383
+ ? { proofObligations: classification.proofObligations.map((obligation) => ({ ...obligation })) }
384
+ : {}),
385
+ })),
386
+ },
387
+ now,
388
+ sourceHasStructuralProductionEdgeAuthority,
389
+ );
390
+ }
391
+
392
+ function readCurrentAdoptionPosture(store: FileStateStore, projectId: string): AdoptionPostureEvent | null {
393
+ const sidecarFile = path.join(store.projectRoot(projectId), ADOPTION_POSTURE_SIDECAR);
394
+ if (!existsSync(sidecarFile)) return null;
395
+ const events = store.readSidecarEvents<AdoptionPostureEvent>(
396
+ store.projectRoot(projectId),
397
+ ADOPTION_POSTURE_SIDECAR,
398
+ {
399
+ validateEvent: (event, context) => validateStoredAdoptionPostureEvent(event, projectId, context.file, context.line),
400
+ },
401
+ );
402
+ return events.at(-1) ?? null;
403
+ }
404
+
405
+ function assertReboundPlanRiskAuditAllowed(
406
+ store: FileStateStore,
407
+ sourceTask: TaskState,
408
+ reboundTask: TaskState,
409
+ sourceAudit: CurrentPlanRiskAudit,
410
+ ): void {
411
+ if (sourceTask.projectId !== reboundTask.projectId || sourceTask.id !== reboundTask.id) {
412
+ throw new WorkflowError(
413
+ 'TRANSITION_BLOCKED',
414
+ 'Plan Risk Audit rebound requires the same Task identity before and after content-only refresh.',
415
+ {
416
+ sourceTaskId: sourceTask.id,
417
+ reboundTaskId: reboundTask.id,
418
+ sourceProjectId: sourceTask.projectId,
419
+ reboundProjectId: reboundTask.projectId,
420
+ },
421
+ );
422
+ }
423
+ if (sourceTask.briefHash !== reboundTask.briefHash) {
424
+ throw new WorkflowError(
425
+ 'TRANSITION_BLOCKED',
426
+ 'Plan Risk Audit rebound is limited to content-only refresh and cannot cross a Brief change.',
427
+ {
428
+ taskId: sourceTask.id,
429
+ sourceBriefHash: sourceTask.briefHash,
430
+ reboundBriefHash: reboundTask.briefHash,
431
+ },
432
+ );
433
+ }
434
+ if (sourceTask.planHash === reboundTask.planHash) {
435
+ throw new WorkflowError(
436
+ 'TRANSITION_BLOCKED',
437
+ 'Plan Risk Audit rebound requires a current rebound Plan binding.',
438
+ {
439
+ taskId: sourceTask.id,
440
+ planHash: sourceTask.planHash,
441
+ },
442
+ );
443
+ }
444
+ if (
445
+ sourceTask.knowledgeMapRevision === reboundTask.knowledgeMapRevision
446
+ && sourceTask.knowledgeMapHash === reboundTask.knowledgeMapHash
447
+ ) {
448
+ throw new WorkflowError(
449
+ 'TRANSITION_BLOCKED',
450
+ 'Plan Risk Audit rebound requires a changed Task knowledge binding.',
451
+ {
452
+ taskId: sourceTask.id,
453
+ knowledgeMapRevision: sourceTask.knowledgeMapRevision,
454
+ knowledgeMapHash: sourceTask.knowledgeMapHash,
455
+ },
456
+ );
457
+ }
458
+ const reboundBoundary = readPlanRiskAuditBoundary(store, reboundTask);
459
+ if (!reboundBoundary) {
460
+ throw new WorkflowError(
461
+ 'TRANSITION_BLOCKED',
462
+ 'Plan Risk Audit rebound requires an active adoption posture boundary.',
463
+ {
464
+ taskId: reboundTask.id,
465
+ sidecar: ADOPTION_POSTURE_SIDECAR,
466
+ },
467
+ );
468
+ }
469
+ assertExactIdSet(
470
+ sourceAudit.expectedStepIds,
471
+ reboundBoundary.expectedStepIds,
472
+ 'TRANSITION_BLOCKED',
473
+ 'Plan Risk Audit rebound requires the exact same classified Step set after content-only refresh.',
474
+ {
475
+ taskId: reboundTask.id,
476
+ sourceExpectedStepIds: sourceAudit.expectedStepIds,
477
+ reboundExpectedStepIds: reboundBoundary.expectedStepIds,
478
+ },
479
+ );
480
+ const sourceStepDefinitions = buildTaskStepDefinitionHashes(sourceTask);
481
+ const reboundStepDefinitions = buildTaskStepDefinitionHashes(reboundTask);
482
+ assertExactIdSet(
483
+ [...sourceStepDefinitions.keys()],
484
+ [...reboundStepDefinitions.keys()],
485
+ 'TRANSITION_BLOCKED',
486
+ 'Plan Risk Audit rebound requires the same Task Step identifiers after content-only refresh.',
487
+ {
488
+ taskId: reboundTask.id,
489
+ sourceStepIds: [...sourceStepDefinitions.keys()],
490
+ reboundStepIds: [...reboundStepDefinitions.keys()],
491
+ },
492
+ );
493
+ for (const [stepId, sourceDefinitionHash] of sourceStepDefinitions.entries()) {
494
+ if (reboundStepDefinitions.get(stepId) === sourceDefinitionHash) continue;
495
+ throw new WorkflowError(
496
+ 'TRANSITION_BLOCKED',
497
+ 'Plan Risk Audit rebound is limited to content-only refresh and cannot cross a Step definition change.',
498
+ {
499
+ taskId: reboundTask.id,
500
+ stepId,
501
+ sourceDefinitionHash,
502
+ reboundDefinitionHash: reboundStepDefinitions.get(stepId) ?? null,
503
+ },
504
+ );
505
+ }
506
+ }
507
+
508
+ function derivePlanRiskScope(
509
+ task: TaskState,
510
+ posture: AdoptionPostureEvent,
511
+ ): {
512
+ scope: PlanRiskAuditBoundary['scope'];
513
+ expectedStepIds: string[];
514
+ } {
515
+ const currentTaskStepIds = task.steps.map((step) => step.id);
516
+ assertUniqueValues(currentTaskStepIds, 'STATE_CORRUPT', 'Task contains duplicate Step identifiers.', {
517
+ taskId: task.id,
518
+ stepIds: currentTaskStepIds,
519
+ });
520
+ const baseline = posture.activeTasks.find((candidate) => candidate.taskId === task.id) ?? null;
521
+ if (!baseline) {
522
+ return {
523
+ scope: 'current-task-steps',
524
+ expectedStepIds: currentTaskStepIds,
525
+ };
526
+ }
527
+ validateAdoptionBaseline(task, baseline, currentTaskStepIds, posture);
528
+ const completedStepIdSet = new Set(baseline.completedSteps.map((step) => step.stepId));
529
+ return {
530
+ scope: 'legacy-remaining-scope',
531
+ expectedStepIds: currentTaskStepIds.filter((stepId) => !completedStepIdSet.has(stepId)),
532
+ };
533
+ }
534
+
535
+ function validateStoredAdoptionPostureEvent(
536
+ event: AdoptionPostureEvent,
537
+ expectedProjectId: string,
538
+ file: string,
539
+ line: number,
540
+ ): void {
541
+ assertPlainObject(event, 'Adoption posture event', file, line);
542
+ const eventType = normalizeAdoptionPostureEventType(event.eventType, file, line, event.eventId);
543
+ if (event.projectId !== expectedProjectId) {
544
+ throw new WorkflowError(
545
+ 'STATE_CORRUPT',
546
+ 'Adoption posture event project binding does not match its project root.',
547
+ {
548
+ file,
549
+ line,
550
+ eventId: event.eventId,
551
+ expectedProjectId,
552
+ actualProjectId: event.projectId,
553
+ },
554
+ );
555
+ }
556
+ normalizeNonEmptyString(event.adoptionBoundaryAt, 'Adoption posture adoptionBoundaryAt', {
557
+ code: 'STATE_CORRUPT',
558
+ file,
559
+ line,
560
+ eventId: event.eventId,
561
+ });
562
+ if (!Array.isArray(event.activeTasks)) {
563
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption posture activeTasks must be an array.', {
564
+ file,
565
+ line,
566
+ eventId: event.eventId,
567
+ eventType,
568
+ });
569
+ }
570
+ assertUniqueValues(
571
+ event.activeTasks.map((candidate) => candidate?.taskId),
572
+ 'STATE_CORRUPT',
573
+ 'Adoption posture contains duplicate active Task baselines.',
574
+ { file, line, eventId: event.eventId },
575
+ );
576
+ for (const baseline of event.activeTasks) {
577
+ validateStoredAdoptionTaskBaseline(baseline, file, line, event.eventId);
578
+ }
579
+ }
580
+
581
+ function validateStoredAdoptionTaskBaseline(
582
+ baseline: AdoptionTaskBaseline,
583
+ file: string,
584
+ line: number,
585
+ eventId: string,
586
+ ): void {
587
+ assertPlainObject(baseline, 'Adoption Task baseline', file, line);
588
+ normalizeNonEmptyString(baseline.taskId, 'Adoption Task baseline taskId', {
589
+ code: 'STATE_CORRUPT',
590
+ file,
591
+ line,
592
+ eventId,
593
+ });
594
+ assertPositiveInteger(baseline.taskRevision, 'Adoption Task baseline taskRevision', file, line, eventId);
595
+ if (!ACTIVE_ADOPTION_TASK_STATUSES.has(baseline.status)) {
596
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline status is invalid.', {
597
+ file,
598
+ line,
599
+ eventId,
600
+ taskId: baseline.taskId,
601
+ status: baseline.status,
602
+ });
603
+ }
604
+ if (!Array.isArray(baseline.completedSteps)) {
605
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline completedSteps must be an array.', {
606
+ file,
607
+ line,
608
+ eventId,
609
+ taskId: baseline.taskId,
610
+ });
611
+ }
612
+ if (!Array.isArray(baseline.remainingStepIds)) {
613
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline remainingStepIds must be an array.', {
614
+ file,
615
+ line,
616
+ eventId,
617
+ taskId: baseline.taskId,
618
+ });
619
+ }
620
+ const completedStepIds: string[] = [];
621
+ for (const completed of baseline.completedSteps) {
622
+ assertPlainObject(completed, 'Adoption completed Step baseline', file, line);
623
+ const stepId = normalizeNonEmptyString(completed.stepId, 'Adoption completed Step stepId', {
624
+ code: 'STATE_CORRUPT',
625
+ file,
626
+ line,
627
+ eventId,
628
+ taskId: baseline.taskId,
629
+ });
630
+ completedStepIds.push(stepId);
631
+ if (completed.status !== 'completed' && completed.status !== 'skipped') {
632
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption completed Step status is invalid.', {
633
+ file,
634
+ line,
635
+ eventId,
636
+ taskId: baseline.taskId,
637
+ stepId,
638
+ status: completed.status,
639
+ });
640
+ }
641
+ if (completed.evidenceHash !== null) {
642
+ if (typeof completed.evidenceHash !== 'string') {
643
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption completed Step evidenceHash must be null or a hash.', {
644
+ file,
645
+ line,
646
+ eventId,
647
+ taskId: baseline.taskId,
648
+ stepId,
649
+ evidenceHash: completed.evidenceHash,
650
+ });
651
+ }
652
+ assertHash64(completed.evidenceHash, 'Adoption completed Step evidenceHash');
653
+ }
654
+ }
655
+ assertUniqueValues(completedStepIds, 'STATE_CORRUPT', 'Adoption completed Step baseline must not contain duplicates.', {
656
+ file,
657
+ line,
658
+ eventId,
659
+ taskId: baseline.taskId,
660
+ });
661
+ const remainingStepIds = baseline.remainingStepIds.map((stepId) =>
662
+ normalizeNonEmptyString(stepId, 'Adoption remaining Step id', {
663
+ code: 'STATE_CORRUPT',
664
+ file,
665
+ line,
666
+ eventId,
667
+ taskId: baseline.taskId,
668
+ }),
669
+ );
670
+ assertUniqueValues(remainingStepIds, 'STATE_CORRUPT', 'Adoption posture remaining Step baseline must not contain duplicates.', {
671
+ file,
672
+ line,
673
+ eventId,
674
+ taskId: baseline.taskId,
675
+ });
676
+ const completedSet = new Set(completedStepIds);
677
+ const overlap = remainingStepIds.filter((stepId) => completedSet.has(stepId));
678
+ if (overlap.length > 0) {
679
+ throw new WorkflowError(
680
+ 'STATE_CORRUPT',
681
+ 'Adoption Task baseline cannot list the same Step as both completed and remaining.',
682
+ {
683
+ file,
684
+ line,
685
+ eventId,
686
+ taskId: baseline.taskId,
687
+ overlappingStepIds: overlap,
688
+ },
689
+ );
690
+ }
691
+ }
692
+
693
+ function validateAdoptionBaseline(
694
+ task: TaskState,
695
+ baseline: AdoptionTaskBaseline,
696
+ currentTaskStepIds: string[],
697
+ posture: AdoptionPostureEvent,
698
+ ): void {
699
+ if (baseline.taskRevision > task.revision) {
700
+ throw new WorkflowError(
701
+ 'STATE_CORRUPT',
702
+ 'Adoption posture baseline refers to a future Task revision.',
703
+ {
704
+ taskId: task.id,
705
+ baselineTaskRevision: baseline.taskRevision,
706
+ currentTaskRevision: task.revision,
707
+ adoptionBoundaryAt: posture.adoptionBoundaryAt,
708
+ },
709
+ );
710
+ }
711
+ const currentTaskStepIdSet = new Set(currentTaskStepIds);
712
+ const completedStepIds = baseline.completedSteps.map((step) => step.stepId);
713
+ const completedStepIdSet = new Set(completedStepIds);
714
+ const missingCompletedFromTask = completedStepIds.filter((stepId) => !currentTaskStepIdSet.has(stepId));
715
+ if (missingCompletedFromTask.length > 0) {
716
+ throw new WorkflowError(
717
+ 'TRANSITION_BLOCKED',
718
+ 'Adoption posture remaining-scope baseline no longer matches the current Task Step set.',
719
+ {
720
+ taskId: task.id,
721
+ adoptionBoundaryAt: posture.adoptionBoundaryAt,
722
+ missingCompletedStepIds: missingCompletedFromTask,
723
+ },
724
+ );
725
+ }
726
+ const overlap = baseline.remainingStepIds.filter((stepId) => completedStepIdSet.has(stepId));
727
+ if (overlap.length > 0) {
728
+ throw new WorkflowError(
729
+ 'STATE_CORRUPT',
730
+ 'Adoption posture baseline cannot mark the same Step as completed and remaining.',
731
+ {
732
+ taskId: task.id,
733
+ adoptionBoundaryAt: posture.adoptionBoundaryAt,
734
+ overlappingStepIds: overlap,
735
+ },
736
+ );
737
+ }
738
+ if (baseline.taskRevision === task.revision) {
739
+ const missingRemainingFromTask = baseline.remainingStepIds.filter((stepId) => !currentTaskStepIdSet.has(stepId));
740
+ if (missingRemainingFromTask.length > 0) {
741
+ throw new WorkflowError(
742
+ 'TRANSITION_BLOCKED',
743
+ 'Adoption posture remaining-scope baseline no longer matches the Task Step set at the posture boundary revision.',
744
+ {
745
+ taskId: task.id,
746
+ adoptionBoundaryAt: posture.adoptionBoundaryAt,
747
+ missingStepIds: missingRemainingFromTask,
748
+ },
749
+ );
750
+ }
751
+ const partitionIds = [...completedStepIds, ...baseline.remainingStepIds];
752
+ assertExactIdSet(
753
+ currentTaskStepIds,
754
+ partitionIds,
755
+ 'STATE_CORRUPT',
756
+ 'Adoption posture baseline must partition the Task Step set at the posture boundary revision.',
757
+ {
758
+ taskId: task.id,
759
+ adoptionBoundaryAt: posture.adoptionBoundaryAt,
760
+ currentTaskRevision: task.revision,
761
+ completedStepIds,
762
+ remainingStepIds: baseline.remainingStepIds,
763
+ },
764
+ );
765
+ }
766
+ }
767
+
768
+ function validateStoredPlanRiskAuditEvent(
769
+ event: PlanRiskAuditEvent,
770
+ task: TaskState,
771
+ file: string,
772
+ line: number,
773
+ ): void {
774
+ const hasStructuralAuthorityBoundary = event.planHash === task.planHash && (
775
+ event.structuralProductionEdgeAuthority !== undefined
776
+ || Object.hasOwn(task, 'requiresProductionEdges')
777
+ || Object.hasOwn(task, 'productionEdges')
778
+ );
779
+ validatePlanRiskAuditPayload(task, event, null, 'STATE_CORRUPT', {
780
+ file,
781
+ line,
782
+ eventId: event.eventId,
783
+ }, false, hasStructuralAuthorityBoundary);
784
+ }
785
+
786
+ function validatePlanRiskAuditPayload(
787
+ task: TaskState,
788
+ event: Pick<
789
+ PlanRiskAuditEvent,
790
+ | 'taskId'
791
+ | 'taskRevision'
792
+ | 'briefHash'
793
+ | 'planHash'
794
+ | 'knowledgeMapRevision'
795
+ | 'knowledgeMapHash'
796
+ | 'planner'
797
+ | 'auditor'
798
+ | 'summary'
799
+ | 'decision'
800
+ | 'splitRationale'
801
+ | 'stepClassifications'
802
+ | 'structuralProductionEdgeAuthority'
803
+ >,
804
+ expectedStepIds: readonly string[] | null,
805
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
806
+ details: Record<string, unknown>,
807
+ requireCurrentStepChecks = true,
808
+ requireStructuralProductionEdgeAuthority = true,
809
+ ): void {
810
+ if (event.taskId !== task.id) {
811
+ throw new WorkflowError(errorCode, 'Plan Risk Audit task binding does not match its task root.', {
812
+ ...details,
813
+ expectedTaskId: task.id,
814
+ actualTaskId: event.taskId,
815
+ });
816
+ }
817
+ assertPositiveInteger(event.taskRevision, 'Plan Risk Audit taskRevision', details.file, details.line, details.eventId, errorCode);
818
+ assertHash64String(event.briefHash, 'Plan Risk Audit briefHash', errorCode, details);
819
+ assertHash64String(event.planHash, 'Plan Risk Audit planHash', errorCode, details);
820
+ assertPositiveInteger(
821
+ event.knowledgeMapRevision,
822
+ 'Plan Risk Audit knowledgeMapRevision',
823
+ details.file,
824
+ details.line,
825
+ details.eventId,
826
+ errorCode,
827
+ );
828
+ assertHash64String(event.knowledgeMapHash, 'Plan Risk Audit knowledgeMapHash', errorCode, details);
829
+ const planner = normalizeNonEmptyString(event.planner, 'Plan Risk Audit planner', { code: errorCode, ...details });
830
+ const auditor = normalizeNonEmptyString(event.auditor, 'Plan Risk Audit auditor', { code: errorCode, ...details });
831
+ if (planner === auditor) {
832
+ throw new WorkflowError(errorCode, 'Plan Risk Audit auditor must be independent from the Plan author.', {
833
+ ...details,
834
+ taskId: task.id,
835
+ planner,
836
+ auditor,
837
+ });
838
+ }
839
+ normalizeNonEmptyString(event.summary, 'Plan Risk Audit summary', { code: errorCode, ...details });
840
+ normalizePlanRiskDecision(event.decision, errorCode, details);
841
+ normalizeOptionalNonEmptyString(event.splitRationale, 'Plan Risk Audit splitRationale', { code: errorCode, ...details });
842
+ if (
843
+ event.structuralProductionEdgeAuthority !== undefined
844
+ && event.structuralProductionEdgeAuthority !== STRUCTURAL_PRODUCTION_EDGE_AUTHORITY
845
+ ) {
846
+ throw new WorkflowError(errorCode, 'Plan Risk Audit structural production-edge authority marker is invalid.', {
847
+ ...details,
848
+ structuralProductionEdgeAuthority: event.structuralProductionEdgeAuthority,
849
+ });
850
+ }
851
+ if (!Array.isArray(event.stepClassifications)) {
852
+ throw new WorkflowError(errorCode, 'Plan Risk Audit stepClassifications must be an array.', details);
853
+ }
854
+ validateStepClassificationSet(
855
+ task,
856
+ event.stepClassifications,
857
+ event.decision,
858
+ event.splitRationale,
859
+ expectedStepIds,
860
+ errorCode,
861
+ details,
862
+ requireCurrentStepChecks,
863
+ );
864
+ if (requireStructuralProductionEdgeAuthority) {
865
+ validateStructuralProductionEdgeAuthority(task, event.stepClassifications);
866
+ }
867
+ }
868
+
869
+ function validateStepClassificationSet(
870
+ task: TaskState,
871
+ classifications: readonly PlanRiskAuditStepClassification[],
872
+ decision: PlanRiskAuditDecision,
873
+ splitRationale: string | null,
874
+ expectedStepIds: readonly string[] | null,
875
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
876
+ details: Record<string, unknown>,
877
+ requireCurrentStepChecks = true,
878
+ ): void {
879
+ const currentTaskStepIds = new Set(task.steps.map((step) => step.id));
880
+ const normalizedStepIds = classifications.map((classification) =>
881
+ normalizeNonEmptyString(classification?.stepId, 'Plan Risk Audit classification stepId', {
882
+ code: errorCode,
883
+ ...details,
884
+ }),
885
+ );
886
+ assertUniqueValues(normalizedStepIds, errorCode, 'Plan Risk Audit must classify each Step exactly once.', {
887
+ ...details,
888
+ taskId: task.id,
889
+ });
890
+ if (expectedStepIds) {
891
+ assertExactIdSet(expectedStepIds, normalizedStepIds, errorCode, 'Plan Risk Audit Step classifications do not match the required Step set.', {
892
+ ...details,
893
+ taskId: task.id,
894
+ expectedStepIds,
895
+ actualStepIds: normalizedStepIds,
896
+ });
897
+ }
898
+ let reviewRequiredCount = 0;
899
+ const guardedCategories = new Set<GuardedRiskCategory>();
900
+ for (const classification of classifications) {
901
+ if (expectedStepIds && !currentTaskStepIds.has(classification.stepId)) {
902
+ throw new WorkflowError(
903
+ errorCode,
904
+ 'Plan Risk Audit references a Step that is not present in the Task.',
905
+ {
906
+ ...details,
907
+ taskId: task.id,
908
+ stepId: classification.stepId,
909
+ },
910
+ );
911
+ }
912
+ if (classification.reviewRequired) reviewRequiredCount += 1;
913
+ validateStepClassification(task, classification, errorCode, details, requireCurrentStepChecks);
914
+ classification.categories.forEach((category) => guardedCategories.add(category));
915
+ }
916
+ if ((decision === 'split-required' || decision === 'stop-escalate') && reviewRequiredCount === 0) {
917
+ throw new WorkflowError(
918
+ errorCode,
919
+ `Plan Risk Audit decision ${decision} requires at least one guarded Step.`,
920
+ {
921
+ ...details,
922
+ taskId: task.id,
923
+ },
924
+ );
925
+ }
926
+ const coupledRecoveryDomains = [
927
+ 'restart-replay',
928
+ 'crash-window',
929
+ 'runtime-composition',
930
+ ].filter((category) => guardedCategories.has(category as GuardedRiskCategory));
931
+ const requiresSplitRationale = guardedCategories.has('migration') && coupledRecoveryDomains.length > 0;
932
+ if (requiresSplitRationale && decision === 'approved') {
933
+ throw new WorkflowError(
934
+ errorCode,
935
+ 'A positively approved Plan combining migration with recovery or runtime-composition risk requires approved-with-rationale.',
936
+ {
937
+ ...details,
938
+ taskId: task.id,
939
+ guardedCategories: [...guardedCategories],
940
+ },
941
+ );
942
+ }
943
+ if (decision === 'approved-with-rationale' && !splitRationale) {
944
+ throw new WorkflowError(errorCode, 'Plan Risk Audit approved-with-rationale requires an explicit split rationale.', {
945
+ ...details,
946
+ taskId: task.id,
947
+ });
948
+ }
949
+ if (decision !== 'approved-with-rationale' && splitRationale !== null) {
950
+ throw new WorkflowError(errorCode, 'Plan Risk Audit splitRationale is only valid with approved-with-rationale.', {
951
+ ...details,
952
+ taskId: task.id,
953
+ decision,
954
+ });
955
+ }
956
+ }
957
+
958
+ function validateCurrentPlanRiskAudit(
959
+ store: FileStateStore,
960
+ task: TaskState,
961
+ audit: PlanRiskAuditEvent,
962
+ expectedStepIds: readonly string[],
963
+ options: { requireCurrentTaskRevision?: boolean },
964
+ allowBlocking = false,
965
+ ): string[] {
966
+ const taskRoot = store.taskRoot(task.projectId, task.id);
967
+ const briefHash = store.hashArtifact(taskRoot, 'brief.md');
968
+ const planHash = store.hashArtifact(taskRoot, 'plan.md');
969
+ if (audit.briefHash !== briefHash || audit.planHash !== planHash) {
970
+ throw new WorkflowError(
971
+ 'TRANSITION_BLOCKED',
972
+ 'Plan Risk Audit is stale for the current Brief or Plan.',
973
+ {
974
+ taskId: task.id,
975
+ auditEventId: audit.eventId,
976
+ auditBriefHash: audit.briefHash,
977
+ currentBriefHash: briefHash,
978
+ auditPlanHash: audit.planHash,
979
+ currentPlanHash: planHash,
980
+ },
981
+ );
982
+ }
983
+ if (audit.knowledgeMapRevision !== task.knowledgeMapRevision || audit.knowledgeMapHash !== task.knowledgeMapHash) {
984
+ throw new WorkflowError(
985
+ 'TRANSITION_BLOCKED',
986
+ 'Plan Risk Audit is stale for the current Task knowledge binding.',
987
+ {
988
+ taskId: task.id,
989
+ auditEventId: audit.eventId,
990
+ auditKnowledgeMapRevision: audit.knowledgeMapRevision,
991
+ currentKnowledgeMapRevision: task.knowledgeMapRevision,
992
+ auditKnowledgeMapHash: audit.knowledgeMapHash,
993
+ currentKnowledgeMapHash: task.knowledgeMapHash,
994
+ },
995
+ );
996
+ }
997
+ if (options.requireCurrentTaskRevision && audit.taskRevision !== task.revision) {
998
+ throw new WorkflowError(
999
+ 'TRANSITION_BLOCKED',
1000
+ 'Plan Risk Audit must bind the current Task revision before execution authorization.',
1001
+ {
1002
+ taskId: task.id,
1003
+ auditEventId: audit.eventId,
1004
+ auditTaskRevision: audit.taskRevision,
1005
+ currentTaskRevision: task.revision,
1006
+ },
1007
+ );
1008
+ }
1009
+ if (audit.taskRevision > task.revision) {
1010
+ throw new WorkflowError(
1011
+ 'STATE_CORRUPT',
1012
+ 'Plan Risk Audit refers to a future Task revision.',
1013
+ {
1014
+ taskId: task.id,
1015
+ auditEventId: audit.eventId,
1016
+ auditTaskRevision: audit.taskRevision,
1017
+ currentTaskRevision: task.revision,
1018
+ },
1019
+ );
1020
+ }
1021
+ validateStepClassificationSet(
1022
+ task,
1023
+ audit.stepClassifications,
1024
+ audit.decision,
1025
+ audit.splitRationale,
1026
+ expectedStepIds,
1027
+ 'TRANSITION_BLOCKED',
1028
+ {
1029
+ taskId: task.id,
1030
+ auditEventId: audit.eventId,
1031
+ expectedStepIds,
1032
+ },
1033
+ );
1034
+ if (!allowBlocking && (audit.decision === 'split-required' || audit.decision === 'stop-escalate')) {
1035
+ throw new WorkflowError(
1036
+ 'TRANSITION_BLOCKED',
1037
+ `Plan Risk Audit decision ${audit.decision} blocks execution authorization.`,
1038
+ {
1039
+ taskId: task.id,
1040
+ auditEventId: audit.eventId,
1041
+ decision: audit.decision,
1042
+ requiredAction: audit.decision === 'split-required'
1043
+ ? 'Split the guarded work into separate Tasks and replace the Plan.'
1044
+ : 'Escalate the Plan to the coordinator before continuing.',
1045
+ },
1046
+ );
1047
+ }
1048
+ return audit.stepClassifications
1049
+ .filter((classification) => classification.reviewRequired)
1050
+ .map((classification) => classification.stepId);
1051
+ }
1052
+
1053
+ function validateStepClassification(
1054
+ task: TaskState,
1055
+ classification: PlanRiskAuditStepClassification,
1056
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1057
+ details: Record<string, unknown>,
1058
+ requireCurrentStepChecks = true,
1059
+ ): void {
1060
+ if (!Array.isArray(classification.categories)) {
1061
+ throw new WorkflowError(errorCode, 'Plan Risk Audit categories must be an array.', {
1062
+ ...details,
1063
+ taskId: task.id,
1064
+ stepId: classification.stepId,
1065
+ });
1066
+ }
1067
+ if (typeof classification.reviewRequired !== 'boolean') {
1068
+ throw new WorkflowError(errorCode, 'Plan Risk Audit reviewRequired must be boolean.', {
1069
+ ...details,
1070
+ taskId: task.id,
1071
+ stepId: classification.stepId,
1072
+ reviewRequired: classification.reviewRequired,
1073
+ });
1074
+ }
1075
+ const normalizedCategories = classification.categories.map((category) =>
1076
+ normalizeGuardedRiskCategory(category, errorCode, {
1077
+ ...details,
1078
+ taskId: task.id,
1079
+ stepId: classification.stepId,
1080
+ }),
1081
+ );
1082
+ assertUniqueValues(normalizedCategories, errorCode, 'Plan Risk Audit categories must be unique per Step.', {
1083
+ ...details,
1084
+ taskId: task.id,
1085
+ stepId: classification.stepId,
1086
+ categories: normalizedCategories,
1087
+ });
1088
+ const canonicalCategories = [...normalizedCategories]
1089
+ .sort((left, right) => compareGuardedRiskCategories(left, right));
1090
+ if (JSON.stringify(canonicalCategories) !== JSON.stringify(normalizedCategories)) {
1091
+ throw new WorkflowError(
1092
+ errorCode,
1093
+ 'Plan Risk Audit categories must be unique and in canonical order.',
1094
+ {
1095
+ ...details,
1096
+ taskId: task.id,
1097
+ stepId: classification.stepId,
1098
+ expectedCategories: canonicalCategories,
1099
+ actualCategories: normalizedCategories,
1100
+ },
1101
+ );
1102
+ }
1103
+ const expectedReviewRequired = normalizedCategories.length > 0;
1104
+ if (classification.reviewRequired !== expectedReviewRequired) {
1105
+ throw new WorkflowError(
1106
+ errorCode,
1107
+ 'Plan Risk Audit reviewRequired must match category presence for each Step.',
1108
+ {
1109
+ ...details,
1110
+ taskId: task.id,
1111
+ stepId: classification.stepId,
1112
+ categories: normalizedCategories,
1113
+ reviewRequired: classification.reviewRequired,
1114
+ },
1115
+ );
1116
+ }
1117
+ const failureModes = normalizeStringArray(
1118
+ classification.failureModes,
1119
+ 'Plan Risk Audit failureModes',
1120
+ errorCode,
1121
+ { ...details, taskId: task.id, stepId: classification.stepId },
1122
+ );
1123
+ const requiredEvidence = normalizeStringArray(
1124
+ classification.requiredEvidence,
1125
+ 'Plan Risk Audit requiredEvidence',
1126
+ errorCode,
1127
+ { ...details, taskId: task.id, stepId: classification.stepId },
1128
+ );
1129
+ const proofObligations = normalizeProofObligations(
1130
+ classification.proofObligations,
1131
+ expectedReviewRequired,
1132
+ errorCode,
1133
+ { ...details, taskId: task.id, stepId: classification.stepId },
1134
+ );
1135
+ if (expectedReviewRequired && (failureModes.length === 0 || requiredEvidence.length === 0)) {
1136
+ throw new WorkflowError(errorCode, 'Every guarded Step must declare failure modes and required evidence.', {
1137
+ ...details,
1138
+ taskId: task.id,
1139
+ stepId: classification.stepId,
1140
+ });
1141
+ }
1142
+ if (!expectedReviewRequired && (failureModes.length > 0 || requiredEvidence.length > 0)) {
1143
+ throw new WorkflowError(errorCode, 'An unguarded Step cannot declare guarded failure modes or evidence.', {
1144
+ ...details,
1145
+ taskId: task.id,
1146
+ stepId: classification.stepId,
1147
+ });
1148
+ }
1149
+ if (proofObligations) {
1150
+ const coveredFailureModes = proofObligations.map((obligation) => obligation.failureMode);
1151
+ assertUniqueValues(
1152
+ coveredFailureModes,
1153
+ errorCode,
1154
+ 'Plan Risk Audit proof obligations must cover each failure mode only once.',
1155
+ { ...details, taskId: task.id, stepId: classification.stepId },
1156
+ );
1157
+ assertExactIdSet(
1158
+ failureModes,
1159
+ coveredFailureModes,
1160
+ errorCode,
1161
+ 'Plan Risk Audit proof obligations must cover every failure mode exactly once.',
1162
+ { ...details, taskId: task.id, stepId: classification.stepId },
1163
+ );
1164
+ const unsupportedEvidence = proofObligations
1165
+ .map((obligation) => obligation.evidence)
1166
+ .filter((evidence) => !requiredEvidence.includes(evidence));
1167
+ if (unsupportedEvidence.length > 0) {
1168
+ throw new WorkflowError(errorCode, 'Plan Risk Audit proof obligations must use declared required evidence.', {
1169
+ ...details,
1170
+ taskId: task.id,
1171
+ stepId: classification.stepId,
1172
+ unsupportedEvidence,
1173
+ });
1174
+ }
1175
+ }
1176
+ const step = requireCurrentStepChecks
1177
+ ? task.steps.find((candidate) => candidate.id === classification.stepId)
1178
+ : undefined;
1179
+ if (step) {
1180
+ const missingChecks = requiredEvidence.filter((evidence) => !step.checks.includes(evidence));
1181
+ if (missingChecks.length > 0) {
1182
+ throw new WorkflowError(errorCode, 'Plan Risk Audit required evidence must be backed by exact executable Step checks.', {
1183
+ ...details,
1184
+ taskId: task.id,
1185
+ stepId: classification.stepId,
1186
+ missingChecks,
1187
+ declaredChecks: step.checks,
1188
+ });
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ function normalizeProofObligations(
1194
+ value: unknown,
1195
+ guarded: boolean,
1196
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1197
+ details: Record<string, unknown>,
1198
+ ): Array<{ failureMode: string; counterexample: string; expectedResult: string; evidence: string }> | null {
1199
+ if (value === undefined) {
1200
+ if (guarded && errorCode === 'INVALID_ARGUMENT') {
1201
+ throw new WorkflowError(
1202
+ errorCode,
1203
+ 'Every newly recorded guarded Step must declare executable proof obligations.',
1204
+ details,
1205
+ );
1206
+ }
1207
+ // Alpha.6 sidecars predate proof obligations. They remain readable so strict-review
1208
+ // recovery can finish, but every newly recorded alpha.7 audit must provide them.
1209
+ return null;
1210
+ }
1211
+ if (!Array.isArray(value)) {
1212
+ throw new WorkflowError(errorCode, 'Plan Risk Audit proofObligations must be an array.', details);
1213
+ }
1214
+ if (!guarded && value.length > 0) {
1215
+ throw new WorkflowError(errorCode, 'An unguarded Step cannot declare proof obligations.', details);
1216
+ }
1217
+ if (guarded && value.length === 0) {
1218
+ throw new WorkflowError(errorCode, 'Every guarded Step must declare executable proof obligations.', details);
1219
+ }
1220
+ return value.map((candidate, index) => {
1221
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
1222
+ throw new WorkflowError(errorCode, 'Plan Risk Audit proof obligation must be an object.', { ...details, index });
1223
+ }
1224
+ const obligation = candidate as Record<string, unknown>;
1225
+ const keys = Object.keys(obligation).sort();
1226
+ const expectedKeys = ['counterexample', 'evidence', 'expectedResult', 'failureMode'];
1227
+ if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
1228
+ throw new WorkflowError(errorCode, 'Plan Risk Audit proof obligation fields are invalid.', {
1229
+ ...details,
1230
+ index,
1231
+ expectedKeys,
1232
+ actualKeys: keys,
1233
+ });
1234
+ }
1235
+ return {
1236
+ failureMode: normalizeNonEmptyString(obligation.failureMode, 'Proof obligation failureMode', { code: errorCode, ...details, index }),
1237
+ counterexample: normalizeNonEmptyString(obligation.counterexample, 'Proof obligation counterexample', { code: errorCode, ...details, index }),
1238
+ expectedResult: normalizeNonEmptyString(obligation.expectedResult, 'Proof obligation expectedResult', { code: errorCode, ...details, index }),
1239
+ evidence: normalizeNonEmptyString(obligation.evidence, 'Proof obligation evidence', { code: errorCode, ...details, index }),
1240
+ };
1241
+ });
1242
+ }
1243
+
1244
+ function buildTaskStepDefinitionHashes(task: TaskState): Map<string, string> {
1245
+ return new Map(task.steps.map((step) => [step.id, sha256Hex(canonicalJsonStringify(stripTaskStepRuntime(step)))]));
1246
+ }
1247
+
1248
+ function stripTaskStepRuntime(step: TaskState['steps'][number]) {
1249
+ const { status: _status, evidence: _evidence, failurePause: _failurePause, ...definition } = step;
1250
+ return definition;
1251
+ }
1252
+
1253
+ function assertRecordableTask(task: TaskState): void {
1254
+ if (!PLAN_RISK_RECORDABLE_TASK_STATUSES.has(task.status)) {
1255
+ throw new WorkflowError(
1256
+ 'TRANSITION_BLOCKED',
1257
+ `Task ${task.id} cannot record a Plan Risk Audit while ${task.status}.`,
1258
+ { taskId: task.id, status: task.status },
1259
+ );
1260
+ }
1261
+ if (!task.planHash || task.steps.length === 0) {
1262
+ throw new WorkflowError(
1263
+ 'TRANSITION_BLOCKED',
1264
+ 'Plan Risk Audit recording requires a current Task Plan with explicit Steps.',
1265
+ { taskId: task.id, status: task.status },
1266
+ );
1267
+ }
1268
+ }
1269
+
1270
+ export function readPlanRiskAuditEvents(store: FileStateStore, task: TaskState): PlanRiskAuditEvent[] {
1271
+ const taskRoot = store.taskRoot(task.projectId, task.id);
1272
+ return store.readSidecarEvents<PlanRiskAuditEvent>(
1273
+ taskRoot,
1274
+ PLAN_RISK_AUDIT_SIDECAR,
1275
+ {
1276
+ validateEvent: (event, context) => validateStoredPlanRiskAuditEvent(event, task, context.file, context.line),
1277
+ },
1278
+ );
1279
+ }
1280
+
1281
+ function assertPlanRiskAuditAppendAllowed(
1282
+ task: TaskState,
1283
+ boundary: PlanRiskAuditBoundary,
1284
+ existingEvents: readonly PlanRiskAuditEvent[],
1285
+ ): void {
1286
+ if (task.status === 'awaiting_execution_authorization') return;
1287
+ if (existingEvents.length > 0) {
1288
+ throw new WorkflowError(
1289
+ 'TRANSITION_BLOCKED',
1290
+ 'Replacing a Plan Risk Audit requires returning the Task to awaiting_execution_authorization through a legitimate replan.',
1291
+ {
1292
+ taskId: task.id,
1293
+ status: task.status,
1294
+ existingAuditCount: existingEvents.length,
1295
+ },
1296
+ );
1297
+ }
1298
+ if (boundary.scope !== 'legacy-remaining-scope') {
1299
+ throw new WorkflowError(
1300
+ 'TRANSITION_BLOCKED',
1301
+ 'Mid-execution Plan Risk Audit recording is limited to the first legacy bootstrap audit.',
1302
+ {
1303
+ taskId: task.id,
1304
+ status: task.status,
1305
+ scope: boundary.scope,
1306
+ postureEventType: boundary.postureEventType,
1307
+ },
1308
+ );
1309
+ }
1310
+ }
1311
+
1312
+ function normalizePlanRiskDecision(
1313
+ decision: unknown,
1314
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1315
+ details: Record<string, unknown>,
1316
+ ): PlanRiskAuditDecision {
1317
+ if (typeof decision !== 'string' || !PLAN_RISK_DECISIONS.has(decision as PlanRiskAuditDecision)) {
1318
+ throw new WorkflowError(errorCode, 'Plan Risk Audit decision is invalid.', {
1319
+ ...details,
1320
+ decision,
1321
+ });
1322
+ }
1323
+ return decision as PlanRiskAuditDecision;
1324
+ }
1325
+
1326
+ function normalizeAdoptionPostureEventType(
1327
+ value: unknown,
1328
+ file: string,
1329
+ line: number,
1330
+ eventId: string,
1331
+ ): AdoptionPostureEvent['eventType'] {
1332
+ if (typeof value !== 'string' || !ADOPTION_POSTURE_EVENT_TYPES.has(value as AdoptionPostureEvent['eventType'])) {
1333
+ throw new WorkflowError('STATE_CORRUPT', 'Adoption posture eventType is invalid.', {
1334
+ file,
1335
+ line,
1336
+ eventId,
1337
+ eventType: value,
1338
+ });
1339
+ }
1340
+ return value as AdoptionPostureEvent['eventType'];
1341
+ }
1342
+
1343
+ function normalizeGuardedRiskCategory(
1344
+ value: unknown,
1345
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1346
+ details: Record<string, unknown>,
1347
+ ): GuardedRiskCategory {
1348
+ if (typeof value !== 'string' || !GUARDED_RISK_CATEGORY_SET.has(value as GuardedRiskCategory)) {
1349
+ throw new WorkflowError(errorCode, 'Plan Risk Audit category is invalid.', {
1350
+ ...details,
1351
+ category: value,
1352
+ });
1353
+ }
1354
+ return value as GuardedRiskCategory;
1355
+ }
1356
+
1357
+ function normalizePlanRiskClassifications(
1358
+ value: unknown,
1359
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1360
+ details: Record<string, unknown>,
1361
+ ): PlanRiskAuditStepClassification[] {
1362
+ if (!Array.isArray(value)) {
1363
+ throw new WorkflowError(errorCode, 'Plan Risk Audit stepClassifications must be an array.', details);
1364
+ }
1365
+ return value as PlanRiskAuditStepClassification[];
1366
+ }
1367
+
1368
+ function normalizeStringArray(
1369
+ value: unknown,
1370
+ label: string,
1371
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1372
+ details: Record<string, unknown>,
1373
+ ): string[] {
1374
+ if (!Array.isArray(value)) {
1375
+ throw new WorkflowError(errorCode, `${label} must be an array.`, details);
1376
+ }
1377
+ const normalized = value.map((entry) => normalizeNonEmptyString(entry, label, { code: errorCode, ...details }));
1378
+ assertUniqueValues(normalized, errorCode, `${label} must contain unique values.`, details);
1379
+ return normalized;
1380
+ }
1381
+
1382
+ function normalizeOptionalNonEmptyString(
1383
+ value: unknown,
1384
+ label: string,
1385
+ details: { code?: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED' } & Record<string, unknown> = {},
1386
+ ): string | null {
1387
+ if (value === null) return null;
1388
+ return normalizeNonEmptyString(value, label, details);
1389
+ }
1390
+
1391
+ function normalizeNonEmptyString(
1392
+ value: unknown,
1393
+ label: string,
1394
+ details: { code?: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED' } & Record<string, unknown> = {},
1395
+ ): string {
1396
+ const errorCode = details.code ?? 'INVALID_ARGUMENT';
1397
+ if (typeof value !== 'string' || !value.trim()) {
1398
+ throw new WorkflowError(errorCode, `${label} must be a non-empty string.`, details);
1399
+ }
1400
+ return value.trim();
1401
+ }
1402
+
1403
+ function assertPositiveInteger(
1404
+ value: unknown,
1405
+ label: string,
1406
+ file?: unknown,
1407
+ line?: unknown,
1408
+ eventId?: unknown,
1409
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED' = 'STATE_CORRUPT',
1410
+ ): asserts value is number {
1411
+ if (!Number.isInteger(value) || Number(value) < 1) {
1412
+ throw new WorkflowError(errorCode, `${label} must be a positive integer.`, {
1413
+ ...(file !== undefined ? { file } : {}),
1414
+ ...(line !== undefined ? { line } : {}),
1415
+ ...(eventId !== undefined ? { eventId } : {}),
1416
+ value,
1417
+ });
1418
+ }
1419
+ }
1420
+
1421
+ function assertHash64String(
1422
+ value: unknown,
1423
+ label: string,
1424
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1425
+ details: Record<string, unknown>,
1426
+ ): void {
1427
+ if (typeof value !== 'string') {
1428
+ throw new WorkflowError(errorCode, `${label} must be a sha256 hex digest.`, {
1429
+ ...details,
1430
+ value,
1431
+ });
1432
+ }
1433
+ try {
1434
+ assertHash64(value, label);
1435
+ } catch (error) {
1436
+ if (error instanceof WorkflowError) {
1437
+ throw new WorkflowError(errorCode, error.message, { ...details, ...error.details });
1438
+ }
1439
+ throw error;
1440
+ }
1441
+ }
1442
+
1443
+ function assertPlainObject(value: unknown, label: string, file: string, line: number): void {
1444
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1445
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be an object.`, { file, line });
1446
+ }
1447
+ }
1448
+
1449
+ function compareGuardedRiskCategories(left: GuardedRiskCategory, right: GuardedRiskCategory): number {
1450
+ return (GUARDED_RISK_CATEGORY_INDEX.get(left) ?? Number.MAX_SAFE_INTEGER)
1451
+ - (GUARDED_RISK_CATEGORY_INDEX.get(right) ?? Number.MAX_SAFE_INTEGER);
1452
+ }
1453
+
1454
+ function assertUniqueValues(
1455
+ values: readonly unknown[],
1456
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1457
+ message: string,
1458
+ details: Record<string, unknown>,
1459
+ ): void {
1460
+ const unique = new Set(values);
1461
+ if (unique.size !== values.length) {
1462
+ throw new WorkflowError(errorCode, message, details);
1463
+ }
1464
+ }
1465
+
1466
+ function assertExactIdSet(
1467
+ expected: readonly string[],
1468
+ actual: readonly string[],
1469
+ errorCode: 'STATE_CORRUPT' | 'INVALID_ARGUMENT' | 'TRANSITION_BLOCKED',
1470
+ message: string,
1471
+ details: Record<string, unknown>,
1472
+ ): void {
1473
+ if (expected.length !== actual.length) {
1474
+ throw new WorkflowError(errorCode, message, details);
1475
+ }
1476
+ const expectedSet = new Set(expected);
1477
+ if (actual.some((value) => !expectedSet.has(value))) {
1478
+ throw new WorkflowError(errorCode, message, details);
1479
+ }
1480
+ }