codex-workflow-v2 2.0.0-beta.13.9 → 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 (271) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +89 -29
  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/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 +6 -21
  30. package/dist/src/alpha6/remediation.js +161 -476
  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/beta1/project-transaction.d.ts +3 -1
  36. package/dist/src/beta1/project-transaction.js +6 -2
  37. package/dist/src/beta1/project-transaction.js.map +1 -1
  38. package/dist/src/checks/runner.d.ts +26 -0
  39. package/dist/src/checks/runner.js +223 -0
  40. package/dist/src/checks/runner.js.map +1 -0
  41. package/dist/src/checks/task-sync.d.ts +10 -0
  42. package/dist/src/checks/task-sync.js +41 -0
  43. package/dist/src/checks/task-sync.js.map +1 -0
  44. package/dist/src/checks/worker.d.ts +1 -0
  45. package/dist/src/checks/worker.js +154 -0
  46. package/dist/src/checks/worker.js.map +1 -0
  47. package/dist/src/cli-actions.d.ts +2 -2
  48. package/dist/src/cli-actions.js +3 -15
  49. package/dist/src/cli-actions.js.map +1 -1
  50. package/dist/src/cli.js +14 -98
  51. package/dist/src/cli.js.map +1 -1
  52. package/dist/src/contracts.d.ts +35 -32
  53. package/dist/src/dependency-provenance.d.ts +2 -2
  54. package/dist/src/dependency-provenance.js +12 -69
  55. package/dist/src/dependency-provenance.js.map +1 -1
  56. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  57. package/dist/src/domain/base-sync-conflict.js +32 -0
  58. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  59. package/dist/src/domain/step-start-admission.d.ts +4 -0
  60. package/dist/src/domain/step-start-admission.js +9 -0
  61. package/dist/src/domain/step-start-admission.js.map +1 -0
  62. package/dist/src/domain/virgin-registration.d.ts +3 -0
  63. package/dist/src/domain/virgin-registration.js +65 -0
  64. package/dist/src/domain/virgin-registration.js.map +1 -0
  65. package/dist/src/errors.d.ts +1 -1
  66. package/dist/src/errors.js.map +1 -1
  67. package/dist/src/gateway-handshake.js +0 -8
  68. package/dist/src/gateway-handshake.js.map +1 -1
  69. package/dist/src/git.d.ts +3 -1
  70. package/dist/src/git.js +41 -14
  71. package/dist/src/git.js.map +1 -1
  72. package/dist/src/graph.js +25 -3
  73. package/dist/src/graph.js.map +1 -1
  74. package/dist/src/index.d.ts +1 -0
  75. package/dist/src/navigation-actions.d.ts +9 -0
  76. package/dist/src/navigation-actions.js +72 -0
  77. package/dist/src/navigation-actions.js.map +1 -0
  78. package/dist/src/navigation-update-artifact.d.ts +5 -0
  79. package/dist/src/navigation-update-artifact.js +227 -0
  80. package/dist/src/navigation-update-artifact.js.map +1 -0
  81. package/dist/src/navigation-update.d.ts +39 -0
  82. package/dist/src/navigation-update.js +82 -0
  83. package/dist/src/navigation-update.js.map +1 -0
  84. package/dist/src/observation.js +52 -29
  85. package/dist/src/observation.js.map +1 -1
  86. package/dist/src/observed-routes.js +5 -6
  87. package/dist/src/observed-routes.js.map +1 -1
  88. package/dist/src/pending-review-update.d.ts +0 -13
  89. package/dist/src/pending-review-update.js +1 -6
  90. package/dist/src/pending-review-update.js.map +1 -1
  91. package/dist/src/reviewer.d.ts +1 -1
  92. package/dist/src/reviewer.js +20 -17
  93. package/dist/src/reviewer.js.map +1 -1
  94. package/dist/src/runtime.d.ts +3 -0
  95. package/dist/src/runtime.js +6 -0
  96. package/dist/src/runtime.js.map +1 -0
  97. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  98. package/dist/src/state/corrective-replan-executor.js +37 -1
  99. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  100. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  101. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  102. package/dist/src/state/corrective-replan-public.js +1 -1
  103. package/dist/src/state/corrective-replan-public.js.map +1 -1
  104. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  105. package/dist/src/state/corrective-replan-transaction.js +12 -11
  106. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  107. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  108. package/dist/src/state/corrective-yield-transaction.js +11 -10
  109. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  110. package/dist/src/state/lock.d.ts +4 -0
  111. package/dist/src/state/lock.js +21 -0
  112. package/dist/src/state/lock.js.map +1 -1
  113. package/dist/src/version.d.ts +1 -1
  114. package/dist/src/version.js +1 -1
  115. package/dist/src/version.js.map +1 -1
  116. package/dist/src/workflow-blocker-route.d.ts +8 -0
  117. package/dist/src/workflow-blocker-route.js +106 -0
  118. package/dist/src/workflow-blocker-route.js.map +1 -0
  119. package/dist/src/workflow.d.ts +48 -63
  120. package/dist/src/workflow.js +666 -1460
  121. package/dist/src/workflow.js.map +1 -1
  122. package/docs/autonomy-guardrails.md +26 -304
  123. package/docs/decisions.md +14 -112
  124. package/docs/development-flow.md +35 -238
  125. package/docs/project-memory.md +31 -50
  126. package/docs/release-app-evidence.md +190 -0
  127. package/docs/release.md +95 -388
  128. package/docs/updating-existing-project.md +19 -717
  129. package/package.json +11 -13
  130. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  131. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  132. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  133. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  134. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  135. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  136. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  137. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
  138. package/references/state-machine.md +4 -4
  139. package/roles/technical-planner.md +1 -1
  140. package/schemas/corrective-decision-event.schema.json +3 -1
  141. package/schemas/project-knowledge-map.schema.json +3 -1
  142. package/schemas/remediation-event.schema.json +10 -1
  143. package/schemas/task.schema.json +46 -2
  144. package/schemas/transition-payloads.schema.json +16 -1
  145. package/src/alpha6/adoption.ts +1123 -0
  146. package/src/alpha6/captured-check-evidence.ts +132 -0
  147. package/src/alpha6/check-support-anchor.ts +128 -0
  148. package/src/alpha6/component-owner.ts +168 -0
  149. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  150. package/src/alpha6/downstream-proof.ts +520 -0
  151. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  152. package/src/alpha6/handoff.ts +1443 -0
  153. package/src/alpha6/journal.ts +473 -0
  154. package/src/alpha6/literal-test-invocation.ts +84 -0
  155. package/src/alpha6/mechanical-feasibility.ts +488 -0
  156. package/src/alpha6/milestone.ts +2192 -0
  157. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  158. package/src/alpha6/npm-check-contract.ts +47 -0
  159. package/src/alpha6/plan-integrity.ts +298 -0
  160. package/src/alpha6/plan-risk.ts +1480 -0
  161. package/src/alpha6/preexecution-replan.ts +187 -0
  162. package/src/alpha6/remediation-cause.ts +98 -0
  163. package/src/alpha6/remediation.ts +2438 -0
  164. package/src/alpha6/review.ts +1198 -0
  165. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  166. package/src/alpha6/store-sidecars.ts +335 -0
  167. package/src/alpha7/autonomy.ts +411 -0
  168. package/src/alpha7/corrective-recovery.ts +1332 -0
  169. package/src/artifacts.ts +130 -0
  170. package/src/beta1/project-transaction.ts +355 -0
  171. package/src/change-explanation.ts +153 -0
  172. package/src/checks/runner.ts +245 -0
  173. package/src/checks/task-sync.ts +41 -0
  174. package/src/checks/worker.ts +149 -0
  175. package/src/cli-actions.ts +107 -0
  176. package/src/cli.ts +1457 -0
  177. package/src/contracts.ts +1494 -0
  178. package/src/credential-output.ts +89 -0
  179. package/src/credential-transport.ts +215 -0
  180. package/src/delegation.ts +190 -0
  181. package/src/dependency-provenance.ts +472 -0
  182. package/src/diagnostics.ts +93 -0
  183. package/src/domain/base-sync-conflict.ts +35 -0
  184. package/src/domain/completed-step-carryover.ts +76 -0
  185. package/src/domain/discovery.ts +27 -0
  186. package/src/domain/plan-semantics.ts +58 -0
  187. package/src/domain/step-start-admission.ts +10 -0
  188. package/src/domain/validation.ts +177 -0
  189. package/src/domain/virgin-registration.ts +46 -0
  190. package/src/errors.ts +24 -0
  191. package/src/fs-utils.ts +61 -0
  192. package/src/gateway-handshake.ts +95 -0
  193. package/src/git.ts +183 -0
  194. package/src/graph.ts +342 -0
  195. package/src/historical-step-provenance.ts +136 -0
  196. package/src/index.ts +23 -0
  197. package/src/lifecycle/canonical-hash.ts +28 -0
  198. package/src/lifecycle/catalog.ts +202 -0
  199. package/src/lifecycle/compiler-inspection.ts +29 -0
  200. package/src/lifecycle/core-static-readiness.ts +132 -0
  201. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  202. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  203. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  204. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  205. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  206. package/src/lifecycle/corrective-replan.ts +843 -0
  207. package/src/lifecycle/evaluator.ts +48 -0
  208. package/src/lifecycle/fingerprint.ts +488 -0
  209. package/src/lifecycle/immutable.ts +8 -0
  210. package/src/lifecycle/implementation-table.ts +118 -0
  211. package/src/lifecycle/index.ts +8 -0
  212. package/src/lifecycle/schema-artifact.ts +263 -0
  213. package/src/lifecycle/semantic-registry.ts +572 -0
  214. package/src/lifecycle/types.ts +838 -0
  215. package/src/memory.ts +273 -0
  216. package/src/migration.ts +161 -0
  217. package/src/navigation-actions.ts +70 -0
  218. package/src/navigation-update-artifact.ts +198 -0
  219. package/src/navigation-update.ts +121 -0
  220. package/src/observation.ts +225 -0
  221. package/src/observed-routes.ts +660 -0
  222. package/src/operational-contract.ts +125 -0
  223. package/src/pending-review-update.ts +175 -0
  224. package/src/repository.ts +99 -0
  225. package/src/reviewer.ts +1879 -0
  226. package/src/runtime.ts +6 -0
  227. package/src/state/corrective-replan-executor.ts +818 -0
  228. package/src/state/corrective-replan-public-schema.ts +83 -0
  229. package/src/state/corrective-replan-public.ts +908 -0
  230. package/src/state/corrective-replan-transaction.ts +949 -0
  231. package/src/state/corrective-yield-executor.ts +327 -0
  232. package/src/state/corrective-yield-transaction.ts +730 -0
  233. package/src/state/lock.ts +902 -0
  234. package/src/state/store.ts +567 -0
  235. package/src/transition-core.ts +330 -0
  236. package/src/ulid.ts +24 -0
  237. package/src/version.ts +2 -0
  238. package/src/workflow-blocker-route.ts +109 -0
  239. package/src/workflow.ts +10172 -0
  240. package/docs/alpha7.1-implementation-brief.md +0 -268
  241. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  242. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  243. package/docs/beta1-stabilization-brief.md +0 -165
  244. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  245. package/docs/beta13.2-signal-review-recovery.md +0 -38
  246. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  247. package/docs/change-model.md +0 -118
  248. package/docs/delegated-approval.md +0 -254
  249. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  250. package/docs/pdf/README.md +0 -24
  251. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  252. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  253. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  254. package/docs/pdf/requirements.txt +0 -1
  255. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  256. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  257. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  258. package/docs/pending-review-update.md +0 -15
  259. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  260. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  261. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  262. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  263. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  264. package/docs/problem-briefs/README.md +0 -120
  265. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  266. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  267. package/docs/split-required-recovery.md +0 -47
  268. package/docs/stable-release-defect-register.md +0 -730
  269. package/docs/validation-report.md +0 -182
  270. package/scripts/generate-pdf-docs.py +0 -524
  271. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,1332 @@
1
+ import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type {
4
+ AuthorizationEvent,
5
+ CorrectiveDecisionEvent,
6
+ CorrectiveDecisionRecoveryEvent,
7
+ DelegationGrant,
8
+ DependencyProvenanceRecoveryRecord,
9
+ PlanRiskAuditEvent,
10
+ ProjectKnowledgeMap,
11
+ StopEscalateOverrideEvent,
12
+ TaskKnowledgeRebindRecord,
13
+ TaskState,
14
+ WriterLease,
15
+ } from '../contracts.js';
16
+ import { hashDelegationPolicy } from '../delegation.js';
17
+ import { WorkflowError } from '../errors.js';
18
+ import { hashKnowledgeMap } from '../memory.js';
19
+ import type { FileStateStore } from '../state/store.js';
20
+ import { createUlid } from '../ulid.js';
21
+ import { readTaskC1Posture, readTaskHandoffEvents } from '../alpha6/handoff.js';
22
+ import { readCurrentPlanRiskAudit, readPlanRiskAuditEvents } from '../alpha6/plan-risk.js';
23
+ import { readMilestoneAutonomyEvents, type MilestoneAutonomyEvent } from './autonomy.js';
24
+ import {
25
+ assertCorrectiveAuditorIndependence,
26
+ readCorrectiveDecisionEvents,
27
+ readRemediationEvents,
28
+ readStopEscalateOverrideEvents,
29
+ } from '../alpha6/remediation.js';
30
+ import { assertHash64, canonicalJsonStringify, sha256Hex } from '../alpha6/store-sidecars.js';
31
+ import { gitIsAncestor, runGit } from '../repository.js';
32
+ import { PACKAGE_NAME } from '../version.js';
33
+
34
+ export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
35
+ const RECOVERY_LOCK = '.corrective-decision-recovery.lock';
36
+ const KNOWLEDGE_LINE = /^- Knowledge map: revision ([1-9]\d*), hash ([a-f0-9]{64})$/gm;
37
+ const KNOWLEDGE_SENTINEL = '- Knowledge map: revision <context>, hash <context>';
38
+ const REFRESH_REASONS = new Set([
39
+ 'Delegated content-only context refresh.',
40
+ 'Delegated Plan-bounded supporting-source context refresh.',
41
+ ]);
42
+
43
+ export interface CorrectiveRecoveryAssessment {
44
+ task: TaskState;
45
+ decision: CorrectiveDecisionEvent;
46
+ directRebind: TaskKnowledgeRebindRecord;
47
+ semanticPlanHash: string;
48
+ sourceAudit: PlanRiskAuditEvent;
49
+ reboundAudit: PlanRiskAuditEvent;
50
+ knowledgeMapApprovalFingerprint: string;
51
+ taskAuthorizationFingerprint: string;
52
+ delegationGrantFingerprint: string;
53
+ c1EvidenceFingerprint: string;
54
+ evidenceFingerprint: string;
55
+ }
56
+
57
+ export interface StopOverrideContextRebindAssessment {
58
+ task: TaskState;
59
+ override: StopEscalateOverrideEvent;
60
+ stopDecision: CorrectiveDecisionEvent;
61
+ semanticPlanHash: string;
62
+ }
63
+
64
+ interface ContextOnlyPlanRebindEvidence {
65
+ directRebind: TaskKnowledgeRebindRecord;
66
+ semanticPlanHash: string;
67
+ sourceAudit: PlanRiskAuditEvent;
68
+ reboundAudit: PlanRiskAuditEvent;
69
+ planRiskAudits: PlanRiskAuditEvent[];
70
+ knowledgeMap: ProjectKnowledgeMap;
71
+ authorization: AuthorizationEvent;
72
+ grant: DelegationGrant;
73
+ autonomyContract: MilestoneAutonomyEvent | null;
74
+ }
75
+
76
+ interface AppendOnlyContextPlanRebindEvidence {
77
+ rebindChain: TaskKnowledgeRebindRecord[];
78
+ semanticPlanHash: string;
79
+ sourceAudit: PlanRiskAuditEvent;
80
+ reboundAudit: PlanRiskAuditEvent;
81
+ planRiskAudits: PlanRiskAuditEvent[];
82
+ knowledgeMap: ProjectKnowledgeMap;
83
+ authorization: AuthorizationEvent;
84
+ grant: DelegationGrant;
85
+ autonomyContract: MilestoneAutonomyEvent | null;
86
+ }
87
+
88
+ interface RecoveryChainSnapshot {
89
+ fingerprint: string;
90
+ matchingEvent: CorrectiveDecisionRecoveryEvent | null;
91
+ }
92
+
93
+ export function readCorrectiveDecisionRecoveryEvents(
94
+ store: FileStateStore,
95
+ task: TaskState,
96
+ ): CorrectiveDecisionRecoveryEvent[] {
97
+ return store.readSidecarEvents<CorrectiveDecisionRecoveryEvent>(
98
+ store.taskRoot(task.projectId, task.id),
99
+ CORRECTIVE_DECISION_RECOVERIES_SIDECAR,
100
+ { validateEvent: (event, context) => validateRecoveryEvent(event, task, context.file, context.line) },
101
+ );
102
+ }
103
+
104
+ export function assessCorrectiveDecisionRecovery(
105
+ store: FileStateStore,
106
+ task: TaskState,
107
+ stepId: string,
108
+ actor: string,
109
+ writerToken: string | null,
110
+ ): CorrectiveRecoveryAssessment {
111
+ const normalizedActor = actor.trim();
112
+ if (!normalizedActor) throw new WorkflowError('INVALID_ARGUMENT', 'Corrective recovery actor is required.');
113
+ if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
114
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
115
+ }
116
+ const step = task.steps.find((candidate) => candidate.id === stepId);
117
+ if (!step) throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
118
+ if (!['planned', 'failed'].includes(step.status)) {
119
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective recovery is unavailable while ${stepId} is ${step.status}.`);
120
+ }
121
+ const remediationEvents = readRemediationEvents(store, task);
122
+ const stepRemediations = remediationEvents.filter((event) => event.stepId === stepId);
123
+ if (stepRemediations.length !== 2) {
124
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery requires exactly two failed remediation attempts.', {
125
+ taskId: task.id,
126
+ stepId,
127
+ attemptCount: stepRemediations.length,
128
+ });
129
+ }
130
+ const correctiveDecisions = readCorrectiveDecisionEvents(store, task);
131
+ const matchingDecisions = correctiveDecisions.filter((event) =>
132
+ event.stepId === stepId && event.triggeringAttemptCount === 3,
133
+ );
134
+ if (matchingDecisions.length !== 1) {
135
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery requires one decision for the exact Step/ordinal.', {
136
+ taskId: task.id,
137
+ stepId,
138
+ matchingDecisionIds: matchingDecisions.map((event) => event.eventId),
139
+ });
140
+ }
141
+ const decision = matchingDecisions[0]!;
142
+ if (decision.decision !== 'continue-fix') {
143
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Only continue-fix may be recovered after context refresh.', {
144
+ taskId: task.id,
145
+ stepId,
146
+ decision: decision.decision,
147
+ });
148
+ }
149
+ if (decision.planHash === task.planHash) {
150
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective decision already binds the current Plan; recovery is unnecessary.');
151
+ }
152
+ assertCorrectiveAuditorIndependence(store, task, stepId, decision.auditor);
153
+
154
+ const rebindEvidence = assessContextOnlyPlanRebindEvidence(store, task, stepId, decision);
155
+ const {
156
+ directRebind,
157
+ semanticPlanHash,
158
+ sourceAudit,
159
+ reboundAudit,
160
+ planRiskAudits,
161
+ knowledgeMap,
162
+ authorization,
163
+ grant,
164
+ autonomyContract,
165
+ } = rebindEvidence;
166
+ const c1 = requireC1Evidence(store, task, normalizedActor, writerToken);
167
+
168
+ const knowledgeMapApprovalFingerprint = hashEvidence(knowledgeMap.approval);
169
+ const taskAuthorizationFingerprint = hashEvidence(authorization);
170
+ const delegationGrantFingerprint = hashEvidence(grant);
171
+ const c1EvidenceFingerprint = hashEvidence(c1);
172
+ const evidenceFingerprint = hashEvidence({
173
+ task,
174
+ decisionPlanHash: decision.planHash,
175
+ reboundPlanHash: task.planHash,
176
+ semanticPlanHash,
177
+ remediationEvents,
178
+ correctiveDecisions,
179
+ planRiskAudits,
180
+ knowledgeMap,
181
+ grant,
182
+ autonomyContract,
183
+ handoffs: readTaskHandoffEvents(store, task),
184
+ c1,
185
+ });
186
+ return {
187
+ task,
188
+ decision,
189
+ directRebind,
190
+ semanticPlanHash,
191
+ sourceAudit,
192
+ reboundAudit,
193
+ knowledgeMapApprovalFingerprint,
194
+ taskAuthorizationFingerprint,
195
+ delegationGrantFingerprint,
196
+ c1EvidenceFingerprint,
197
+ evidenceFingerprint,
198
+ };
199
+ }
200
+
201
+ export function appendCorrectiveDecisionRecovery(
202
+ store: FileStateStore,
203
+ task: TaskState,
204
+ stepId: string,
205
+ expectedRevision: number,
206
+ actor: string,
207
+ writerToken: string | null,
208
+ now: Date,
209
+ ): CorrectiveDecisionRecoveryEvent {
210
+ if (task.revision !== expectedRevision) {
211
+ throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
212
+ expectedRevision,
213
+ actualRevision: task.revision,
214
+ });
215
+ }
216
+ const preflight = assessCorrectiveDecisionRecovery(store, task, stepId, actor, writerToken);
217
+ const preflightRecoveryChain = snapshotRecoveryChain(store, task, preflight, actor.trim());
218
+ const taskRoot = store.taskRoot(task.projectId, task.id);
219
+ const lockFile = path.join(taskRoot, RECOVERY_LOCK);
220
+ let descriptor: number;
221
+ try {
222
+ descriptor = openSync(lockFile, 'wx', 0o600);
223
+ closeSync(descriptor);
224
+ } catch (error) {
225
+ if (existsSync(lockFile)) {
226
+ throw new WorkflowError('LOCKED', `Corrective recovery for ${task.id} is already being serialized.`, {
227
+ taskId: task.id,
228
+ stepId,
229
+ });
230
+ }
231
+ throw error;
232
+ }
233
+ try {
234
+ const currentTask = store.readTask(task.projectId, task.id);
235
+ if (currentTask.revision !== expectedRevision) {
236
+ throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
237
+ expectedRevision,
238
+ actualRevision: currentTask.revision,
239
+ });
240
+ }
241
+ let underLock: CorrectiveRecoveryAssessment;
242
+ try {
243
+ underLock = assessCorrectiveDecisionRecovery(store, currentTask, stepId, actor, writerToken);
244
+ } catch (error) {
245
+ throw new WorkflowError('STATE_CONFLICT', 'Corrective recovery evidence changed after preflight.', {
246
+ taskId: task.id,
247
+ stepId,
248
+ cause: error instanceof Error ? error.message : String(error),
249
+ });
250
+ }
251
+ if (underLock.evidenceFingerprint !== preflight.evidenceFingerprint) {
252
+ throw new WorkflowError('STATE_CONFLICT', 'Corrective recovery evidence changed after preflight.', {
253
+ taskId: task.id,
254
+ stepId,
255
+ preflightEvidenceFingerprint: preflight.evidenceFingerprint,
256
+ currentEvidenceFingerprint: underLock.evidenceFingerprint,
257
+ });
258
+ }
259
+ const underLockRecoveryChain = snapshotRecoveryChain(store, currentTask, underLock, actor.trim());
260
+ if (underLockRecoveryChain.matchingEvent) {
261
+ return underLockRecoveryChain.matchingEvent;
262
+ }
263
+ if (underLockRecoveryChain.fingerprint !== preflightRecoveryChain.fingerprint) {
264
+ throw new WorkflowError('STATE_CONFLICT', 'Corrective recovery chain changed after preflight.', {
265
+ taskId: task.id,
266
+ stepId,
267
+ preflightRecoveryChainFingerprint: preflightRecoveryChain.fingerprint,
268
+ currentRecoveryChainFingerprint: underLockRecoveryChain.fingerprint,
269
+ });
270
+ }
271
+ return store.appendSidecarEvent<CorrectiveDecisionRecoveryEvent>(
272
+ taskRoot,
273
+ CORRECTIVE_DECISION_RECOVERIES_SIDECAR,
274
+ recoveryPayload(underLock, actor.trim(), now),
275
+ { validateEvent: (event, context) => validateRecoveryEvent(event, currentTask, context.file, context.line) },
276
+ );
277
+ } finally {
278
+ if (existsSync(lockFile)) unlinkSync(lockFile);
279
+ }
280
+ }
281
+
282
+ function snapshotRecoveryChain(
283
+ store: FileStateStore,
284
+ task: TaskState,
285
+ assessment: CorrectiveRecoveryAssessment,
286
+ actor: string,
287
+ ): RecoveryChainSnapshot {
288
+ const events = readCorrectiveDecisionRecoveryEvents(store, task);
289
+ const sameBinding = events.filter((event) =>
290
+ event.correctiveDecisionEventId === assessment.decision.eventId
291
+ && event.stepId === assessment.decision.stepId
292
+ && event.triggeringAttemptCount === assessment.decision.triggeringAttemptCount,
293
+ );
294
+ if (sameBinding.length > 1) {
295
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple recoveries exist for one corrective decision binding.', {
296
+ taskId: task.id,
297
+ stepId: assessment.decision.stepId,
298
+ eventIds: sameBinding.map((event) => event.eventId),
299
+ });
300
+ }
301
+ const matchingEvent = sameBinding[0] ?? null;
302
+ if (matchingEvent) assertRecoveryMatchesAssessment(matchingEvent, assessment, actor);
303
+ return { fingerprint: hashEvidence(events), matchingEvent };
304
+ }
305
+
306
+ export function requireCorrectiveDecisionRecoveryForRun(
307
+ store: FileStateStore,
308
+ task: TaskState,
309
+ stepId: string,
310
+ actor: string,
311
+ writerToken: string | null,
312
+ ): CorrectiveDecisionRecoveryEvent {
313
+ const assessment = assessCorrectiveDecisionRecovery(store, task, stepId, actor, writerToken);
314
+ const events = readCorrectiveDecisionRecoveryEvents(store, task);
315
+ const matches = events.filter((event) =>
316
+ event.correctiveDecisionEventId === assessment.decision.eventId
317
+ && event.stepId === stepId
318
+ && event.triggeringAttemptCount === assessment.decision.triggeringAttemptCount,
319
+ );
320
+ if (matches.length !== 1) {
321
+ throw new WorkflowError('TRANSITION_BLOCKED', 'A validated corrective recovery is required before this run.', {
322
+ taskId: task.id,
323
+ stepId,
324
+ correctiveDecisionEventId: assessment.decision.eventId,
325
+ recoveryCount: matches.length,
326
+ });
327
+ }
328
+ const recovery = matches[0]!;
329
+ assertRecoveryMatchesAssessment(recovery, assessment, actor.trim());
330
+ return recovery;
331
+ }
332
+
333
+ export function assessCorrectiveDecisionRecoveryForNavigation(
334
+ store: FileStateStore,
335
+ task: TaskState,
336
+ stepId: string,
337
+ ): CorrectiveRecoveryAssessment {
338
+ const posture = readTaskC1Posture(store, task);
339
+ const lease = readWriterLease(store, task);
340
+ const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
341
+ return assessCorrectiveDecisionRecovery(store, task, stepId, actor, lease?.token ?? null);
342
+ }
343
+
344
+ export function readValidatedStopOverrideContextRebindIfApplicable(
345
+ store: FileStateStore,
346
+ task: TaskState,
347
+ stepId: string,
348
+ repositoryRoot: string,
349
+ ): StopOverrideContextRebindAssessment | null {
350
+ if (!task.planHash) return null;
351
+ const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
352
+ const triggeringAttemptCount = remediationEvents.length + 1;
353
+ const staleStops = readCorrectiveDecisionEvents(store, task).filter((event) =>
354
+ event.stepId === stepId
355
+ && event.triggeringAttemptCount === triggeringAttemptCount
356
+ && event.decision === 'stop-escalate'
357
+ && event.planHash !== task.planHash,
358
+ );
359
+ if (staleStops.length === 0) return null;
360
+ if (staleStops.length !== 1) {
361
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple stale stop decisions match one guarded retry binding.', {
362
+ taskId: task.id,
363
+ stepId,
364
+ triggeringAttemptCount,
365
+ decisionEventIds: staleStops.map((event) => event.eventId),
366
+ });
367
+ }
368
+ return assessStopOverrideContextRebind(store, task, stepId, staleStops[0]!, remediationEvents, repositoryRoot);
369
+ }
370
+
371
+ function assessStopOverrideContextRebind(
372
+ store: FileStateStore,
373
+ task: TaskState,
374
+ stepId: string,
375
+ stopDecision: CorrectiveDecisionEvent,
376
+ remediationEvents: ReturnType<typeof readRemediationEvents>,
377
+ repositoryRoot: string,
378
+ ): StopOverrideContextRebindAssessment {
379
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
380
+ if (!step) throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
381
+ if (!['planned', 'failed', 'in_progress'].includes(step.status)) {
382
+ throw new WorkflowError('TRANSITION_BLOCKED', `Attempt-four continuation is unavailable while ${stepId} is ${step.status}.`);
383
+ }
384
+ if (
385
+ remediationEvents.length !== 3
386
+ || remediationEvents[0]?.attemptOrdinal !== 1
387
+ || remediationEvents[0]?.mode !== 'ordinary'
388
+ || remediationEvents[1]?.attemptOrdinal !== 2
389
+ || remediationEvents[1]?.mode !== 'ordinary'
390
+ || remediationEvents[2]?.attemptOrdinal !== 3
391
+ || remediationEvents[2]?.mode !== 'corrective'
392
+ ) {
393
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound stop override requires the exact three-attempt beta.11 history.', {
394
+ taskId: task.id,
395
+ stepId,
396
+ attempts: remediationEvents.map((event) => ({ ordinal: event.attemptOrdinal, mode: event.mode })),
397
+ });
398
+ }
399
+ const overrides = readStopEscalateOverrideEvents(store, task).filter((event) =>
400
+ event.stepId === stepId && event.stopDecisionEventId === stopDecision.eventId,
401
+ );
402
+ if (overrides.length !== 1) {
403
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound attempt four requires one exact applied stop override.', {
404
+ taskId: task.id,
405
+ stepId,
406
+ stopDecisionEventId: stopDecision.eventId,
407
+ overrideEventIds: overrides.map((event) => event.eventId),
408
+ });
409
+ }
410
+ const override = overrides[0]!;
411
+ if (
412
+ stopDecision.triggeringAttemptCount !== 4
413
+ || stopDecision.planHash !== override.planHash
414
+ || task.planHash === override.planHash
415
+ || override.coveredEventIds.length !== remediationEvents.length
416
+ || override.coveredEventIds.some((eventId, index) => eventId !== remediationEvents[index]!.eventId)
417
+ ) {
418
+ throw new WorkflowError('STATE_CORRUPT', 'Context-rebound stop override no longer binds the exact source Plan/history.', {
419
+ taskId: task.id,
420
+ stepId,
421
+ overrideEventId: override.eventId,
422
+ });
423
+ }
424
+ assertCorrectiveAuditorIndependence(store, task, stepId, stopDecision.auditor);
425
+ const rebindEvidence = assessAppendOnlyContextPlanRebindEvidence(
426
+ store,
427
+ task,
428
+ stepId,
429
+ stopDecision,
430
+ );
431
+ const verifiedHeadRecoveries = requireStopOverrideHeadContinuity(
432
+ repositoryRoot,
433
+ task,
434
+ override,
435
+ rebindEvidence.authorization,
436
+ );
437
+ requireEvidenceBoundAuditTaskRevision(
438
+ task,
439
+ rebindEvidence.reboundAudit,
440
+ verifiedHeadRecoveries,
441
+ step.status === 'in_progress',
442
+ );
443
+ const stopAt = Date.parse(stopDecision.recordedAt);
444
+ const overrideAt = Date.parse(override.recordedAt);
445
+ const firstRebindAt = Date.parse(rebindEvidence.rebindChain[0]!.recordedAt);
446
+ const lastRebindAt = Date.parse(rebindEvidence.rebindChain.at(-1)!.recordedAt);
447
+ if (
448
+ ![stopAt, overrideAt, firstRebindAt, lastRebindAt].every(Number.isFinite)
449
+ || overrideAt < stopAt
450
+ || firstRebindAt < overrideAt
451
+ || lastRebindAt < firstRebindAt
452
+ ) {
453
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override and context-rebind chronology is invalid.', {
454
+ taskId: task.id,
455
+ stepId,
456
+ stopDecisionRecordedAt: stopDecision.recordedAt,
457
+ overrideRecordedAt: override.recordedAt,
458
+ firstRebindRecordedAt: rebindEvidence.rebindChain[0]!.recordedAt,
459
+ lastRebindRecordedAt: rebindEvidence.rebindChain.at(-1)!.recordedAt,
460
+ });
461
+ }
462
+ return {
463
+ task,
464
+ override,
465
+ stopDecision,
466
+ semanticPlanHash: rebindEvidence.semanticPlanHash,
467
+ };
468
+ }
469
+
470
+ function recoveryPayload(
471
+ assessment: CorrectiveRecoveryAssessment,
472
+ actor: string,
473
+ now: Date,
474
+ ): Omit<CorrectiveDecisionRecoveryEvent, 'previousEventHash' | 'eventHash'> {
475
+ const { task, decision, directRebind, sourceAudit, reboundAudit } = assessment;
476
+ return {
477
+ eventId: `CDR-${createUlid(now.getTime())}`,
478
+ recordedAt: now.toISOString(),
479
+ taskId: task.id,
480
+ stepId: decision.stepId,
481
+ triggeringAttemptCount: decision.triggeringAttemptCount,
482
+ correctiveDecisionEventId: decision.eventId,
483
+ correctiveDecisionEventHash: decision.eventHash,
484
+ decisionPlanHash: decision.planHash,
485
+ reboundPlanHash: task.planHash!,
486
+ semanticPlanHash: assessment.semanticPlanHash,
487
+ fromKnowledgeMapRevision: directRebind.fromKnowledgeMapRevision,
488
+ fromKnowledgeMapHash: directRebind.fromKnowledgeMapHash,
489
+ toKnowledgeMapRevision: directRebind.toKnowledgeMapRevision,
490
+ toKnowledgeMapHash: directRebind.toKnowledgeMapHash,
491
+ rebindRecordedAt: directRebind.recordedAt,
492
+ sourcePlanRiskAuditEventId: sourceAudit.eventId,
493
+ sourcePlanRiskAuditEventHash: sourceAudit.eventHash,
494
+ reboundPlanRiskAuditEventId: reboundAudit.eventId,
495
+ reboundPlanRiskAuditEventHash: reboundAudit.eventHash,
496
+ knowledgeMapApprovalFingerprint: assessment.knowledgeMapApprovalFingerprint,
497
+ taskAuthorizationFingerprint: assessment.taskAuthorizationFingerprint,
498
+ delegationGrantFingerprint: assessment.delegationGrantFingerprint,
499
+ c1EvidenceFingerprint: assessment.c1EvidenceFingerprint,
500
+ evidenceFingerprint: assessment.evidenceFingerprint,
501
+ actor,
502
+ };
503
+ }
504
+
505
+ function hashEvidence(value: unknown): string {
506
+ return sha256Hex(canonicalJsonStringify(value));
507
+ }
508
+
509
+ function assessContextOnlyPlanRebindEvidence(
510
+ store: FileStateStore,
511
+ task: TaskState,
512
+ stepId: string,
513
+ sourceDecision: CorrectiveDecisionEvent,
514
+ options: { allowTaskRevisionAdvance?: boolean } = {},
515
+ ): ContextOnlyPlanRebindEvidence {
516
+ if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
517
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
518
+ }
519
+ if (sourceDecision.planHash === task.planHash) {
520
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Context-only Plan rebind validation requires distinct source and current Plan hashes.');
521
+ }
522
+ const taskRoot = store.taskRoot(task.projectId, task.id);
523
+ const planEvidence = comparePlanArtifacts(taskRoot, sourceDecision.planHash, task.planHash);
524
+ const currentAudit = readCurrentPlanRiskAudit(store, task);
525
+ if (!currentAudit || !currentAudit.reviewRequiredStepIds.includes(stepId)) {
526
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
527
+ }
528
+ const planRiskAudits = readPlanRiskAuditEvents(store, task);
529
+ const { sourceAudit, reboundAudit } = requireMechanicalAuditRebound(
530
+ task,
531
+ sourceDecision,
532
+ planRiskAudits,
533
+ options.allowTaskRevisionAdvance ?? false,
534
+ );
535
+ const directRebind = requireDirectRebind(task, sourceDecision, sourceAudit);
536
+ assertRebindMatchesPlans(directRebind, planEvidence);
537
+ if (
538
+ sourceAudit.knowledgeMapRevision !== directRebind.fromKnowledgeMapRevision
539
+ || sourceAudit.knowledgeMapHash !== directRebind.fromKnowledgeMapHash
540
+ || reboundAudit.knowledgeMapRevision !== directRebind.toKnowledgeMapRevision
541
+ || reboundAudit.knowledgeMapHash !== directRebind.toKnowledgeMapHash
542
+ ) {
543
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit pair does not bind the direct Knowledge rebind endpoints.');
544
+ }
545
+
546
+ const knowledgeMap = store.readKnowledgeMap(task.projectId);
547
+ const authorization = requireRefreshAuthorization(task);
548
+ const { grant, autonomyContract } = requireRefreshAuthority(
549
+ store,
550
+ task,
551
+ knowledgeMap,
552
+ authorization,
553
+ directRebind,
554
+ );
555
+ assertRefreshChronology(knowledgeMap, authorization, directRebind, reboundAudit);
556
+ return {
557
+ directRebind,
558
+ semanticPlanHash: planEvidence.semanticPlanHash,
559
+ sourceAudit,
560
+ reboundAudit,
561
+ planRiskAudits,
562
+ knowledgeMap,
563
+ authorization,
564
+ grant,
565
+ autonomyContract,
566
+ };
567
+ }
568
+
569
+ function assessAppendOnlyContextPlanRebindEvidence(
570
+ store: FileStateStore,
571
+ task: TaskState,
572
+ stepId: string,
573
+ sourceDecision: CorrectiveDecisionEvent,
574
+ ): AppendOnlyContextPlanRebindEvidence {
575
+ if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
576
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
577
+ }
578
+ if (sourceDecision.planHash === task.planHash) {
579
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Append-only Plan rebind validation requires distinct source and current Plan hashes.');
580
+ }
581
+ const planRiskAudits = readPlanRiskAuditEvents(store, task);
582
+ const rebindChain = requireAppendOnlyRebindChain(task, sourceDecision, planRiskAudits);
583
+ const planEvidence = comparePlanRebindChainArtifacts(
584
+ store.taskRoot(task.projectId, task.id),
585
+ sourceDecision.planHash,
586
+ task.planHash,
587
+ rebindChain,
588
+ );
589
+ const { sourceAudit, reboundAudit, auditChain } = requireMechanicalAuditReboundChain(
590
+ task,
591
+ sourceDecision,
592
+ planRiskAudits,
593
+ rebindChain,
594
+ );
595
+ if (!reboundAudit.stepClassifications.some((classification) =>
596
+ classification.stepId === stepId && classification.reviewRequired
597
+ )) {
598
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
599
+ }
600
+ for (let index = 0; index < rebindChain.length; index += 1) {
601
+ const rebind = rebindChain[index]!;
602
+ const fromBinding = planEvidence.bindings.get(rebind.previousPlanHash);
603
+ const toBinding = planEvidence.bindings.get(rebind.reboundPlanHash);
604
+ if (!fromBinding || !toBinding) {
605
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind references an unverified Plan artifact.', {
606
+ previousPlanHash: rebind.previousPlanHash,
607
+ reboundPlanHash: rebind.reboundPlanHash,
608
+ });
609
+ }
610
+ assertRebindMatchesPlans(rebind, { decisionBinding: fromBinding, reboundBinding: toBinding });
611
+ const fromAudit = auditChain[index]!;
612
+ const toAudit = auditChain[index + 1]!;
613
+ if (
614
+ fromAudit.knowledgeMapRevision !== rebind.fromKnowledgeMapRevision
615
+ || fromAudit.knowledgeMapHash !== rebind.fromKnowledgeMapHash
616
+ || toAudit.knowledgeMapRevision !== rebind.toKnowledgeMapRevision
617
+ || toAudit.knowledgeMapHash !== rebind.toKnowledgeMapHash
618
+ ) {
619
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit chain does not bind every Knowledge rebind endpoint.', {
620
+ rebindIndex: index,
621
+ });
622
+ }
623
+ }
624
+
625
+ const knowledgeMap = store.readKnowledgeMap(task.projectId);
626
+ const authorization = requireRefreshAuthorization(task);
627
+ const finalRebind = rebindChain.at(-1)!;
628
+ const { grant, autonomyContract } = requireRefreshAuthority(
629
+ store,
630
+ task,
631
+ knowledgeMap,
632
+ authorization,
633
+ finalRebind,
634
+ );
635
+ assertRefreshChronology(knowledgeMap, authorization, finalRebind, reboundAudit);
636
+ return {
637
+ rebindChain,
638
+ semanticPlanHash: planEvidence.semanticPlanHash,
639
+ sourceAudit,
640
+ reboundAudit,
641
+ planRiskAudits,
642
+ knowledgeMap,
643
+ authorization,
644
+ grant,
645
+ autonomyContract,
646
+ };
647
+ }
648
+
649
+ function comparePlanRebindChainArtifacts(
650
+ taskRoot: string,
651
+ sourcePlanHash: string,
652
+ currentPlanHash: string,
653
+ rebindChain: TaskKnowledgeRebindRecord[],
654
+ ): {
655
+ semanticPlanHash: string;
656
+ bindings: Map<string, { revision: number; hash: string }>;
657
+ } {
658
+ const planHashes = [sourcePlanHash, ...rebindChain.map((rebind) => rebind.reboundPlanHash)];
659
+ if (planHashes.at(-1) !== currentPlanHash || new Set(planHashes).size !== planHashes.length) {
660
+ throw new WorkflowError('STATE_CORRUPT', 'Knowledge rebind chain does not form one acyclic source-to-current Plan sequence.');
661
+ }
662
+ const contents = planHashes.map((planHash) => readVerifiedPlanArtifact(taskRoot, planHash, currentPlanHash));
663
+ const bindings = new Map<string, { revision: number; hash: string }>();
664
+ let semanticPlanHash: string | null = null;
665
+ for (let index = 0; index < contents.length; index += 1) {
666
+ const planHash = planHashes[index]!;
667
+ bindings.set(planHash, normalizePlanKnowledgeBinding(contents[index]!, `Plan ${planHash}`).binding);
668
+ if (index === 0) continue;
669
+ const adjacentSemanticHash = assertKnowledgeBindingOnlyPlanChange(contents[index - 1]!, contents[index]!);
670
+ if (semanticPlanHash !== null && semanticPlanHash !== adjacentSemanticHash) {
671
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain does not preserve one semantic Plan identity.');
672
+ }
673
+ semanticPlanHash = adjacentSemanticHash;
674
+ }
675
+ if (!semanticPlanHash) {
676
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain has no comparable Plan hop.');
677
+ }
678
+ return { semanticPlanHash, bindings };
679
+ }
680
+
681
+ function readVerifiedPlanArtifact(taskRoot: string, planHash: string, currentPlanHash: string): string {
682
+ assertHash64(planHash, 'Plan hash');
683
+ const file = planHash === currentPlanHash
684
+ ? path.join(taskRoot, 'plan.md')
685
+ : path.join(taskRoot, '.versions', `plan.md.${planHash}`);
686
+ if (!existsSync(file)) {
687
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact required by the append-only rebind chain is missing.', {
688
+ planHash,
689
+ file,
690
+ });
691
+ }
692
+ const content = readFileSync(file, 'utf8');
693
+ if (sha256Hex(content) !== planHash) {
694
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its append-only rebind hash.', {
695
+ planHash,
696
+ file,
697
+ });
698
+ }
699
+ return content;
700
+ }
701
+
702
+ function comparePlanArtifacts(taskRoot: string, decisionPlanHash: string, reboundPlanHash: string): {
703
+ semanticPlanHash: string;
704
+ decisionBinding: { revision: number; hash: string };
705
+ reboundBinding: { revision: number; hash: string };
706
+ } {
707
+ assertHash64(decisionPlanHash, 'Corrective decision Plan hash');
708
+ assertHash64(reboundPlanHash, 'Current Plan hash');
709
+ const historicalFile = path.join(taskRoot, '.versions', `plan.md.${decisionPlanHash}`);
710
+ if (!existsSync(historicalFile)) {
711
+ throw new WorkflowError('STATE_CORRUPT', 'Historical corrective-decision Plan artifact is missing.', {
712
+ decisionPlanHash,
713
+ historicalFile,
714
+ });
715
+ }
716
+ const historical = readFileSync(historicalFile, 'utf8');
717
+ const current = readFileSync(path.join(taskRoot, 'plan.md'), 'utf8');
718
+ if (sha256Hex(historical) !== decisionPlanHash || sha256Hex(current) !== reboundPlanHash) {
719
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its recorded hash.');
720
+ }
721
+ const semanticPlanHash = assertKnowledgeBindingOnlyPlanChange(historical, current);
722
+ const decision = normalizePlanKnowledgeBinding(historical, 'historical');
723
+ const rebound = normalizePlanKnowledgeBinding(current, 'current');
724
+ return {
725
+ semanticPlanHash,
726
+ decisionBinding: decision.binding,
727
+ reboundBinding: rebound.binding,
728
+ };
729
+ }
730
+
731
+ function normalizePlanKnowledgeBinding(content: string, label: string): {
732
+ normalized: string;
733
+ binding: { revision: number; hash: string };
734
+ } {
735
+ const matches = [...content.matchAll(KNOWLEDGE_LINE)];
736
+ if (matches.length !== 1) {
737
+ throw new WorkflowError('STATE_CORRUPT', `${label} Plan must contain exactly one canonical Knowledge Map binding line.`, {
738
+ matchCount: matches.length,
739
+ });
740
+ }
741
+ const match = matches[0]!;
742
+ return {
743
+ normalized: content.slice(0, match.index) + KNOWLEDGE_SENTINEL + content.slice(match.index! + match[0].length),
744
+ binding: { revision: Number(match[1]), hash: match[2]! },
745
+ };
746
+ }
747
+
748
+ function requireMechanicalAuditRebound(
749
+ task: TaskState,
750
+ decision: CorrectiveDecisionEvent,
751
+ events: PlanRiskAuditEvent[],
752
+ allowTaskRevisionAdvance = false,
753
+ ): { sourceAudit: PlanRiskAuditEvent; reboundAudit: PlanRiskAuditEvent } {
754
+ const sources = events.filter((event) => event.planHash === decision.planHash);
755
+ const rebounds = events.filter((event) => event.planHash === task.planHash);
756
+ if (sources.length !== 1 || rebounds.length !== 1) {
757
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery requires one source and one rebound Plan Risk Audit.', {
758
+ sourceAuditIds: sources.map((event) => event.eventId),
759
+ reboundAuditIds: rebounds.map((event) => event.eventId),
760
+ });
761
+ }
762
+ const sourceAudit = sources[0]!;
763
+ const reboundAudit = rebounds[0]!;
764
+ const sourceIndex = events.indexOf(sourceAudit);
765
+ const reboundIndex = events.indexOf(reboundAudit);
766
+ if (reboundIndex !== sourceIndex + 1 || reboundAudit.previousEventHash !== sourceAudit.eventHash || reboundIndex !== events.length - 1) {
767
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebound is not the immediate append-only successor of the source audit.');
768
+ }
769
+ const expectedSummary = `${sourceAudit.summary} Rebound automatically after content-only Task context refresh.`;
770
+ if (
771
+ reboundAudit.taskId !== sourceAudit.taskId
772
+ || reboundAudit.briefHash !== sourceAudit.briefHash
773
+ || reboundAudit.planner !== sourceAudit.planner
774
+ || reboundAudit.auditor !== sourceAudit.auditor
775
+ || reboundAudit.decision !== sourceAudit.decision
776
+ || reboundAudit.splitRationale !== sourceAudit.splitRationale
777
+ || reboundAudit.summary !== expectedSummary
778
+ || canonicalJsonStringify(reboundAudit.stepClassifications) !== canonicalJsonStringify(sourceAudit.stepClassifications)
779
+ ) {
780
+ throw new WorkflowError('STATE_CORRUPT', 'Current Plan Risk Audit is not a mechanical content-refresh rebound.');
781
+ }
782
+ if (sourceAudit.briefHash !== task.briefHash || reboundAudit.briefHash !== task.briefHash) {
783
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebound does not preserve the current Brief binding.');
784
+ }
785
+ const authorizedTaskRevision = reboundAudit.taskRevision + 1;
786
+ if (
787
+ (!allowTaskRevisionAdvance && authorizedTaskRevision !== task.revision)
788
+ || (allowTaskRevisionAdvance && authorizedTaskRevision > task.revision)
789
+ ) {
790
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit does not bind the required Task authorization boundary.', {
791
+ reboundAuditTaskRevision: reboundAudit.taskRevision,
792
+ currentTaskRevision: task.revision,
793
+ allowTaskRevisionAdvance,
794
+ });
795
+ }
796
+ return { sourceAudit, reboundAudit };
797
+ }
798
+
799
+ function requireDirectRebind(
800
+ task: TaskState,
801
+ decision: CorrectiveDecisionEvent,
802
+ sourceAudit: PlanRiskAuditEvent,
803
+ ): TaskKnowledgeRebindRecord {
804
+ for (let index = 1; index < task.knowledgeRebinds.length; index += 1) {
805
+ if (Date.parse(task.knowledgeRebinds[index]!.recordedAt) <= Date.parse(task.knowledgeRebinds[index - 1]!.recordedAt)) {
806
+ throw new WorkflowError('STATE_CORRUPT', 'Task Knowledge rebind timestamps must be strictly increasing.');
807
+ }
808
+ }
809
+ const boundary = Math.max(Date.parse(decision.recordedAt), Date.parse(sourceAudit.recordedAt));
810
+ const afterBoundary = task.knowledgeRebinds.filter((record) => Date.parse(record.recordedAt) > boundary);
811
+ if (afterBoundary.length !== 1) {
812
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Legacy recovery requires exactly one Knowledge rebind after the decision/source-audit binding.', {
813
+ rebindCount: afterBoundary.length,
814
+ });
815
+ }
816
+ const selected = afterBoundary[0]!;
817
+ if (task.knowledgeRebinds.at(-1) !== selected || selected.previousPlanHash !== decision.planHash || selected.reboundPlanHash !== task.planHash) {
818
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance is not one direct decision-Plan to current-Plan hop.');
819
+ }
820
+ return selected;
821
+ }
822
+
823
+ function requireAppendOnlyRebindChain(
824
+ task: TaskState,
825
+ decision: CorrectiveDecisionEvent,
826
+ planRiskAudits: PlanRiskAuditEvent[],
827
+ ): TaskKnowledgeRebindRecord[] {
828
+ for (let index = 0; index < task.knowledgeRebinds.length; index += 1) {
829
+ const recordedAt = Date.parse(task.knowledgeRebinds[index]!.recordedAt);
830
+ if (
831
+ !Number.isFinite(recordedAt)
832
+ || (index > 0 && recordedAt <= Date.parse(task.knowledgeRebinds[index - 1]!.recordedAt))
833
+ ) {
834
+ throw new WorkflowError('STATE_CORRUPT', 'Task Knowledge rebind timestamps must be finite and strictly increasing.');
835
+ }
836
+ }
837
+ const sourceAudits = planRiskAudits.filter((event) => event.planHash === decision.planHash);
838
+ if (sourceAudits.length !== 1) {
839
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one exact source Plan Risk Audit.', {
840
+ sourceAuditIds: sourceAudits.map((event) => event.eventId),
841
+ });
842
+ }
843
+ const boundary = Math.max(Date.parse(decision.recordedAt), Date.parse(sourceAudits[0]!.recordedAt));
844
+ if (!Number.isFinite(boundary)) {
845
+ throw new WorkflowError('STATE_CORRUPT', 'Stop decision or source Plan Risk Audit timestamp is invalid.');
846
+ }
847
+ const chain = task.knowledgeRebinds.filter((record) => Date.parse(record.recordedAt) > boundary);
848
+ if (chain.length < 1) {
849
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop-override continuation requires at least one Knowledge rebind after its source authority.');
850
+ }
851
+ if (task.knowledgeRebinds.at(-1) !== chain.at(-1)) {
852
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind chain does not terminate at the current Task boundary.');
853
+ }
854
+ let expectedPlanHash = decision.planHash;
855
+ for (const record of chain) {
856
+ if (record.previousPlanHash !== expectedPlanHash || record.reboundPlanHash === record.previousPlanHash) {
857
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance is not one contiguous source-to-current Plan chain.', {
858
+ expectedPreviousPlanHash: expectedPlanHash,
859
+ actualPreviousPlanHash: record.previousPlanHash,
860
+ reboundPlanHash: record.reboundPlanHash,
861
+ });
862
+ }
863
+ expectedPlanHash = record.reboundPlanHash;
864
+ }
865
+ if (expectedPlanHash !== task.planHash) {
866
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance does not terminate at the current Task Plan.', {
867
+ expectedCurrentPlanHash: task.planHash,
868
+ actualTerminalPlanHash: expectedPlanHash,
869
+ });
870
+ }
871
+ return chain;
872
+ }
873
+
874
+ function requireMechanicalAuditReboundChain(
875
+ task: TaskState,
876
+ decision: CorrectiveDecisionEvent,
877
+ events: PlanRiskAuditEvent[],
878
+ rebindChain: TaskKnowledgeRebindRecord[],
879
+ ): { sourceAudit: PlanRiskAuditEvent; reboundAudit: PlanRiskAuditEvent; auditChain: PlanRiskAuditEvent[] } {
880
+ const planHashes = [decision.planHash, ...rebindChain.map((record) => record.reboundPlanHash)];
881
+ const auditChain = planHashes.map((planHash) => {
882
+ const matches = events.filter((event) => event.planHash === planHash);
883
+ if (matches.length !== 1) {
884
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one Plan Risk Audit for every Plan in the rebind chain.', {
885
+ planHash,
886
+ auditEventIds: matches.map((event) => event.eventId),
887
+ });
888
+ }
889
+ return matches[0]!;
890
+ });
891
+ const sourceIndex = events.indexOf(auditChain[0]!);
892
+ for (let index = 0; index < auditChain.length; index += 1) {
893
+ if (events[sourceIndex + index] !== auditChain[index]) {
894
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind evidence is not one contiguous append-only chain.');
895
+ }
896
+ if (index > 0) assertMechanicalAuditSuccessor(task, auditChain[index - 1]!, auditChain[index]!);
897
+ }
898
+ if (sourceIndex < 0 || sourceIndex + auditChain.length !== events.length) {
899
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind chain does not end at the current append-only boundary.');
900
+ }
901
+ const reboundAudit = auditChain.at(-1)!;
902
+ return { sourceAudit: auditChain[0]!, reboundAudit, auditChain };
903
+ }
904
+
905
+ function assertMechanicalAuditSuccessor(
906
+ task: TaskState,
907
+ sourceAudit: PlanRiskAuditEvent,
908
+ reboundAudit: PlanRiskAuditEvent,
909
+ ): void {
910
+ const expectedSummary = `${sourceAudit.summary} Rebound automatically after content-only Task context refresh.`;
911
+ if (
912
+ reboundAudit.previousEventHash !== sourceAudit.eventHash
913
+ || reboundAudit.taskId !== sourceAudit.taskId
914
+ || reboundAudit.briefHash !== sourceAudit.briefHash
915
+ || reboundAudit.briefHash !== task.briefHash
916
+ || reboundAudit.planner !== sourceAudit.planner
917
+ || reboundAudit.auditor !== sourceAudit.auditor
918
+ || reboundAudit.decision !== sourceAudit.decision
919
+ || reboundAudit.splitRationale !== sourceAudit.splitRationale
920
+ || reboundAudit.summary !== expectedSummary
921
+ || canonicalJsonStringify(reboundAudit.stepClassifications) !== canonicalJsonStringify(sourceAudit.stepClassifications)
922
+ ) {
923
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit successor is not a mechanical content-refresh rebound.');
924
+ }
925
+ }
926
+
927
+ function requireStopOverrideHeadContinuity(
928
+ repositoryRoot: string,
929
+ task: TaskState,
930
+ override: StopEscalateOverrideEvent,
931
+ authorization: AuthorizationEvent,
932
+ ): DependencyProvenanceRecoveryRecord[] {
933
+ const authorizedHead = authorization.headCommit;
934
+ if (!authorizedHead) {
935
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution authorization does not bind a Git HEAD.');
936
+ }
937
+ if (authorizedHead === override.headCommit) return [];
938
+ if (!gitIsAncestor(repositoryRoot, override.headCommit, authorizedHead)) {
939
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution HEAD is not an append-only descendant of the stop-override HEAD.', {
940
+ overrideHeadCommit: override.headCommit,
941
+ authorizedHeadCommit: authorizedHead,
942
+ });
943
+ }
944
+ const output = runGit(repositoryRoot, ['rev-list', '--reverse', `${override.headCommit}..${authorizedHead}`]);
945
+ const commits = output ? output.split('\n') : [];
946
+ if (commits.length < 1) {
947
+ throw new WorkflowError('STATE_CORRUPT', 'Git reports distinct stop-override and authorization HEADs without an intervening commit.');
948
+ }
949
+ let expectedParent = override.headCommit;
950
+ let previousRecoveryIndex = -1;
951
+ let previousRecoveryAt = Number.NEGATIVE_INFINITY;
952
+ const verifiedRecoveries: DependencyProvenanceRecoveryRecord[] = [];
953
+ for (const commit of commits) {
954
+ const matches = (task.dependencyProvenanceRecoveries ?? []).filter((record) => record.commitSha === commit);
955
+ if (matches.length !== 1) {
956
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Git HEAD advanced through a commit without one exact dependency-provenance recovery.', {
957
+ commit,
958
+ recoveryCount: matches.length,
959
+ });
960
+ }
961
+ const recovery = matches[0]!;
962
+ const recoveryIndex = (task.dependencyProvenanceRecoveries ?? []).indexOf(recovery);
963
+ const recoveryAt = Date.parse(recovery.recordedAt);
964
+ const parents = runGit(repositoryRoot, ['show', '-s', '--format=%P', commit]).split(/\s+/).filter(Boolean);
965
+ const filesOutput = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commit]);
966
+ const files = filesOutput ? filesOutput.split('\n').sort() : [];
967
+ if (
968
+ parents.length !== 1
969
+ || parents[0] !== expectedParent
970
+ || recovery.parentCommitSha !== expectedParent
971
+ || recoveryIndex <= previousRecoveryIndex
972
+ || !Number.isFinite(recoveryAt)
973
+ || recoveryAt <= previousRecoveryAt
974
+ || recovery.packageName !== PACKAGE_NAME
975
+ || canonicalJsonStringify(recovery.files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
976
+ || canonicalJsonStringify(files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
977
+ || !task.systemCommits.includes(commit)
978
+ ) {
979
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency-only Git continuity from stop override to current authorization is invalid.', {
980
+ commit,
981
+ expectedParent,
982
+ recordedParent: recovery.parentCommitSha,
983
+ parents,
984
+ files,
985
+ });
986
+ }
987
+ verifiedRecoveries.push(recovery);
988
+ previousRecoveryIndex = recoveryIndex;
989
+ previousRecoveryAt = recoveryAt;
990
+ expectedParent = commit;
991
+ }
992
+ if (expectedParent !== authorizedHead) {
993
+ throw new WorkflowError('STATE_CORRUPT', 'Dependency-provenance Git chain does not terminate at the authorized HEAD.');
994
+ }
995
+ return verifiedRecoveries;
996
+ }
997
+
998
+ function requireEvidenceBoundAuditTaskRevision(
999
+ task: TaskState,
1000
+ reboundAudit: PlanRiskAuditEvent,
1001
+ verifiedHeadRecoveries: DependencyProvenanceRecoveryRecord[],
1002
+ allowTaskRevisionAdvance: boolean,
1003
+ ): void {
1004
+ const auditAt = Date.parse(reboundAudit.recordedAt);
1005
+ if (!Number.isFinite(auditAt)) {
1006
+ throw new WorkflowError('STATE_CORRUPT', 'Current rebound Plan Risk Audit timestamp is invalid.');
1007
+ }
1008
+ const postAuditRecoveries = verifiedHeadRecoveries.filter(
1009
+ (recovery) => Date.parse(recovery.recordedAt) > auditAt,
1010
+ );
1011
+ const evidenceBoundTaskRevision = reboundAudit.taskRevision + 1 + postAuditRecoveries.length;
1012
+ if (
1013
+ (!allowTaskRevisionAdvance && evidenceBoundTaskRevision !== task.revision)
1014
+ || (allowTaskRevisionAdvance && evidenceBoundTaskRevision > task.revision)
1015
+ ) {
1016
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit chain does not bind the evidence-accounted Task authorization boundary.', {
1017
+ reboundAuditTaskRevision: reboundAudit.taskRevision,
1018
+ currentTaskRevision: task.revision,
1019
+ postAuditDependencyRecoveryCommits: postAuditRecoveries.map((recovery) => recovery.commitSha),
1020
+ evidenceBoundTaskRevision,
1021
+ allowTaskRevisionAdvance,
1022
+ });
1023
+ }
1024
+ }
1025
+
1026
+ function assertRebindMatchesPlans(
1027
+ rebind: TaskKnowledgeRebindRecord,
1028
+ plans: {
1029
+ decisionBinding: { revision: number; hash: string };
1030
+ reboundBinding: { revision: number; hash: string };
1031
+ },
1032
+ ): void {
1033
+ if (
1034
+ rebind.fromKnowledgeMapRevision !== plans.decisionBinding.revision
1035
+ || rebind.fromKnowledgeMapHash !== plans.decisionBinding.hash
1036
+ || rebind.toKnowledgeMapRevision !== plans.reboundBinding.revision
1037
+ || rebind.toKnowledgeMapHash !== plans.reboundBinding.hash
1038
+ ) {
1039
+ throw new WorkflowError('STATE_CORRUPT', 'Direct Knowledge rebind does not match the two verified Plan artifacts.');
1040
+ }
1041
+ }
1042
+
1043
+ function requireRefreshAuthorization(task: TaskState): AuthorizationEvent {
1044
+ const matches = task.authorizations.filter((authorization) =>
1045
+ authorization.kind === 'execution'
1046
+ && authorization.decision === 'approved'
1047
+ && authorization.planHash === task.planHash
1048
+ && authorization.briefHash === task.briefHash,
1049
+ );
1050
+ if (matches.length !== 1) {
1051
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery requires one exact current execution authorization.', {
1052
+ taskId: task.id,
1053
+ authorizationCount: matches.length,
1054
+ });
1055
+ }
1056
+ const authorization = matches[0]!;
1057
+ if (
1058
+ authorization.authorizationMode !== 'delegated'
1059
+ || !authorization.delegation
1060
+ || authorization.delegation.transition !== 'task.execution_authorize'
1061
+ || !REFRESH_REASONS.has(authorization.reason)
1062
+ ) {
1063
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution authorization is not confirmed delegated context-refresh evidence.');
1064
+ }
1065
+ return authorization;
1066
+ }
1067
+
1068
+ function requireRefreshAuthority(
1069
+ store: FileStateStore,
1070
+ task: TaskState,
1071
+ map: ProjectKnowledgeMap,
1072
+ authorization: AuthorizationEvent,
1073
+ rebind: TaskKnowledgeRebindRecord,
1074
+ ): { grant: DelegationGrant; autonomyContract: MilestoneAutonomyEvent | null } {
1075
+ const approval = map.approval;
1076
+ const taskDelegation = authorization.delegation!;
1077
+ if (
1078
+ map.status !== 'active'
1079
+ || !approval
1080
+ || map.revision !== task.knowledgeMapRevision
1081
+ || hashKnowledgeMap(map) !== task.knowledgeMapHash
1082
+ || approval.mapHash !== task.knowledgeMapHash
1083
+ || approval.authorizationMode !== 'delegated'
1084
+ || !approval.delegation
1085
+ || approval.delegation.transition !== 'project_memory.approve'
1086
+ ) {
1087
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Current Knowledge Map is not the delegated approved direct-rebind target.');
1088
+ }
1089
+ const mapDelegation = approval.delegation;
1090
+ if (
1091
+ approval.actor !== authorization.actor
1092
+ || mapDelegation.grantId !== taskDelegation.grantId
1093
+ || mapDelegation.policyHash !== taskDelegation.policyHash
1094
+ || mapDelegation.principal !== taskDelegation.principal
1095
+ || mapDelegation.delegate !== taskDelegation.delegate
1096
+ ) {
1097
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge Map and Task authorization do not share one delegated identity.');
1098
+ }
1099
+ const grant = store.readDelegation(task.projectId, taskDelegation.grantId);
1100
+ const expectedPolicyHash = hashDelegationPolicy(grant.projectId, {
1101
+ principal: grant.principal,
1102
+ delegate: grant.delegate,
1103
+ scope: grant.scope,
1104
+ transitions: grant.transitions,
1105
+ expiresAt: grant.expiresAt,
1106
+ });
1107
+ const autonomyContract = requireRefreshGrantScope(store, task, grant);
1108
+ if (
1109
+ grant.policyHash !== expectedPolicyHash
1110
+ || grant.policyHash !== taskDelegation.policyHash
1111
+ || grant.principal !== taskDelegation.principal
1112
+ || grant.delegate !== taskDelegation.delegate
1113
+ || approval.actor !== grant.delegate
1114
+ || authorization.actor !== grant.delegate
1115
+ || grant.status !== 'active'
1116
+ || grant.revokedAt !== null
1117
+ || !grant.transitions.includes('project_memory.approve')
1118
+ || !grant.transitions.includes('task.execution_authorize')
1119
+ ) {
1120
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation evidence is revoked, corrupt, mismatched, or insufficient for recovery.');
1121
+ }
1122
+ const createdAt = Date.parse(grant.createdAt);
1123
+ const expiresAt = Date.parse(grant.expiresAt);
1124
+ const approvalAt = Date.parse(approval.approvedAt);
1125
+ const authorizationAt = Date.parse(authorization.recordedAt);
1126
+ if (
1127
+ ![createdAt, expiresAt, approvalAt, authorizationAt].every(Number.isFinite)
1128
+ || approvalAt < createdAt
1129
+ || authorizationAt < createdAt
1130
+ || approvalAt >= expiresAt
1131
+ || authorizationAt >= expiresAt
1132
+ ) {
1133
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Delegated context-refresh approvals were not recorded inside the historical grant validity window.');
1134
+ }
1135
+ if (
1136
+ rebind.toKnowledgeMapRevision !== map.revision
1137
+ || rebind.toKnowledgeMapHash !== approval.mapHash
1138
+ ) {
1139
+ throw new WorkflowError('STATE_CORRUPT', 'Knowledge Map approval does not bind the direct rebind target.');
1140
+ }
1141
+ return { grant, autonomyContract };
1142
+ }
1143
+
1144
+ function requireRefreshGrantScope(
1145
+ store: FileStateStore,
1146
+ task: TaskState,
1147
+ grant: DelegationGrant,
1148
+ ): MilestoneAutonomyEvent | null {
1149
+ if (grant.scope.kind === 'project') return null;
1150
+ if (grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
1151
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation scope cannot authorize the Knowledge Map refresh target.');
1152
+ }
1153
+ const contracts = readMilestoneAutonomyEvents(store, task.projectId, grant.scope.id);
1154
+ const matches = contracts.filter((contract) =>
1155
+ contract.delegationGrantId === grant.id
1156
+ && contract.delegationPolicyHash === grant.policyHash
1157
+ && contract.principal === grant.principal
1158
+ && contract.delegate === grant.delegate
1159
+ && contract.expiresAt === grant.expiresAt,
1160
+ );
1161
+ if (matches.length !== 1) {
1162
+ throw new WorkflowError(
1163
+ 'TRANSITION_BLOCKED',
1164
+ 'Milestone-scoped context-refresh authority requires one exact alpha7 autonomy contract.',
1165
+ { grantId: grant.id, contractCount: matches.length },
1166
+ );
1167
+ }
1168
+ return matches[0]!;
1169
+ }
1170
+
1171
+ function requireC1Evidence(
1172
+ store: FileStateStore,
1173
+ task: TaskState,
1174
+ actor: string,
1175
+ writerToken: string | null,
1176
+ ): { posture: string; claimEventHash: string | null; owner: string | null; tokenHash: string | null } {
1177
+ const posture = readTaskC1Posture(store, task);
1178
+ const lease = readWriterLease(store, task);
1179
+ if (posture.state === 'pending') {
1180
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pending C1 handoff must be claimed before corrective recovery.');
1181
+ }
1182
+ if (posture.state === 'claimed') {
1183
+ const tokenHash = writerToken ? sha256Hex(writerToken) : null;
1184
+ if (
1185
+ !lease
1186
+ || actor !== posture.claimant
1187
+ || lease.owner !== posture.claimant
1188
+ || !writerToken
1189
+ || lease.token !== writerToken
1190
+ || tokenHash !== posture.writerLeaseTokenHash
1191
+ ) {
1192
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery requires the claimed C1 actor and exact bound writer lease token.', {
1193
+ taskId: task.id,
1194
+ claimant: posture.claimant,
1195
+ leaseOwner: lease?.owner ?? null,
1196
+ });
1197
+ }
1198
+ return { posture: 'claimed', claimEventHash: posture.latestEvent.eventHash, owner: lease.owner, tokenHash };
1199
+ }
1200
+ if (lease && (lease.owner !== actor || !writerToken || lease.token !== writerToken)) {
1201
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Existing writer lease is not owned by the recovering actor/token.');
1202
+ }
1203
+ return {
1204
+ posture: 'none',
1205
+ claimEventHash: posture.latestEvent?.eventHash ?? null,
1206
+ owner: lease?.owner ?? null,
1207
+ tokenHash: lease ? sha256Hex(lease.token) : null,
1208
+ };
1209
+ }
1210
+
1211
+ function readWriterLease(store: FileStateStore, task: TaskState): WriterLease | null {
1212
+ const file = path.join(store.lockRoot(task.projectId), `${task.id}.json`);
1213
+ if (!existsSync(file)) return null;
1214
+ let lease: WriterLease;
1215
+ try {
1216
+ lease = JSON.parse(readFileSync(file, 'utf8')) as WriterLease;
1217
+ } catch {
1218
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease is unreadable: ${file}`);
1219
+ }
1220
+ if (lease.entityId !== task.id || typeof lease.owner !== 'string' || typeof lease.token !== 'string') {
1221
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease does not match Task ${task.id}.`);
1222
+ }
1223
+ return lease;
1224
+ }
1225
+
1226
+ function assertRefreshChronology(
1227
+ map: ProjectKnowledgeMap,
1228
+ authorization: AuthorizationEvent,
1229
+ rebind: TaskKnowledgeRebindRecord,
1230
+ reboundAudit: PlanRiskAuditEvent,
1231
+ ): void {
1232
+ const values = [
1233
+ Date.parse(map.approval!.approvedAt),
1234
+ Date.parse(rebind.recordedAt),
1235
+ Date.parse(reboundAudit.recordedAt),
1236
+ Date.parse(authorization.recordedAt),
1237
+ ];
1238
+ if (values.some((value) => !Number.isFinite(value)) || values.some((value, index) => index > 0 && value < values[index - 1]!)) {
1239
+ throw new WorkflowError('STATE_CORRUPT', 'Context-refresh evidence chronology is invalid.');
1240
+ }
1241
+ }
1242
+
1243
+ function assertRecoveryMatchesAssessment(
1244
+ event: CorrectiveDecisionRecoveryEvent,
1245
+ assessment: CorrectiveRecoveryAssessment,
1246
+ actor: string,
1247
+ ): void {
1248
+ const expected = recoveryPayload(assessment, actor, new Date(event.recordedAt));
1249
+ const ignored = new Set(['eventId', 'recordedAt']);
1250
+ const mismatches = Object.entries(expected)
1251
+ .filter(([key, value]) => !ignored.has(key) && event[key as keyof CorrectiveDecisionRecoveryEvent] !== value)
1252
+ .map(([key]) => key);
1253
+ if (mismatches.length > 0) {
1254
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery event is stale or conflicts with current evidence.', {
1255
+ taskId: event.taskId,
1256
+ stepId: event.stepId,
1257
+ eventId: event.eventId,
1258
+ mismatches,
1259
+ });
1260
+ }
1261
+ }
1262
+
1263
+ function validateRecoveryEvent(
1264
+ event: CorrectiveDecisionRecoveryEvent,
1265
+ task: TaskState,
1266
+ file: string,
1267
+ line: number,
1268
+ ): void {
1269
+ if (!/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId) || event.taskId !== task.id) {
1270
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery event identity is invalid.', { file, line });
1271
+ }
1272
+ if (typeof event.stepId !== 'string' || !event.stepId || event.triggeringAttemptCount !== 3) {
1273
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery Step/ordinal binding is invalid.', { file, line });
1274
+ }
1275
+ const hashes = [
1276
+ event.correctiveDecisionEventHash,
1277
+ event.decisionPlanHash,
1278
+ event.reboundPlanHash,
1279
+ event.semanticPlanHash,
1280
+ event.fromKnowledgeMapHash,
1281
+ event.toKnowledgeMapHash,
1282
+ event.sourcePlanRiskAuditEventHash,
1283
+ event.reboundPlanRiskAuditEventHash,
1284
+ event.knowledgeMapApprovalFingerprint,
1285
+ event.taskAuthorizationFingerprint,
1286
+ event.delegationGrantFingerprint,
1287
+ event.c1EvidenceFingerprint,
1288
+ event.evidenceFingerprint,
1289
+ ];
1290
+ for (const hash of hashes) assertHash64(hash, `Corrective recovery hash at ${file}:${line}`);
1291
+ if (
1292
+ !Number.isInteger(event.fromKnowledgeMapRevision)
1293
+ || event.fromKnowledgeMapRevision < 1
1294
+ || !Number.isInteger(event.toKnowledgeMapRevision)
1295
+ || event.toKnowledgeMapRevision < 1
1296
+ || typeof event.actor !== 'string'
1297
+ || !event.actor.trim()
1298
+ ) {
1299
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery payload is malformed.', { file, line });
1300
+ }
1301
+ }
1302
+
1303
+ export function readValidatedCorrectiveDecisionRecoveryForNavigation(
1304
+ store: FileStateStore,
1305
+ task: TaskState,
1306
+ stepId: string,
1307
+ ): { assessment: CorrectiveRecoveryAssessment; recovery: CorrectiveDecisionRecoveryEvent | null } {
1308
+ const posture = readTaskC1Posture(store, task);
1309
+ const lease = readWriterLease(store, task);
1310
+ const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
1311
+ const assessment = assessCorrectiveDecisionRecovery(store, task, stepId, actor, lease?.token ?? null);
1312
+ const matches = readCorrectiveDecisionRecoveryEvents(store, task).filter((event) =>
1313
+ event.correctiveDecisionEventId === assessment.decision.eventId
1314
+ && event.stepId === stepId
1315
+ && event.triggeringAttemptCount === assessment.decision.triggeringAttemptCount,
1316
+ );
1317
+ if (matches.length > 1) {
1318
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple recoveries exist for one corrective decision binding.');
1319
+ }
1320
+ const recovery = matches[0] ?? null;
1321
+ if (recovery) assertRecoveryMatchesAssessment(recovery, assessment, actor);
1322
+ return { assessment, recovery };
1323
+ }
1324
+
1325
+ export function assertKnowledgeBindingOnlyPlanChange(historical: string, current: string): string {
1326
+ const decision = normalizePlanKnowledgeBinding(historical, 'historical');
1327
+ const rebound = normalizePlanKnowledgeBinding(current, 'current');
1328
+ if (decision.normalized !== rebound.normalized) {
1329
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery permits only the Knowledge Map binding line to change.');
1330
+ }
1331
+ return sha256Hex(decision.normalized);
1332
+ }