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
@@ -1,10 +1,20 @@
1
+ import { assessNavigationDirtyCarryover } from './alpha6/navigation-dirty-carryover.js';
2
+ import { navigationUpdateSource, assertNavigationSourceInstalled, inspectNavigationTransport } from './navigation-update.js';
3
+ import { verifyNavigationUpdateArtifact } from './navigation-update-artifact.js';
4
+ import { taskSyncChecks, taskSyncChecksHash, assertNoPendingBaseSyncValidation, assessPendingBaseSyncValidation, assertPendingBaseSyncValidationReady } from './checks/task-sync.js';
5
+ import { isVirginRegistrationRoot } from './domain/virgin-registration.js';
6
+ import { currentBaseSyncConflict, recordBaseSyncConflict } from './domain/base-sync-conflict.js';
7
+ import { stepStartPhase } from './domain/step-start-admission.js';
8
+ import { correctiveDecisionCheckoutEligible } from './alpha6/corrective-decision-boundary.js';
9
+ import { prepareCapturedCheckEvidence, assertCapturedCheckEvidence, assertCurrentCapturedCheckEvidence } from './alpha6/captured-check-evidence.js';
10
+ import { readCorrectivePlanningAudit, readFailedStepPlanningRecovery } from './alpha6/failed-step-planning-recovery.js';
1
11
  import { explainChange } from './change-explanation.js';
2
- import { pendingReviewRunnerFingerprint, PENDING_REVIEW_UPDATE_SIDECAR, preparePendingReviewUpdateEvent, PENDING_REVIEW_SOURCE_VERSION, pendingReviewSourceBinding, pendingReviewBindingHash, assertPendingReviewTransport, verifiedPendingReviewTransport } from './pending-review-update.js';
12
+ import { verifiedPendingReviewTransport } from './pending-review-update.js';
3
13
  import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
4
14
  import { createHash } from 'node:crypto';
5
15
  import path from 'node:path';
6
16
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
7
- import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, workflowDependencyVersionAt, } from './dependency-provenance.js';
17
+ import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
8
18
  import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
9
19
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
10
20
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
@@ -14,19 +24,20 @@ import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './do
14
24
  import { pathAllowed, validatePlan } from './domain/validation.js';
15
25
  import { WorkflowError } from './errors.js';
16
26
  import { withObservedRouteMetadata } from './observed-routes.js';
27
+ import { withWorkflowBlockerRoute } from './workflow-blocker-route.js';
17
28
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
18
- import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
29
+ import { inspectWorkflowVersions } from './diagnostics.js';
19
30
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
20
- import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
31
+ import { assessDownstreamProofCarryover, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, prepareDownstreamProofInvalidation, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
21
32
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
22
- import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
33
+ import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
23
34
  import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
24
35
  import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
25
- import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
36
+ import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, isKnowledgeReboundSupportDecision, findCurrentGuardedRemediationPosture, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
26
37
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
27
38
  import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
28
39
  import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
29
- import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
40
+ import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, assessMilestoneScopeCompletion, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
30
41
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
31
42
  import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
32
43
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
@@ -37,7 +48,7 @@ import { ensureDirectory } from './fs-utils.js';
37
48
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
38
49
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
39
50
  import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
40
- import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
51
+ import { CoreTaskOperationLockManager, inspectCoreTaskOperationMutex, listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
41
52
  import { FileStateStore } from './state/store.js';
42
53
  import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
43
54
  import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
@@ -50,10 +61,12 @@ import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluate
50
61
  const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
51
62
  export class WorkflowService {
52
63
  store;
64
+ checkOptions;
53
65
  now;
54
66
  transactionHooks;
55
- constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
67
+ constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}, checkOptions = {}) {
56
68
  this.store = store;
69
+ this.checkOptions = checkOptions;
57
70
  this.now = now;
58
71
  this.transactionHooks = transactionHooks;
59
72
  }
@@ -88,6 +101,9 @@ export class WorkflowService {
88
101
  const projectFile = path.join(projectRoot, 'project.json');
89
102
  const projectAlreadyRegistered = existsSync(projectFile);
90
103
  if (!projectAlreadyRegistered) {
104
+ if (!isVirginRegistrationRoot(projectRoot, this.store.root, identity, PACKAGE_VERSION)) {
105
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Missing registration with retained or unrecognized state requires explicit recovery.');
106
+ }
91
107
  // The registration posture is a sidecar under the project root. A completely
92
108
  // fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
93
109
  // only that directory before writing the posture; project.json remains the
@@ -115,6 +131,36 @@ export class WorkflowService {
115
131
  return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
116
132
  }
117
133
  repairStaleWriterLease(repository, entityId) {
134
+ const identity = resolveRepositoryIdentity(repository);
135
+ const locksRoot = this.store.lockRoot(identity.projectId);
136
+ const core = inspectCoreTaskOperationMutex(locksRoot, entityId, this.store.root);
137
+ if (core) {
138
+ const project = this.store.readProject(identity.projectId);
139
+ if (!project.repositoryRoots.includes(identity.repositoryRoot)) {
140
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Repository is not registered for Core orphan repair.');
141
+ }
142
+ const taskRoot = this.store.taskRoot(identity.projectId, entityId);
143
+ this.store.readTask(identity.projectId, entityId);
144
+ return new CoreTaskOperationLockManager(locksRoot, this.now, this.store.root).repairExpiredOrphan(entityId, (mutex) => {
145
+ const replan = inspectCorrectiveReplanTaskTransactions(this.store.projectRoot(identity.projectId), this.store.root);
146
+ const yielded = inspectCorrectiveYieldTaskTransactions(this.store.projectRoot(identity.projectId), this.store.root);
147
+ if ([...replan.pending, ...replan.corrupt, ...yielded.pending, ...yielded.corrupt].some(item => item.taskId === entityId)) {
148
+ throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'Pending or corrupt Task journal state requires transaction recovery.');
149
+ }
150
+ for (const name of ['.transactions', '.yield-transactions']) {
151
+ const exactTransaction = path.join(taskRoot, name, mutex.operationId);
152
+ try {
153
+ lstatSync(exactTransaction);
154
+ }
155
+ catch (error) {
156
+ if (error.code === 'ENOENT')
157
+ continue;
158
+ throw error;
159
+ }
160
+ throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'The Core operation has retained journal state; use transaction recovery.');
161
+ }
162
+ });
163
+ }
118
164
  const context = this.#mutationContext(repository);
119
165
  const lease = context.locks.repairStale(entityId);
120
166
  return {
@@ -122,31 +168,6 @@ export class WorkflowService {
122
168
  repaired: true,
123
169
  };
124
170
  }
125
- prepareTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason) {
126
- const snapshot = this.observationSnapshot(repository);
127
- if (snapshot.observation.kind !== 'assessment') {
128
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
129
- observation: snapshot.observation,
130
- });
131
- }
132
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId) ?? null;
133
- if (!task)
134
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
135
- this.assertStopOverrideRepositoryBoundary(snapshot.identity, task);
136
- return prepareStopEscalateOverride(this.store, task, stepId, expectedRevision, actor, reason, headCommit(snapshot.identity.repositoryRoot));
137
- }
138
- applyTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason, confirmationCode) {
139
- const snapshot = this.observationSnapshot(repository);
140
- if (snapshot.observation.kind !== 'assessment') {
141
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
142
- observation: snapshot.observation,
143
- });
144
- }
145
- const context = this.#mutationContext(repository);
146
- const task = context.store.readTask(context.identity.projectId, taskId);
147
- this.assertStopOverrideRepositoryBoundary(context.identity, task, context.locks.list());
148
- return appendStopEscalateOverride(context.store, task, stepId, expectedRevision, actor, reason, headCommit(context.identity.repositoryRoot), confirmationCode, this.now());
149
- }
150
171
  prepareCorrectiveReplan(repository, taskId, candidate) {
151
172
  return this.correctiveReplanController(repository).prepare({ taskId, candidate });
152
173
  }
@@ -292,92 +313,6 @@ export class WorkflowService {
292
313
  invalidation: invalidation.event,
293
314
  };
294
315
  }
295
- recoverDownstreamProofReplan(repository, taskId, expectedTaskRevision, actor, reason) {
296
- const context = this.#mutationContext(repository);
297
- const task = this.store.readTask(context.identity.projectId, taskId);
298
- assertExpectedRevision(task, expectedTaskRevision);
299
- // This repair intentionally runs before Project Knowledge refresh: the exact
300
- // dependency-only transport can make the current map stale, while the
301
- // recovery assessment still binds the stranded Task to its recorded rebind.
302
- const observation = this.observationSnapshot(repository).observation;
303
- if (observation.kind !== 'assessment') {
304
- throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan recovery requires a coherent observation boundary.', {
305
- observation,
306
- });
307
- }
308
- const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
309
- const assessment = assessDownstreamProofReplanRecovery(this.store, context.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
310
- const environmentBlockers = [
311
- ...(!assessment.dependencyRecoveryCommit
312
- ? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
313
- : []),
314
- ...(observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
315
- ...(observation.pendingProjectTransactions.length > 0
316
- || observation.pendingMilestoneTransactions.length > 0
317
- || observation.pendingTaskTransactions.length > 0
318
- ? ['No pending Workflow transaction may exist.'] : []),
319
- ...(observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
320
- ...(observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
321
- ];
322
- if (!assessment.applicable || !assessment.eligible || environmentBlockers.length > 0) {
323
- throw new WorkflowError('TRANSITION_BLOCKED', 'Task is not eligible for bounded downstream-proof replan recovery.', {
324
- taskId,
325
- blockers: [...assessment.blockers, ...environmentBlockers],
326
- });
327
- }
328
- const now = this.now();
329
- const resultTask = {
330
- ...task,
331
- revision: task.revision + 1,
332
- updatedAt: now.toISOString(),
333
- status: 'needs_fix',
334
- authorizations: task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
335
- ? { ...authorization, decision: 'superseded' }
336
- : authorization),
337
- review: emptyReview(),
338
- resultHash: null,
339
- evidenceHash: null,
340
- blockReason: `${assessment.invalidation.activeStepId} downstream-proof recovery requires replacement of the full unfinished Step Plan before execution can resume.`,
341
- };
342
- const recovery = prepareDownstreamProofReplanRecovery(this.store, resultTask, assessment, { previousTaskRevision: task.revision, actor, reason }, now);
343
- const projectRoot = this.store.projectRoot(task.projectId);
344
- const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
345
- const taskRoot = this.store.taskRoot(task.projectId, task.id);
346
- applyProjectTransaction({
347
- projectRoot,
348
- projectId: task.projectId,
349
- transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
350
- kind: 'downstream-proof-replan-recovery',
351
- now,
352
- targets: [
353
- {
354
- path: relative(path.join(taskRoot, downstreamProofReplanRecoverySidecar())),
355
- content: recovery.postimage,
356
- },
357
- { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
358
- ],
359
- validate: () => {
360
- const saved = this.store.readTask(task.projectId, task.id);
361
- const events = readDownstreamProofReplanRecoveries(this.store, saved);
362
- if (saved.revision !== resultTask.revision
363
- || saved.status !== 'needs_fix'
364
- || saved.planHash !== task.planHash
365
- || events.at(-1)?.eventHash !== recovery.event.eventHash
366
- || headCommit(context.identity.repositoryRoot) !== recovery.event.preservedHeadCommit
367
- || JSON.stringify(changedFiles(context.identity.repositoryRoot).sort())
368
- !== JSON.stringify(recovery.event.dirtyFiles)) {
369
- throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery transaction readback failed.', {
370
- taskId,
371
- });
372
- }
373
- validateTaskHistory(context.identity.repositoryRoot, saved);
374
- },
375
- });
376
- return {
377
- task: this.store.readTask(task.projectId, task.id),
378
- recovery: recovery.event,
379
- };
380
- }
381
316
  correctiveReplanController(repository) {
382
317
  const identity = resolveRepositoryIdentity(repository);
383
318
  return createCorrectiveReplanPublicController({
@@ -565,6 +500,7 @@ export class WorkflowService {
565
500
  if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
566
501
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
567
502
  }
503
+ assertMilestoneScopeChangeStatusAllowed(milestone);
568
504
  if (milestone.baseBranch !== baseBranch) {
569
505
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task and Milestone must use the same base branch.', {
570
506
  taskBaseBranch: baseBranch,
@@ -1006,7 +942,7 @@ export class WorkflowService {
1006
942
  milestoneId: milestone.id,
1007
943
  oldTaskId: oldTask.id,
1008
944
  incidentDependencies,
1009
- requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
945
+ requiredAction: 'The owner must decide whether to stop this Task or revise the Milestone scope through supported transitions; structural replacement is unavailable.',
1010
946
  });
1011
947
  }
1012
948
  /* P04-B retains the existing one-for-one transaction below as implementation evidence,
@@ -1519,34 +1455,37 @@ export class WorkflowService {
1519
1455
  locks.release(milestoneId, authorityLease.token);
1520
1456
  }
1521
1457
  }
1458
+ assertMilestoneValidationReady(repositoryRoot, milestone) {
1459
+ if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
1460
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} cannot be validated while ${milestone.status}.`);
1461
+ }
1462
+ this.assertMilestoneAuthorizationFresh(milestone);
1463
+ if (currentBranch(repositoryRoot) !== milestone.baseBranch || !isClean(repositoryRoot)) {
1464
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
1465
+ expectedBranch: milestone.baseBranch,
1466
+ actualBranch: currentBranch(repositoryRoot),
1467
+ changedFiles: changedFiles(repositoryRoot),
1468
+ });
1469
+ }
1470
+ const incomplete = milestone.memberships.filter((membership) => {
1471
+ if (membership.disposition !== 'required')
1472
+ return false;
1473
+ return this.store.readTask(milestone.projectId, membership.taskId).status !== 'merged';
1474
+ });
1475
+ if (incomplete.length > 0) {
1476
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
1477
+ taskIds: incomplete.map((membership) => membership.taskId),
1478
+ });
1479
+ }
1480
+ }
1522
1481
  validateMilestone(repository, milestoneId, expectedRevision) {
1523
1482
  const { identity, locks } = this.#mutationContext(repository);
1524
1483
  const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1525
1484
  try {
1526
1485
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1527
1486
  assertExpectedRevision(milestone, expectedRevision);
1528
- if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
1529
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
1530
- }
1531
- this.assertMilestoneAuthorizationFresh(milestone);
1532
- if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1533
- throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
1534
- expectedBranch: milestone.baseBranch,
1535
- actualBranch: currentBranch(identity.repositoryRoot),
1536
- changedFiles: changedFiles(identity.repositoryRoot),
1537
- });
1538
- }
1539
- const incomplete = milestone.memberships.filter((membership) => {
1540
- if (membership.disposition !== 'required')
1541
- return false;
1542
- return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
1543
- });
1544
- if (incomplete.length > 0) {
1545
- throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
1546
- taskIds: incomplete.map((membership) => membership.taskId),
1547
- });
1548
- }
1549
- const checks = runChecks(identity.repositoryRoot, milestone.checks);
1487
+ this.assertMilestoneValidationReady(identity.repositoryRoot, milestone);
1488
+ const checks = runChecks(identity.repositoryRoot, milestone.checks, this.checkOptions);
1550
1489
  const baseHead = headCommit(identity.repositoryRoot);
1551
1490
  const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1552
1491
  const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
@@ -2094,10 +2033,10 @@ export class WorkflowService {
2094
2033
  const { identity } = this.#mutationContext(repository);
2095
2034
  const task = this.store.readTask(identity.projectId, taskId);
2096
2035
  assertExpectedRevision(task, expectedRevision);
2097
- this.assertTaskContextChangeAllowed(task);
2098
- if (!taskCanRebindKnowledge(task)) {
2099
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while Task is ${task.status}.`);
2100
- }
2036
+ const contextBlocker = this.taskContextChangeBlocker(task, 'rebind');
2037
+ // Preserve Plan-corruption precedence over the active-Step rejection.
2038
+ if (contextBlocker && !contextBlocker.activeStep)
2039
+ throw contextBlocker.error;
2101
2040
  if (!task.planHash ||
2102
2041
  !task.planObjective ||
2103
2042
  task.knowledgeMapRevision === null ||
@@ -2107,10 +2046,8 @@ export class WorkflowService {
2107
2046
  task.graphUse === null) {
2108
2047
  throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no complete Plan binding to rebind.`);
2109
2048
  }
2110
- const activeStep = task.steps.find((step) => step.status === 'in_progress');
2111
- if (activeStep) {
2112
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
2113
- }
2049
+ if (contextBlocker)
2050
+ throw contextBlocker.error;
2114
2051
  const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
2115
2052
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
2116
2053
  if (task.knowledgeMapRevision === knowledgeMap.revision &&
@@ -2187,21 +2124,20 @@ export class WorkflowService {
2187
2124
  const currentMap = this.store.readKnowledgeMap(identity.projectId);
2188
2125
  assertExpectedRevision(task, expectedTaskRevision);
2189
2126
  assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
2190
- this.assertTaskContextChangeAllowed(task);
2191
- if (!taskCanRebindKnowledge(task)) {
2192
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
2193
- }
2194
- const activeStep = task.steps.find((step) => step.status === 'in_progress');
2195
- if (activeStep) {
2196
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
2197
- }
2127
+ const contextBlocker = this.taskContextChangeBlocker(task, 'refresh');
2128
+ if (contextBlocker)
2129
+ throw contextBlocker.error;
2198
2130
  const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
2199
2131
  const transitions = [];
2200
- const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2132
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
2133
+ const currentPlanRiskAudit = planningRecovery?.current ?? readCurrentPlanRiskAudit(this.store, task);
2201
2134
  const currentPlanArtifactHash = task.planHash
2202
2135
  ? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
2203
2136
  : null;
2204
2137
  const refreshAssessment = assessDelegatedKnowledgeRefresh(currentMap, inspectedMap, task, currentPlanArtifactHash);
2138
+ if (planningRecovery && refreshAssessment.mode !== 'delegated-content-only') {
2139
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stopped planning context refresh permits content-only Knowledge drift.');
2140
+ }
2205
2141
  // Validate every delegated transition before the first write so a narrow grant cannot
2206
2142
  // leave the composite operation half-applied.
2207
2143
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
@@ -2248,11 +2184,15 @@ export class WorkflowService {
2248
2184
  appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
2249
2185
  }
2250
2186
  transitions.push('task knowledge-rebind');
2251
- refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
2252
- ? 'Delegated Plan-bounded supporting-source context refresh.'
2253
- : 'Delegated content-only context refresh.', delegationGrantId);
2254
- transitions.push('task execution authorize');
2187
+ // A stopped Plan may regain its Knowledge binding, never execution authority.
2188
+ if (!planningRecovery)
2189
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
2190
+ ? 'Delegated Plan-bounded supporting-source context refresh.'
2191
+ : 'Delegated content-only context refresh.', delegationGrantId);
2192
+ if (!planningRecovery)
2193
+ transitions.push('task execution authorize');
2255
2194
  return {
2195
+ authority: planningRecovery ? 'planning-only' : 'execution',
2256
2196
  mode: refreshAssessment.mode,
2257
2197
  transitions,
2258
2198
  addedSources: refreshAssessment.addedSources,
@@ -2262,8 +2202,16 @@ export class WorkflowService {
2262
2202
  },
2263
2203
  validate: (result) => {
2264
2204
  const currentTask = this.store.readTask(identity.projectId, task.id);
2265
- if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
2266
- throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit an authorized Task.');
2205
+ if (currentTask.revision !== result.task.revision || currentTask.status !== (planningRecovery ? 'awaiting_execution_authorization' : 'ready')) {
2206
+ throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit the expected Task posture.');
2207
+ }
2208
+ if (planningRecovery) {
2209
+ const rebound = readFailedStepPlanningRecovery(this.store, currentTask);
2210
+ if (!rebound || rebound.baseline.eventHash !== planningRecovery.baseline.eventHash
2211
+ || JSON.stringify(currentTask.steps) !== JSON.stringify(task.steps)
2212
+ || JSON.stringify(currentTask.authorizations) !== JSON.stringify(task.authorizations)) {
2213
+ throw new WorkflowError('STATE_CORRUPT', 'Planning context refresh changed retained recovery evidence.');
2214
+ }
2267
2215
  }
2268
2216
  const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
2269
2217
  if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
@@ -2336,6 +2284,7 @@ export class WorkflowService {
2336
2284
  const { identity } = this.#mutationContext(repository);
2337
2285
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2338
2286
  const task = this.store.readTask(identity.projectId, taskId);
2287
+ assertNoPendingBaseSyncValidation(task);
2339
2288
  assertExpectedRevision(task, expectedRevision);
2340
2289
  if (readCurrentPreExecutionReplan(this.store, task)) {
2341
2290
  throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires task plan-set; the blocking audit cannot be replaced in place.', { taskId, requiredAction: 'task plan-set' });
@@ -2486,7 +2435,16 @@ export class WorkflowService {
2486
2435
  handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
2487
2436
  const context = this.#mutationContext(repository);
2488
2437
  const task = this.store.readTask(context.identity.projectId, taskId);
2438
+ assertNoPendingBaseSyncValidation(task);
2489
2439
  assertExpectedRevision(task, expectedRevision);
2440
+ if (task.status === 'ready' && changedFiles(context.identity.repositoryRoot).length > 0) {
2441
+ const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task);
2442
+ if (carryover.requiredActor && targetActor.trim() !== carryover.requiredActor) {
2443
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Native carryover handoff must return to its recorded execution actor.', {
2444
+ taskId, requiredActor: carryover.requiredActor,
2445
+ });
2446
+ }
2447
+ }
2490
2448
  if (readCurrentPreExecutionReplan(this.store, task)) {
2491
2449
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
2492
2450
  taskId,
@@ -2648,8 +2606,12 @@ export class WorkflowService {
2648
2606
  });
2649
2607
  }
2650
2608
  }
2651
- const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
2652
- const action = String(navigation.action);
2609
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
2610
+ const navigation = planningRecovery
2611
+ ? this.nextForTaskWithDelegation(task.projectId, context.identity.repositoryRoot, task, planningRecovery.current)
2612
+ : this.taskGitNavigationOverride(task) ?? nextForTask(task);
2613
+ const action = String(navigation.action === 'task writer-credential-replace'
2614
+ ? navigation.blockedAction : navigation.action);
2653
2615
  const currentLease = context.locks.inspect(taskId);
2654
2616
  const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
2655
2617
  const requiredActor = writerTokenContract
@@ -2786,7 +2748,7 @@ export class WorkflowService {
2786
2748
  const step = requireStep(task, stepId);
2787
2749
  const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2788
2750
  const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
2789
- if (!['planned', 'failed'].includes(step.status)) {
2751
+ if (stepStartPhase(step.status, false) === 'not-startable') {
2790
2752
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
2791
2753
  }
2792
2754
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
@@ -2806,6 +2768,7 @@ export class WorkflowService {
2806
2768
  ? guardedPosture.correctiveDecision
2807
2769
  : null;
2808
2770
  const recovery = staleDecision?.decision === 'continue-fix'
2771
+ && !isKnowledgeReboundSupportDecision(this.store, task, staleDecision)
2809
2772
  ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
2810
2773
  : null;
2811
2774
  const stopOverrideContextRebind = staleDecision?.decision === 'stop-escalate'
@@ -2816,23 +2779,28 @@ export class WorkflowService {
2816
2779
  }
2817
2780
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2818
2781
  const dirty = changedFiles(context.identity.repositoryRoot);
2819
- const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
2782
+ const startPhase = stepStartPhase(step.status, dirty.length > 0);
2783
+ const downstreamProofCarryover = startPhase === 'planned-dirty'
2820
2784
  ? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
2821
2785
  : null;
2822
- const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2786
+ const planIntegrityDirtyCarryover = startPhase === 'planned-dirty'
2823
2787
  ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2824
2788
  : null;
2825
- const rootCauseReplanDirtyCarryover = step.status === 'planned' && dirty.length > 0
2789
+ const navigationDirtyCarryover = startPhase === 'planned-dirty'
2790
+ ? assessNavigationDirtyCarryover(this.store, context.identity.repositoryRoot, task, stepId)
2791
+ : null;
2792
+ const rootCauseReplanDirtyCarryover = startPhase === 'planned-dirty' && !navigationDirtyCarryover?.applicable
2826
2793
  ? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2827
2794
  : null;
2828
- if (step.status === 'planned'
2829
- && dirty.length > 0
2795
+ if (startPhase === 'planned-dirty'
2830
2796
  && !resumesCorrectiveReplan
2831
2797
  && !downstreamProofCarryover?.eligible
2832
2798
  && !planIntegrityDirtyCarryover?.eligible
2799
+ && !navigationDirtyCarryover?.eligible
2833
2800
  && !rootCauseReplanDirtyCarryover?.eligible) {
2834
2801
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2835
2802
  changedFiles: dirty,
2803
+ ...(navigationDirtyCarryover?.applicable ? { navigationDirtyCarryoverBlockers: navigationDirtyCarryover.blockers } : {}),
2836
2804
  ...(downstreamProofCarryover?.applicable
2837
2805
  ? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
2838
2806
  : {}),
@@ -2845,7 +2813,7 @@ export class WorkflowService {
2845
2813
  });
2846
2814
  }
2847
2815
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
2848
- if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
2816
+ if ((startPhase === 'failed-retry' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
2849
2817
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
2850
2818
  changedFiles: dirtyOutsideStep,
2851
2819
  });
@@ -3066,7 +3034,7 @@ export class WorkflowService {
3066
3034
  return task;
3067
3035
  }
3068
3036
  validateTaskHistory(context.identity.repositoryRoot, task);
3069
- const checks = runChecks(context.identity.repositoryRoot, step.checks);
3037
+ const checks = runChecks(context.identity.repositoryRoot, step.checks, this.checkOptions);
3070
3038
  const failed = checks.filter((check) => !check.passed);
3071
3039
  if (failed.length > 0) {
3072
3040
  const now = this.now();
@@ -3074,13 +3042,16 @@ export class WorkflowService {
3074
3042
  // Bind useful uncommitted work to this failure without treating it as
3075
3043
  // completed-Step authority.
3076
3044
  const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3045
+ const capturedEvidence = prepareCapturedCheckEvidence(this.store, task, checks);
3077
3046
  const failurePause = {
3078
3047
  recordedAt: now.toISOString(),
3079
3048
  dirtyFiles,
3080
3049
  dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
3081
3050
  checksHash: sha256Hex(canonicalJsonStringify(checks)),
3082
3051
  failedChecks: normalizedFailures,
3052
+ checksEvidence: capturedEvidence.reference,
3083
3053
  };
3054
+ const failureC1Posture = readTaskC1Posture(this.store, task);
3084
3055
  const preparedRemediation = reviewRequired && planRiskAudit
3085
3056
  ? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
3086
3057
  completionCommit: headCommit(context.identity.repositoryRoot),
@@ -3089,6 +3060,11 @@ export class WorkflowService {
3089
3060
  reviewerAttestationHash: null,
3090
3061
  executionTaskRevision: task.revision,
3091
3062
  dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
3063
+ ...(failureC1Posture.state === 'none' ? { unclaimedExecutionBoundary: {
3064
+ kind: 'native-unclaimed-check-failure',
3065
+ actor: lease.owner,
3066
+ priorHandoffEventHash: failureC1Posture.latestEvent?.eventHash ?? null,
3067
+ } } : {}),
3092
3068
  }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
3093
3069
  : null;
3094
3070
  const steps = task.steps.map((candidate) => candidate.id === stepId
@@ -3136,6 +3112,7 @@ export class WorkflowService {
3136
3112
  : 'failed-step-continuation',
3137
3113
  now,
3138
3114
  targets: [
3115
+ ...(capturedEvidence.target ? [capturedEvidence.target] : []),
3139
3116
  ...(preparedRemediation?.postimage
3140
3117
  ? [{
3141
3118
  path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
@@ -3151,6 +3128,7 @@ export class WorkflowService {
3151
3128
  validate: () => {
3152
3129
  const saved = this.store.readTask(task.projectId, task.id);
3153
3130
  const savedStep = requireStep(saved, stepId);
3131
+ assertCapturedCheckEvidence(this.store, saved, savedStep.failurePause, checks);
3154
3132
  const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3155
3133
  const remediationHash = preparedRemediation
3156
3134
  ? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
@@ -3515,6 +3493,7 @@ export class WorkflowService {
3515
3493
  prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
3516
3494
  const identity = resolveRepositoryIdentity(repository);
3517
3495
  const task = this.store.readTask(identity.projectId, taskId);
3496
+ assertNoPendingBaseSyncValidation(task);
3518
3497
  assertExpectedRevision(task, expectedRevision);
3519
3498
  const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
3520
3499
  if (task.status !== 'validating' && !retryingUnverified) {
@@ -3597,6 +3576,7 @@ export class WorkflowService {
3597
3576
  const { identity } = this.#mutationContext(repository);
3598
3577
  this.readCurrentAdoptionPostureStrict(identity.projectId);
3599
3578
  const task = this.store.readTask(identity.projectId, taskId);
3579
+ assertNoPendingBaseSyncValidation(task);
3600
3580
  assertExpectedRevision(task, expectedRevision);
3601
3581
  this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
3602
3582
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
@@ -3647,12 +3627,52 @@ export class WorkflowService {
3647
3627
  this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
3648
3628
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
3649
3629
  }
3650
- recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
3651
- const context = this.#mutationContext(repository);
3652
- const task = this.store.readTask(context.identity.projectId, taskId);
3653
- assertExpectedRevision(task, expectedRevision);
3654
- this.requireExecutionAdoptionPosture(context.identity, taskId);
3655
- return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
3630
+ /** Recording a machine finding may retain its exact failed execution context;
3631
+ * it never approves changed Knowledge or authorizes another task run. */
3632
+ assessPlanIntegrityRecoveryAdmission(repository, task, stepId) {
3633
+ const assessment = assessPlanIntegrityRecovery(this.store, repository, task, stepId);
3634
+ if (!assessment.eligible)
3635
+ return assessment;
3636
+ try {
3637
+ this.assertExecutionAuthorizationFresh(task, repository);
3638
+ const stored = this.store.readKnowledgeMap(task.projectId);
3639
+ const storedHash = hashKnowledgeMap(stored);
3640
+ if (stored.status !== 'active' || !stored.approval || stored.approval.mapHash !== storedHash
3641
+ || task.knowledgeMapRevision !== stored.revision || task.knowledgeMapHash !== storedHash) {
3642
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery requires the exact approved Task Knowledge binding.');
3643
+ }
3644
+ const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repository, task.projectId, this.now()));
3645
+ if (inspected.status === 'active' && inspected.approval?.mapHash === storedHash && hashKnowledgeMap(inspected) === storedHash)
3646
+ return assessment;
3647
+ if (inspected.status !== 'stale' || !isContentOnlyKnowledgeRefresh(stored, inspected)) {
3648
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery permits only content changes within the recorded failed execution.');
3649
+ }
3650
+ const step = requireStep(task, stepId);
3651
+ const pause = step.failurePause;
3652
+ const last = readRemediationEvents(this.store, task).filter(event => event.stepId === stepId).at(-1);
3653
+ const dirty = changedFiles(repository).sort();
3654
+ const changedKnowledge = inspected.entries.filter(entry => stored.entries.find(previous => previous.id === entry.id)?.contentHash !== entry.contentHash);
3655
+ // Only the immediately recorded native failure may defer Knowledge approval;
3656
+ // historical transport or later Task revisions never establish this admission.
3657
+ if (!pause || !last || last.failureKind !== 'checks-failed'
3658
+ || last.failureEvidence.executionTaskRevision !== task.revision - 1
3659
+ || last.failureEvidence.completionCommit !== headCommit(repository)
3660
+ || last.failureEvidence.checksHash !== pause.checksHash
3661
+ || last.failureEvidence.dirtyWorktreeHash !== pause.dirtyWorktreeHash
3662
+ || JSON.stringify(dirty) !== JSON.stringify([...pause.dirtyFiles].sort())
3663
+ || hashDirtyWorktree(repository, dirty) !== pause.dirtyWorktreeHash
3664
+ || changedKnowledge.some(entry => !dirty.includes(entry.path)
3665
+ || !pathAllowed(entry.path, step.allowedWrites) || pathAllowed(entry.path, step.forbiddenScope))) {
3666
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery Knowledge drift must match the exact native failed-Step HEAD, checks and dirty bytes.');
3667
+ }
3668
+ assertCurrentCapturedCheckEvidence(this.store, task, stepId);
3669
+ return assessment;
3670
+ }
3671
+ catch (error) {
3672
+ if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
3673
+ throw error;
3674
+ return { ...assessment, eligible: false, blockers: [...assessment.blockers, error.message] };
3675
+ }
3656
3676
  }
3657
3677
  recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
3658
3678
  const context = this.#mutationContext(repository);
@@ -3681,29 +3701,12 @@ export class WorkflowService {
3681
3701
  stepId,
3682
3702
  });
3683
3703
  }
3684
- const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
3704
+ const assessment = this.assessPlanIntegrityRecoveryAdmission(context.identity.repositoryRoot, task, stepId);
3685
3705
  if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
3686
3706
  throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
3687
- taskId,
3688
- stepId,
3689
- applicable: assessment.applicable,
3690
- blockers: assessment.blockers,
3707
+ taskId, stepId, applicable: assessment.applicable, blockers: assessment.blockers,
3691
3708
  });
3692
3709
  }
3693
- const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
3694
- ? [assessment.evidence.remediationEventId]
3695
- : assessment.evidence.remediationEvents.map((event) => event.eventId);
3696
- const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
3697
- && record.packageVersion === PACKAGE_VERSION
3698
- && task.systemCommits.includes(record.commitSha)
3699
- && record.planIntegrity?.stepId === stepId
3700
- && record.planIntegrity.planHash === task.planHash
3701
- && canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
3702
- && record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
3703
- if (!compatibility) {
3704
- this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
3705
- this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
3706
- }
3707
3710
  const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
3708
3711
  return { assessment, event };
3709
3712
  }
@@ -3714,10 +3717,14 @@ export class WorkflowService {
3714
3717
  assertExpectedRevision(task, expectedRevision);
3715
3718
  this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
3716
3719
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
3717
- if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
3720
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
3721
+ if (!['ready', 'needs_fix', 'blocked'].includes(task.status) && !planningRecovery) {
3718
3722
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
3719
3723
  }
3720
- const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
3724
+ if (planningRecovery && correctiveAudit.decision === 'continue-fix') {
3725
+ throw new WorkflowError('TRANSITION_BLOCKED', 'A stopped rebound Plan requires replan, split, or escalation; continue-fix cannot restore execution authority.', { taskId, stepId });
3726
+ }
3727
+ const planRiskAudit = readCorrectivePlanningAudit(this.store, task);
3721
3728
  if (!planRiskAudit) {
3722
3729
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
3723
3730
  taskId,
@@ -3750,6 +3757,9 @@ export class WorkflowService {
3750
3757
  resolution: currentCycle,
3751
3758
  });
3752
3759
  }
3760
+ if (!correctiveDecisionCheckoutEligible(this.store, identity.repositoryRoot, task, stepId)) {
3761
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective decision requires the Task checkout to be clean or match its exact in-scope failed checkpoint.', { taskId, stepId });
3762
+ }
3753
3763
  assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
3754
3764
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
3755
3765
  }
@@ -3757,6 +3767,7 @@ export class WorkflowService {
3757
3767
  const context = this.#mutationContext(repository);
3758
3768
  const { identity } = context;
3759
3769
  const task = this.store.readTask(identity.projectId, taskId);
3770
+ assertNoPendingBaseSyncValidation(task);
3760
3771
  assertExpectedRevision(task, expectedRevision);
3761
3772
  if (task.status !== 'validating' || task.review.status !== 'passed') {
3762
3773
  throw new WorkflowError('TRANSITION_BLOCKED', 'Result requires a passing independent review.');
@@ -3777,6 +3788,7 @@ export class WorkflowService {
3777
3788
  acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
3778
3789
  const { identity } = this.#mutationContext(repository);
3779
3790
  const task = this.store.readTask(identity.projectId, taskId);
3791
+ assertNoPendingBaseSyncValidation(task);
3780
3792
  assertExpectedRevision(task, expectedRevision);
3781
3793
  if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
3782
3794
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for final acceptance.`);
@@ -3823,59 +3835,70 @@ export class WorkflowService {
3823
3835
  }
3824
3836
  syncTaskBase(repository, taskId, expectedRevision) {
3825
3837
  const { identity } = this.#mutationContext(repository);
3826
- const task = this.store.readTask(identity.projectId, taskId);
3838
+ let task = this.store.readTask(identity.projectId, taskId);
3827
3839
  assertExpectedRevision(task, expectedRevision);
3828
- if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
3829
- throw new WorkflowError('TRANSITION_BLOCKED', 'Base sync is available only for an accepted local Task.');
3830
- }
3831
- const currentBase = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
3832
- if (currentBase === task.baseCommit) {
3833
- throw new WorkflowError('TRANSITION_BLOCKED', 'Base branch has not advanced; synchronization is unnecessary.');
3834
- }
3835
- const synced = syncBaseIntoTask(identity.repositoryRoot, task.taskBranch, task.baseBranch);
3836
- const checks = runChecks(identity.repositoryRoot, [...new Set(task.steps.flatMap((step) => step.checks))]);
3837
- const failed = checks.filter((check) => !check.passed);
3838
- if (failed.length > 0) {
3839
- return this.store.writeTask({
3840
- ...task,
3841
- status: 'needs_fix',
3842
- baseCommit: synced.baseCommit,
3843
- systemCommits: [...task.systemCommits, synced.syncCommit],
3844
- review: emptyReview(),
3845
- resultHash: null,
3846
- blockReason: 'Checks failed after synchronizing the base branch.',
3847
- }, expectedRevision);
3840
+ if (task.pendingBaseSyncValidation !== undefined) {
3841
+ assertPendingBaseSyncValidationReady(task);
3848
3842
  }
3843
+ else {
3844
+ if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
3845
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Base sync is available only for an accepted local Task.');
3846
+ }
3847
+ const currentBase = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
3848
+ if (currentBase === task.baseCommit) {
3849
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Base branch has not advanced; synchronization is unnecessary.');
3850
+ }
3851
+ const taskHead = headCommit(identity.repositoryRoot);
3852
+ if (currentBaseSyncConflict(this.store, task, taskHead, currentBase)) {
3853
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Base sync awaits an external decision for this exact conflicting HEAD pair.', {
3854
+ taskId, taskHead, baseHead: currentBase, waitFor: 'The base owner resolves the conflict on the base branch without changing the reviewed Task head.',
3855
+ });
3856
+ }
3857
+ let synced;
3858
+ try {
3859
+ synced = syncBaseIntoTask(identity.repositoryRoot, task.taskBranch, task.baseBranch);
3860
+ }
3861
+ catch (error) {
3862
+ if (error instanceof WorkflowError && error.code === 'GIT_PRECONDITION_FAILED'
3863
+ && error.details.cleanAbortVerified === true && headCommit(identity.repositoryRoot) === taskHead
3864
+ && runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]) === currentBase) {
3865
+ recordBaseSyncConflict(this.store, task, taskHead, currentBase);
3866
+ }
3867
+ throw error;
3868
+ }
3869
+ task = this.store.writeTask({
3870
+ ...task, status: 'validating', baseCommit: synced.baseCommit,
3871
+ systemCommits: [...new Set([...task.systemCommits, synced.syncCommit])],
3872
+ evidenceHash: null, resultHash: null, review: emptyReview(),
3873
+ authorizations: task.authorizations.map(item => item.kind === 'final_acceptance' && item.decision === 'approved'
3874
+ ? { ...item, decision: 'superseded' } : item),
3875
+ pendingBaseSyncValidation: { baseCommit: synced.baseCommit, syncCommit: synced.syncCommit,
3876
+ headCommit: headCommit(identity.repositoryRoot), checksHash: taskSyncChecksHash(task) },
3877
+ blockReason: 'Base synchronized; checks must complete before review and acceptance.',
3878
+ }, task.revision);
3879
+ }
3880
+ assertPendingBaseSyncValidationReady(task);
3881
+ const checks = runChecks(identity.repositoryRoot, taskSyncChecks(task), this.checkOptions);
3882
+ // A check that changes the repository cannot certify the recorded merge snapshot.
3883
+ assertPendingBaseSyncValidationReady(task);
3884
+ const { pendingBaseSyncValidation: _pending, ...validatedTask } = task;
3885
+ const failed = checks.some(check => !check.passed);
3849
3886
  const root = this.store.taskRoot(task.projectId, task.id);
3850
3887
  const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
3851
- taskId: task.id,
3852
- headCommit: headCommit(identity.repositoryRoot),
3853
- generatedAt: this.now().toISOString(),
3854
- reason: 'base-sync',
3855
- checks,
3856
- steps: task.steps.map((step) => ({ id: step.id, evidence: step.evidence })),
3857
- systemCommits: [...task.systemCommits, synced.syncCommit],
3888
+ taskId: task.id, headCommit: headCommit(identity.repositoryRoot), generatedAt: this.now().toISOString(),
3889
+ reason: 'base-sync', checks, steps: task.steps.map(step => ({ id: step.id, evidence: step.evidence })),
3890
+ systemCommits: task.systemCommits,
3858
3891
  }, null, 2));
3859
- const authorizations = task.authorizations.map((authorization) => authorization.kind === 'final_acceptance' && authorization.decision === 'approved'
3860
- ? { ...authorization, decision: 'superseded' }
3861
- : authorization);
3862
- const saved = this.store.writeTask({
3863
- ...task,
3864
- status: 'validating',
3865
- baseCommit: synced.baseCommit,
3866
- systemCommits: [...task.systemCommits, synced.syncCommit],
3867
- evidenceHash: stagedEvidence.hash,
3868
- review: emptyReview(),
3869
- resultHash: null,
3870
- authorizations,
3871
- blockReason: 'Base changed; validation, review, and final acceptance must be repeated.',
3872
- }, expectedRevision);
3892
+ const saved = this.store.writeTask({ ...validatedTask, status: failed ? 'needs_fix' : 'validating', evidenceHash: stagedEvidence.hash,
3893
+ blockReason: failed ? 'Checks failed after synchronizing the base branch.'
3894
+ : 'Base changed; validation, review, and final acceptance must be repeated.' }, task.revision);
3873
3895
  this.store.publishArtifact(stagedEvidence);
3874
3896
  return saved;
3875
3897
  }
3876
3898
  mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
3877
3899
  const context = this.#mutationContext(repository);
3878
3900
  const task = this.store.readTask(context.identity.projectId, taskId);
3901
+ assertNoPendingBaseSyncValidation(task);
3879
3902
  assertExpectedRevision(task, expectedRevision);
3880
3903
  const lease = context.locks.heartbeat(taskId, writerToken);
3881
3904
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
@@ -4025,137 +4048,6 @@ export class WorkflowService {
4025
4048
  now: this.now(),
4026
4049
  }, actor, confirmationCode);
4027
4050
  }
4028
- pendingReviewUpdatePreflight(repository, taskId) {
4029
- const snapshot = this.observationSnapshot(repository);
4030
- const blockers = observationPreflightBlockers(snapshot.observation);
4031
- try {
4032
- pendingReviewRunnerFingerprint();
4033
- }
4034
- catch (error) {
4035
- blockers.push(error instanceof Error ? error.message : String(error));
4036
- }
4037
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4038
- let binding = null;
4039
- if (PACKAGE_VERSION === PENDING_REVIEW_SOURCE_VERSION)
4040
- blockers.push('The exact target runner must be newer than the supported source.');
4041
- if (!task)
4042
- blockers.push('Task not found.');
4043
- if (task) {
4044
- try {
4045
- binding = pendingReviewSourceBinding(snapshot.identity.repositoryRoot, this.store, task);
4046
- }
4047
- catch (error) {
4048
- blockers.push(error instanceof Error ? error.message : String(error));
4049
- }
4050
- const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4051
- if ([versions.declared, versions.locked, versions.installed, versions.currentBranch,
4052
- ...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PENDING_REVIEW_SOURCE_VERSION)) {
4053
- blockers.push('All source version surfaces must equal the exact supported beta.13.1.');
4054
- }
4055
- }
4056
- const otherRunningSteps = snapshot.tasks.flatMap((candidate) => candidate.steps.filter((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== binding?.stepId)));
4057
- if (otherRunningSteps.length)
4058
- blockers.push('No other Step may be running.');
4059
- const stale = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= this.now().getTime());
4060
- if (snapshot.leases.some((lease) => !stale.includes(lease)))
4061
- blockers.push('No live writer lease may exist.');
4062
- if (stale.some((lease) => lease.entityId !== taskId))
4063
- blockers.push('No unrelated stale writer lease may exist.');
4064
- const staleLeaseRepair = blockers.length === 0 && binding
4065
- ? stale.map((lease) => ({ action: 'locks repair', entityId: lease.entityId })) : [];
4066
- if (stale.length)
4067
- blockers.push('Repair the exact stale Task lease with the installed source locks repair, then rerun source preflight.');
4068
- return { readOnly: true, eligible: blockers.length === 0 && binding !== null, binding,
4069
- bindingHash: binding ? pendingReviewBindingHash(binding) : null, blockers, staleLeaseRepair };
4070
- }
4071
- pendingReviewDependencyPreflight(repository, taskId, source) {
4072
- const snapshot = this.observationSnapshot(repository);
4073
- const blockers = observationPreflightBlockers(snapshot.observation);
4074
- try {
4075
- pendingReviewRunnerFingerprint();
4076
- }
4077
- catch (error) {
4078
- blockers.push(error instanceof Error ? error.message : String(error));
4079
- }
4080
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4081
- const taskHead = headCommit(snapshot.identity.repositoryRoot);
4082
- let baseTransportCommit = null;
4083
- if (snapshot.leases.length)
4084
- blockers.push('No live or stale writer lease may exist.');
4085
- if (!task)
4086
- blockers.push('Task not found.');
4087
- if (task && existsSync(path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)))
4088
- blockers.push('A pending review update is already recorded.');
4089
- if (task) {
4090
- try {
4091
- // Receipt fields are claims, not authority. Reconstruct every source binding from
4092
- // canonical Task/sidecars and the exact parents of the candidate Git commits.
4093
- const parent = runGit(repository, ['rev-parse', 'HEAD^']);
4094
- baseTransportCommit = runGit(repository, ['rev-parse', task.baseBranch]);
4095
- const baseParent = runGit(repository, ['rev-parse', `${baseTransportCommit}^`]);
4096
- const reconstructed = pendingReviewSourceBinding(repository, this.store, task, parent, baseParent);
4097
- if (canonicalJsonStringify(reconstructed) !== canonicalJsonStringify(source)) {
4098
- throw new Error('Source receipt does not match reconstructed Task/revision/Plan/HEAD/pending bindings.');
4099
- }
4100
- assertPendingReviewTransport(repository, reconstructed, taskHead, baseTransportCommit);
4101
- if (snapshot.tasks.some((candidate) => candidate.steps.some((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== source.stepId)))) {
4102
- throw new Error('No other Step may be running.');
4103
- }
4104
- const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
4105
- if ([versions.runtime, versions.declared, versions.locked, versions.installed, versions.currentBranch,
4106
- ...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PACKAGE_VERSION)) {
4107
- throw new Error('All target version surfaces must equal the exact target runner.');
4108
- }
4109
- }
4110
- catch (error) {
4111
- blockers.push(error instanceof Error ? error.message : String(error));
4112
- }
4113
- }
4114
- return { readOnly: true, eligible: blockers.length === 0, blockers, taskHead, baseTransportCommit };
4115
- }
4116
- recoverPendingReviewDependency(repository, taskId, expectedRevision, source, actor, reason) {
4117
- if (!actor.trim() || !reason.trim())
4118
- throw new WorkflowError('INVALID_ARGUMENT', 'Actor and reason are required.');
4119
- const initial = this.pendingReviewDependencyPreflight(repository, taskId, source);
4120
- if (!initial.eligible)
4121
- throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight failed.', initial);
4122
- const context = this.#mutationContext(repository);
4123
- const task = this.store.readTask(context.identity.projectId, taskId);
4124
- assertExpectedRevision(task, expectedRevision);
4125
- const preflight = this.pendingReviewDependencyPreflight(repository, taskId, source);
4126
- if (!preflight.eligible || !preflight.baseTransportCommit) {
4127
- throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight changed.', preflight);
4128
- }
4129
- const now = this.now();
4130
- const nextTask = { ...task, revision: task.revision + 1, updatedAt: now.toISOString(),
4131
- systemCommits: [...task.systemCommits, preflight.taskHead],
4132
- dependencyProvenanceRecoveries: [...(task.dependencyProvenanceRecoveries ?? []), {
4133
- commitSha: preflight.taskHead, parentCommitSha: source.completionCommit,
4134
- packageName: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, files: ['package-lock.json', 'package.json'],
4135
- actor: actor.trim(), reason: reason.trim(), recordedAt: now.toISOString(),
4136
- }] };
4137
- const projectRoot = this.store.projectRoot(task.projectId);
4138
- const event = preparePendingReviewUpdateEvent({ source, baseTransportCommit: preflight.baseTransportCommit }, preflight.taskHead, now.toISOString());
4139
- applyProjectTransaction({ projectRoot, projectId: task.projectId,
4140
- transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
4141
- kind: 'pending-review-dependency-recovery', now,
4142
- targets: [{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')).split(path.sep).join('/'),
4143
- content: `${JSON.stringify(nextTask, null, 2)}\n` },
4144
- { path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)).split(path.sep).join('/'),
4145
- content: `${canonicalJsonStringify(event)}\n` }],
4146
- validate: () => {
4147
- const saved = this.store.readTask(task.projectId, task.id);
4148
- if (saved.revision !== nextTask.revision || !verifiedPendingReviewTransport(repository, saved, source.stepId, this.store.taskRoot(task.projectId, task.id))) {
4149
- throw new WorkflowError('STATE_CORRUPT', 'Pending review dependency recovery readback failed.');
4150
- }
4151
- const cycle = buildCurrentStrictStepReviewCycle(this.store, saved, source.stepId);
4152
- if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== source.pendingEventHash) {
4153
- throw new WorkflowError('STATE_CORRUPT', 'Pending review changed during dependency recovery.');
4154
- }
4155
- },
4156
- });
4157
- return this.store.readTask(task.projectId, task.id);
4158
- }
4159
4051
  updatePreflight(repository) {
4160
4052
  const snapshot = this.observationSnapshot(repository);
4161
4053
  const tasks = snapshot.tasks;
@@ -4237,6 +4129,69 @@ export class WorkflowService {
4237
4129
  observation: snapshot.observation,
4238
4130
  };
4239
4131
  }
4132
+ navigationUpdateBoundary(repository, taskId) {
4133
+ const snapshot = this.observationSnapshot(repository);
4134
+ const observation = snapshot.observation;
4135
+ const task = snapshot.tasks.find(item => item.id === taskId);
4136
+ if (!task || observation.kind !== 'assessment' || snapshot.leases.length
4137
+ || snapshot.tasks.some(item => item.steps.some(step => step.status === 'in_progress'))
4138
+ || observation.pendingProjectTransactions.length || observation.pendingMilestoneTransactions.length
4139
+ || observation.pendingTaskTransactions.length || observation.corruptTaskTransactions.length
4140
+ || observation.coreTaskOperations.length) {
4141
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Navigation update requires coherent state and no leases or pending operations.');
4142
+ }
4143
+ const otherBases = snapshot.milestones.filter(item => !['accepted', 'cancelled'].includes(item.status)
4144
+ && item.baseBranch !== task.baseBranch);
4145
+ if (otherBases.length)
4146
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Navigation update supports one active base branch.');
4147
+ return { snapshot, task, root: snapshot.identity.repositoryRoot };
4148
+ }
4149
+ navigationUpdateSourcePreflight(repository, taskId, tarball) {
4150
+ try {
4151
+ const { task, root } = this.navigationUpdateBoundary(repository, taskId);
4152
+ assertNavigationSourceInstalled(root, task);
4153
+ const receipt = navigationUpdateSource(root, this.store, task, verifyNavigationUpdateArtifact(tarball), task.updatedAt);
4154
+ return { eligible: true, blockers: [], receipt };
4155
+ }
4156
+ catch (error) {
4157
+ return { eligible: false, blockers: [error instanceof Error ? error.message : String(error)], receipt: null };
4158
+ }
4159
+ }
4160
+ navigationUpdateTransportPreflight(repository, taskId, tarball, receipt) {
4161
+ try {
4162
+ const { task, root } = this.navigationUpdateBoundary(repository, taskId);
4163
+ const proof = inspectNavigationTransport(root, this.store, task, receipt, verifyNavigationUpdateArtifact(tarball, path.join(root, 'node_modules/codex-workflow-v2')), this.now());
4164
+ return { eligible: true, blockers: [], proof,
4165
+ action: 'update navigation-recover', expectedRevision: task.revision, actor: receipt.actor };
4166
+ }
4167
+ catch (error) {
4168
+ return { eligible: false, blockers: [error instanceof Error ? error.message : String(error)], proof: null };
4169
+ }
4170
+ }
4171
+ recoverNavigationUpdate(repository, taskId, expectedRevision, tarball, receipt, actor, reason) {
4172
+ this.#mutationContext(repository);
4173
+ const { task, root } = this.navigationUpdateBoundary(repository, taskId);
4174
+ assertExpectedRevision(task, expectedRevision);
4175
+ if (!actor.trim() || actor !== receipt?.actor || !reason.trim()) {
4176
+ throw new WorkflowError('INVALID_ARGUMENT', 'Navigation update requires the source claimant and an explicit reason.');
4177
+ }
4178
+ const preflight = this.navigationUpdateTransportPreflight(repository, taskId, tarball, receipt);
4179
+ if (!preflight.eligible || !preflight.proof) {
4180
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Navigation transport preflight failed.', { blockers: preflight.blockers });
4181
+ }
4182
+ // Recompute the source/state/Git bindings immediately before the atomic optimistic state write.
4183
+ const boundary = this.navigationUpdateBoundary(repository, taskId);
4184
+ const proof = inspectNavigationTransport(root, this.store, boundary.task, receipt, verifyNavigationUpdateArtifact(tarball, path.join(root, 'node_modules/codex-workflow-v2')), this.now());
4185
+ const saved = this.store.writeTask({ ...boundary.task, status: 'awaiting_execution_authorization',
4186
+ systemCommits: [...boundary.task.systemCommits, ...proof.commits.map(item => item.commitSha)],
4187
+ dependencyProvenanceRecoveries: [...(boundary.task.dependencyProvenanceRecoveries ?? []),
4188
+ ...proof.commits.map(item => ({ ...item, packageName: PACKAGE_NAME,
4189
+ files: ['package-lock.json', 'package.json'],
4190
+ actor, reason: `${reason.trim()} [navigation-update ${receipt.artifact.sha256}]`, recordedAt: this.now().toISOString() }))],
4191
+ }, expectedRevision);
4192
+ validateTaskHistory(root, saved);
4193
+ return saved;
4194
+ }
4240
4195
  dependencyProvenanceRecoveryPreflight(repository, taskId) {
4241
4196
  const snapshot = this.observationSnapshot(repository);
4242
4197
  if (snapshot.observation.kind !== 'assessment') {
@@ -4247,325 +4202,33 @@ export class WorkflowService {
4247
4202
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4248
4203
  if (!task)
4249
4204
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4250
- const candidate = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, headCommit(snapshot.identity.repositoryRoot));
4251
- const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, candidate?.parentCommitSha);
4252
- const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
4253
- if (!preflight.rootCauseReplan)
4254
- return preflight;
4255
- let baseTip = null;
4256
- let baseDependency = null;
4257
- try {
4258
- baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
4259
- baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
4205
+ return this.inspectDependencyProvenanceCandidate(snapshot, task);
4206
+ }
4207
+ historicalStepProvenanceRecoveryPreflight(repository, taskId) {
4208
+ const snapshot = this.observationSnapshot(repository);
4209
+ if (snapshot.observation.kind !== 'assessment') {
4210
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
4211
+ observation: snapshot.observation,
4212
+ });
4260
4213
  }
4261
- catch {
4262
- // The blocker below keeps a missing or unreadable base reference fail closed.
4263
- }
4264
- const bridgeBlockers = [
4265
- ...(preflight.dependencyCommits.length !== 1
4266
- ? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
4267
- ...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
4268
- ? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
4269
- ...(preflight.parentCommitSha
4270
- && workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
4271
- ? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
4272
- ...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
4273
- ? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
4274
- ...(!baseDependency
4275
- || baseDependency.commitSha !== baseTip
4276
- || baseDependency.packageVersion !== PACKAGE_VERSION
4277
- || workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
4278
- !== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
4279
- ? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
4280
- ];
4281
- return bridgeBlockers.length === 0
4282
- ? preflight
4283
- : {
4284
- ...preflight,
4285
- eligible: false,
4286
- action: null,
4287
- commitSha: null,
4288
- parentCommitSha: null,
4289
- blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
4290
- };
4214
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4215
+ if (!task)
4216
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4217
+ return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
4291
4218
  }
4292
- correctiveCarryoverUpdatePreflight(repository, taskId, candidate = null) {
4219
+ historicalStepDependencyRecoveryPreflight(repository, taskId) {
4293
4220
  const snapshot = this.observationSnapshot(repository);
4221
+ if (snapshot.observation.kind !== 'assessment') {
4222
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
4223
+ observation: snapshot.observation,
4224
+ });
4225
+ }
4294
4226
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4295
4227
  if (!task)
4296
4228
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4297
- const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4298
- const sourceVersion = '2.0.0-beta.13.6';
4299
- const targetVersion = '2.0.0-beta.13.8';
4300
- const sourceSurfaces = {
4301
- declared: versions.declared,
4302
- locked: versions.locked,
4303
- installed: versions.installed,
4304
- currentBranch: versions.currentBranch,
4305
- baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4306
- };
4307
- const c1 = readTaskC1Posture(this.store, task);
4308
- if (c1.state !== 'claimed') {
4309
- return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
4310
- }
4311
- const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4312
- const now = this.now().getTime();
4313
- const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
4314
- const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4315
- const transactionCount = snapshot.observation.pendingProjectTransactions.length
4316
- + snapshot.observation.pendingMilestoneTransactions.length
4317
- + snapshot.observation.pendingTaskTransactions.length;
4318
- const authorizationBlockers = [];
4319
- if (assessment.eligible) {
4320
- try {
4321
- this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
4322
- }
4323
- catch (error) {
4324
- authorizationBlockers.push(error instanceof Error ? error.message : String(error));
4325
- }
4326
- }
4327
- const baseBlockers = [
4328
- ...assessment.blockers,
4329
- ...authorizationBlockers,
4330
- ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4331
- ...Object.entries(sourceSurfaces)
4332
- .filter(([, version]) => version !== sourceVersion)
4333
- .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4334
- ...observationPreflightBlockers(snapshot.observation),
4335
- ...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
4336
- ...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
4337
- ...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
4338
- ...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
4339
- ? ['The exact original Worker C1 claim must remain current.'] : []),
4340
- ...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
4341
- ...(staleLeases.some((lease) => lease.entityId !== task.id
4342
- || c1.state !== 'claimed' || lease.owner !== c1.claimant
4343
- || createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
4344
- ? ['No unrelated or differently owned stale writer lease may exist.'] : []),
4345
- ...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
4346
- ];
4347
- const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
4348
- ? staleLeases[0]
4349
- : null;
4350
- const blockers = [
4351
- ...baseBlockers,
4352
- ...(repairableStale
4353
- ? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
4354
- : []),
4355
- ];
4356
- const compatibility = assessment.compatibility;
4357
- return {
4358
- readOnly: true,
4359
- eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
4360
- action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
4361
- ? 'update corrective-carryover-transport'
4362
- : null,
4363
- projectId: task.projectId,
4364
- taskId: task.id,
4365
- taskRevision: task.revision,
4366
- stepId: compatibility?.stepId ?? null,
4367
- sourceVersion,
4368
- targetVersion,
4369
- sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
4370
- taskBranch: task.taskBranch,
4371
- baseBranch: task.baseBranch,
4372
- dirtyFiles: compatibility?.dirtyFiles ?? [],
4373
- dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
4374
- compatibilityHash: compatibility?.compatibilityHash ?? null,
4375
- c1Posture: c1.state === 'claimed' ? {
4376
- state: 'claimed',
4377
- claimant: c1.claimant,
4378
- claimEventId: compatibility?.c1ClaimEventId ?? null,
4379
- claimEventHash: compatibility?.c1ClaimEventHash ?? null,
4380
- } : null,
4381
- staleLeaseRepair: repairableStale ? [{
4382
- action: 'locks repair',
4383
- entityId: task.id,
4384
- owner: repairableStale.owner,
4385
- expiresAt: repairableStale.expiresAt,
4386
- }] : [],
4387
- requiredTransport: [
4388
- 'Commit only package.json and package-lock.json on the active Milestone base.',
4389
- 'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
4390
- 'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
4391
- ],
4392
- blockers,
4393
- };
4394
- }
4395
- consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
4396
- const repositoryRoot = snapshot.identity.repositoryRoot;
4397
- const sourceVersion = '2.0.0-beta.13.6';
4398
- const targetVersion = '2.0.0-beta.13.8';
4399
- const c1 = readTaskC1Posture(this.store, task);
4400
- const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
4401
- const currentHead = headCommit(repositoryRoot);
4402
- const dirtyFiles = changedFiles(repositoryRoot).sort();
4403
- const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
4404
- const navigation = this.next(repositoryRoot, task.id);
4405
- const action = String(navigation.action);
4406
- const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
4407
- const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
4408
- const terminal = readRemediationEvents(this.store, task)
4409
- .filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
4410
- const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
4411
- const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
4412
- const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
4413
- const missingClaimOnly = assessment.blockers.length === 1
4414
- && assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
4415
- const rollingWave = navigation.rollingWave;
4416
- const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
4417
- && rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
4418
- || (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
4419
- || (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
4420
- && c1.state === 'pending' && c1.targetActor === yielded?.actor);
4421
- const blockers = [
4422
- ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4423
- ...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
4424
- .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4425
- ...observationPreflightBlockers(snapshot.observation),
4426
- ...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
4427
- ...(snapshot.observation.pendingProjectTransactions.length
4428
- + snapshot.observation.pendingMilestoneTransactions.length
4429
- + snapshot.observation.pendingTaskTransactions.length
4430
- + snapshot.observation.corruptTaskTransactions.length
4431
- + snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
4432
- ...(!recorded || recorded.stepId === stepId
4433
- || task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
4434
- ? ['A different, verified completed Step must own the historical carryover binding.'] : []),
4435
- ...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
4436
- ...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
4437
- || task.workspaceOwner !== 'local'
4438
- || currentBranch(repositoryRoot) !== task.taskBranch
4439
- || task.steps.some((candidate) => candidate.status === 'in_progress')
4440
- ? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
4441
- ...(!step || dirtyFiles.length === 0
4442
- || dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
4443
- ? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
4444
- ...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
4445
- || terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
4446
- ? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
4447
- ...(!yielded || !terminal || yielded.planHash === null
4448
- || !Number.isFinite(Date.parse(yielded.recordedAt))
4449
- || Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
4450
- ? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
4451
- ];
4452
- let candidateBinding = null;
4453
- if (action === 'task plan-set') {
4454
- if (!candidate) {
4455
- blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
4456
- }
4457
- else {
4458
- try {
4459
- validatePlan(candidate.plan);
4460
- const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
4461
- if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
4462
- throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
4463
- }
4464
- const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
4465
- if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
4466
- throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
4467
- }
4468
- if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
4469
- throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
4470
- }
4471
- if (candidate.plan.objective !== task.planObjective
4472
- || canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
4473
- || canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
4474
- throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
4475
- }
4476
- const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
4477
- this.validateKnowledgeTargets(map, candidate.plan);
4478
- const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
4479
- if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
4480
- === normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
4481
- throw new Error('Candidate must change the failed Plan semantics before source recovery.');
4482
- }
4483
- if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
4484
- throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
4485
- }
4486
- const auditTask = {
4487
- ...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
4488
- knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
4489
- knowledgeMapHash: candidate.plan.knowledgeMapHash,
4490
- steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
4491
- ...candidateStep, status: 'planned', evidence: null, failurePause: null,
4492
- }),
4493
- };
4494
- validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
4495
- candidateBinding = {
4496
- candidatePlanHash: prepared.planHash,
4497
- candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
4498
- riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
4499
- };
4500
- }
4501
- catch (error) {
4502
- blockers.push(error instanceof Error ? error.message : String(error));
4503
- }
4504
- }
4505
- }
4506
- try {
4507
- validateTaskHistory(repositoryRoot, task);
4508
- this.assertTaskKnowledgeBinding(repositoryRoot, task);
4509
- const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
4510
- if (!sourceAudit || !yielded?.planHash || !task.planHash
4511
- || normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
4512
- !== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
4513
- throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
4514
- }
4515
- }
4516
- catch (error) {
4517
- blockers.push(error instanceof Error ? error.message : String(error));
4518
- }
4519
- const binding = {
4520
- taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
4521
- planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
4522
- sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
4523
- completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
4524
- terminalRemediationEventHash: terminal?.eventHash ?? null,
4525
- correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
4526
- c1Posture: c1.state,
4527
- candidateBinding,
4528
- };
4529
- return {
4530
- readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
4531
- action: null, transportAllowed: false,
4532
- ...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
4533
- compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
4534
- sourceRecovery: blockers.length === 0 ? {
4535
- eligible: true, action,
4536
- ...(action === 'task plan-set' ? { ...candidateBinding } : {}),
4537
- ...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
4538
- ...(action === 'task claim' ? { actor: yielded.actor } : {}),
4539
- instruction: 'Execute only this exact action with the installed source package; run source status then next and repeat this read-only preflight. Ordinary Plan Risk Audit and execution authorization retain their normal gates.',
4540
- } : null,
4541
- blockers,
4542
- };
4543
- }
4544
- historicalStepProvenanceRecoveryPreflight(repository, taskId) {
4545
- const snapshot = this.observationSnapshot(repository);
4546
- if (snapshot.observation.kind !== 'assessment') {
4547
- throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
4548
- observation: snapshot.observation,
4549
- });
4550
- }
4551
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4552
- if (!task)
4553
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4554
- return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
4555
- }
4556
- historicalStepDependencyRecoveryPreflight(repository, taskId) {
4557
- const snapshot = this.observationSnapshot(repository);
4558
- if (snapshot.observation.kind !== 'assessment') {
4559
- throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
4560
- observation: snapshot.observation,
4561
- });
4562
- }
4563
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4564
- if (!task)
4565
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4566
- let candidateParent = null;
4567
- try {
4568
- candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
4229
+ let candidateParent = null;
4230
+ try {
4231
+ candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
4569
4232
  }
4570
4233
  catch {
4571
4234
  candidateParent = null;
@@ -4610,7 +4273,7 @@ export class WorkflowService {
4610
4273
  ])],
4611
4274
  };
4612
4275
  }
4613
- return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, {
4276
+ return this.inspectDependencyProvenanceCandidate(snapshot, task, null, {
4614
4277
  activeStepId: assessment.activeStepId,
4615
4278
  planHash: task.planHash,
4616
4279
  dirtyFiles: assessment.dirtyFiles,
@@ -4709,289 +4372,15 @@ export class WorkflowService {
4709
4372
  invalidatedStepIds: assessment.invalidatedSteps.map((step) => step.stepId),
4710
4373
  });
4711
4374
  }
4712
- downstreamProofReplanUpdatePreflight(repository, taskId) {
4713
- const snapshot = this.observationSnapshot(repository);
4714
- if (snapshot.observation.kind !== 'assessment') {
4715
- throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan update preflight requires a coherent observation boundary.', {
4716
- observation: snapshot.observation,
4717
- });
4718
- }
4719
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4720
- if (!task)
4721
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4722
- const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
4723
- const assessment = assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
4724
- const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4725
- const sourceVersion = '2.0.0-beta.12.6';
4726
- const versionSurfaces = {
4727
- declared: versions.declared,
4728
- locked: versions.locked,
4729
- installed: versions.installed,
4730
- currentBranch: versions.currentBranch,
4731
- baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4732
- };
4733
- const blockers = [
4734
- ...assessment.blockers,
4735
- ...(String(PACKAGE_VERSION) === sourceVersion ? ['Recovery runner must be newer than the beta.12.6 source package.'] : []),
4736
- ...Object.entries(versionSurfaces)
4737
- .filter(([, version]) => version !== sourceVersion)
4738
- .map(([surface]) => `${surface} Workflow version must equal the stranded source ${sourceVersion}.`),
4739
- ...(snapshot.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
4740
- ...(snapshot.observation.pendingProjectTransactions.length > 0
4741
- || snapshot.observation.pendingMilestoneTransactions.length > 0
4742
- || snapshot.observation.pendingTaskTransactions.length > 0
4743
- ? ['No pending Workflow transaction may exist.'] : []),
4744
- ...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
4745
- ...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
4746
- ];
4747
- return {
4748
- readOnly: true,
4749
- eligible: assessment.applicable && assessment.eligible && blockers.length === 0,
4750
- action: assessment.applicable && assessment.eligible && blockers.length === 0
4751
- ? 'update downstream-proof-replan-transport'
4752
- : null,
4753
- projectId: task.projectId,
4754
- taskId: task.id,
4755
- taskRevision: task.revision,
4756
- sourceVersion,
4757
- targetVersion: PACKAGE_VERSION,
4758
- sourceHeadCommit: assessment.invalidation?.preservedHeadCommit ?? null,
4759
- taskBranch: task.taskBranch,
4760
- baseBranch: task.baseBranch,
4761
- dirtyFiles: assessment.compatibility?.dirtyFiles ?? [],
4762
- dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
4763
- requiredTransport: [
4764
- 'Commit only package.json and package-lock.json on the active Milestone base.',
4765
- 'Commit only the same dependency files on the Task branch while preserving all product bytes.',
4766
- 'Install the exact target package, then follow fresh next to dependency registration.',
4767
- ],
4768
- blockers,
4769
- };
4770
- }
4771
- planIntegrityUpdatePreflight(repository, taskId) {
4772
- const snapshot = this.observationSnapshot(repository);
4773
- if (snapshot.observation.kind !== 'assessment') {
4774
- throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
4775
- observation: snapshot.observation,
4776
- });
4777
- }
4778
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4779
- if (!task)
4780
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4781
- const assessments = task.steps
4782
- .filter((step) => step.status === 'failed')
4783
- .map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
4784
- .filter((assessment) => assessment.applicable);
4785
- const assessment = assessments.length === 1 ? assessments[0] : null;
4786
- const adoptedCompatibility = assessment
4787
- ? null
4788
- : this.postRebindCheckSupportCompatibility(snapshot, task);
4789
- const dirtyCarryoverAssessment = assessment || adoptedCompatibility
4790
- ? null
4791
- : this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
4792
- const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
4793
- && dirtyCarryoverAssessment.compatibility
4794
- ? {
4795
- ...dirtyCarryoverAssessment.compatibility,
4796
- planHash: task.planHash,
4797
- mode: 'post-rebind-dirty-carryover',
4798
- }
4799
- : null;
4800
- const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4801
- const versionSurfaces = {
4802
- declared: versions.declared,
4803
- locked: versions.locked,
4804
- installed: versions.installed,
4805
- currentBranch: versions.currentBranch,
4806
- baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4807
- };
4808
- const sourceVersion = versionSurfaces.declared;
4809
- const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
4810
- const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
4811
- const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
4812
- && sourceBetaVersion.patch !== null
4813
- && sourceBetaVersion.patch >= 11;
4814
- const targetIsNewer = sourceBetaVersion !== null
4815
- && targetBetaVersion !== null
4816
- && (targetBetaVersion.train > sourceBetaVersion.train
4817
- || (targetBetaVersion.train === sourceBetaVersion.train
4818
- && targetBetaVersion.patch !== null
4819
- && sourceBetaVersion.patch !== null
4820
- && targetBetaVersion.patch > sourceBetaVersion.patch));
4821
- const blockers = [
4822
- ...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
4823
- ...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
4824
- ? []
4825
- : (dirtyCarryoverAssessment?.blockers.length
4826
- ? dirtyCarryoverAssessment.blockers
4827
- : ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
4828
- ...(!sourceIsEligibleBeta12 || !targetIsNewer
4829
- ? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
4830
- : []),
4831
- ...Object.entries(versionSurfaces)
4832
- .filter(([, version]) => version !== sourceVersion)
4833
- .map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
4834
- ...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
4835
- ...observationPreflightBlockers(snapshot.observation),
4836
- ];
4837
- return {
4838
- readOnly: true,
4839
- eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
4840
- action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
4841
- ? 'update plan-integrity-transport'
4842
- : null,
4843
- projectId: task.projectId,
4844
- taskId: task.id,
4845
- taskRevision: task.revision,
4846
- stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
4847
- sourceVersion,
4848
- targetVersion: PACKAGE_VERSION,
4849
- taskBranch: task.taskBranch,
4850
- baseBranch: task.baseBranch,
4851
- dirtyFiles: assessment?.evidence?.changedFiles
4852
- ?? adoptedCompatibility?.dirtyFiles
4853
- ?? dirtyCarryoverCompatibility?.dirtyFiles
4854
- ?? [],
4855
- conflictHash: assessment?.evidence?.conflictHash
4856
- ?? adoptedCompatibility?.conflictHash
4857
- ?? dirtyCarryoverCompatibility?.conflictHash
4858
- ?? null,
4859
- compatibilityMode: adoptedCompatibility?.mode
4860
- ?? dirtyCarryoverCompatibility?.mode
4861
- ?? 'failed-step-conflict',
4862
- requiredTransport: [
4863
- 'Commit only package.json and package-lock.json on the active Milestone base.',
4864
- 'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
4865
- 'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
4866
- ],
4867
- blockers,
4868
- };
4869
- }
4870
- planIntegrityDependencyRecoveryPreflight(repository, taskId) {
4871
- const snapshot = this.observationSnapshot(repository);
4872
- if (snapshot.observation.kind !== 'assessment') {
4873
- throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
4874
- observation: snapshot.observation,
4875
- });
4876
- }
4877
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4878
- if (!task)
4879
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4880
- const assessmentBlockers = [];
4881
- const assessments = task.steps
4882
- .filter((step) => step.status === 'failed')
4883
- .flatMap((step) => {
4884
- try {
4885
- return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
4886
- }
4887
- catch (error) {
4888
- if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
4889
- assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
4890
- return [];
4891
- }
4892
- throw error;
4893
- }
4894
- })
4895
- .filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
4896
- const assessment = assessments.length === 1 ? assessments[0] : null;
4897
- const adoptedCompatibility = assessment?.evidence
4898
- ? null
4899
- : this.postRebindCheckSupportCompatibility(snapshot, task);
4900
- const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
4901
- ? null
4902
- : this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
4903
- const carriedCompatibility = !assessment?.evidence && task.planHash
4904
- ? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
4905
- : null;
4906
- if ((!assessment?.evidence
4907
- && !carriedCompatibility
4908
- && !adoptedCompatibility
4909
- && !dirtyCarryoverCompatibility) || !task.planHash) {
4910
- const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
4911
- return {
4912
- ...ordinary,
4913
- eligible: false,
4914
- action: null,
4915
- planIntegrity: null,
4916
- blockers: [...new Set([
4917
- ...ordinary.blockers,
4918
- ...assessmentBlockers,
4919
- 'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
4920
- ])],
4921
- };
4922
- }
4923
- const compatibility = carriedCompatibility
4924
- ?? adoptedCompatibility
4925
- ?? dirtyCarryoverCompatibility
4926
- ?? {
4927
- stepId: assessment.stepId,
4928
- planHash: task.planHash,
4929
- conflictHash: assessment.evidence.conflictHash,
4930
- dirtyFiles: [...assessment.evidence.changedFiles],
4931
- dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
4932
- remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
4933
- ? [assessment.evidence.remediationEventId]
4934
- : assessment.evidence.remediationEvents.map((event) => event.eventId),
4935
- };
4936
- return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
4937
- }
4938
- downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
4939
- const snapshot = this.observationSnapshot(repository);
4940
- if (snapshot.observation.kind !== 'assessment') {
4941
- throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan dependency recovery requires a coherent observation boundary.', {
4942
- observation: snapshot.observation,
4943
- });
4944
- }
4945
- const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4946
- if (!task)
4947
- throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4948
- let candidateParent = null;
4949
- try {
4950
- candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
4951
- }
4952
- catch {
4953
- candidateParent = null;
4954
- }
4955
- const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
4956
- const assessment = candidateParent
4957
- ? assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, {
4958
- historyBoundary: 'HEAD^',
4959
- preservedHeadCommit: candidateParent,
4960
- currentPlanRiskAuditExists,
4961
- })
4962
- : null;
4963
- if (!assessment?.applicable || !assessment.eligible || !assessment.compatibility) {
4964
- const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
4965
- return {
4966
- ...ordinary,
4967
- eligible: false,
4968
- action: null,
4969
- activeDownstreamProof: null,
4970
- downstreamProofReplan: null,
4971
- blockers: [...new Set([
4972
- ...ordinary.blockers,
4973
- ...(assessment?.blockers ?? []),
4974
- 'The Task must preserve the exact beta.12.6 downstream-proof invalidation and accidental knowledge-rebind boundary.',
4975
- ])],
4976
- };
4977
- }
4978
- return this.inspectDependencyProvenanceCandidate(snapshot, task, null, assessment.compatibility);
4979
- }
4980
4375
  recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4981
4376
  return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, false);
4982
4377
  }
4983
4378
  recoverActiveDownstreamProofDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4984
4379
  return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, true);
4985
4380
  }
4986
- recoverDownstreamProofReplanDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4987
- return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'downstream-replan');
4988
- }
4989
4381
  recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4990
4382
  return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
4991
4383
  }
4992
- recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4993
- return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
4994
- }
4995
4384
  recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
4996
4385
  const normalizedActor = actor.trim();
4997
4386
  const normalizedReason = reason.trim();
@@ -5004,15 +4393,9 @@ export class WorkflowService {
5004
4393
  const currentHead = headCommit(context.identity.repositoryRoot);
5005
4394
  const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead
5006
4395
  && record.packageVersion === PACKAGE_VERSION
5007
- && (recoveryMode === 'downstream-replan'
5008
- ? Boolean(record.downstreamProofReplan)
5009
- : recoveryMode === 'historical-step'
5010
- ? Boolean(record.historicalStepProvenance)
5011
- : recoveryMode === 'plan-integrity'
5012
- ? Boolean(record.planIntegrity)
5013
- : recoveryMode === false && record.rootCauseReplan
5014
- ? true
5015
- : Boolean(record.activeDownstreamProof) === recoveryMode));
4396
+ && (recoveryMode === 'historical-step'
4397
+ ? Boolean(record.historicalStepProvenance)
4398
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
5016
4399
  if (prior && current.systemCommits.includes(currentHead))
5017
4400
  return current;
5018
4401
  assertExpectedRevision(current, expectedRevision);
@@ -5020,34 +4403,17 @@ export class WorkflowService {
5020
4403
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
5021
4404
  if (!task)
5022
4405
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
5023
- const preflight = recoveryMode === 'downstream-replan'
5024
- ? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
5025
- : recoveryMode === 'historical-step'
5026
- ? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
5027
- : recoveryMode === 'plan-integrity'
5028
- ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
5029
- : recoveryMode
5030
- ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
5031
- : this.dependencyProvenanceRecoveryPreflight(repository, taskId);
4406
+ const preflight = recoveryMode === 'historical-step'
4407
+ ? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
4408
+ : recoveryMode
4409
+ ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4410
+ : this.dependencyProvenanceRecoveryPreflight(repository, taskId);
5032
4411
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
5033
4412
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
5034
4413
  taskId,
5035
4414
  blockers: preflight.blockers,
5036
4415
  });
5037
4416
  }
5038
- if (preflight.rootCauseReplan) {
5039
- const posture = readTaskC1Posture(this.store, task);
5040
- if (posture.state !== 'claimed'
5041
- || posture.claimant !== preflight.rootCauseReplan.claimant
5042
- || normalizedActor !== posture.claimant) {
5043
- throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
5044
- taskId,
5045
- actor: normalizedActor,
5046
- requiredActor: preflight.rootCauseReplan.claimant,
5047
- posture: posture.state,
5048
- });
5049
- }
5050
- }
5051
4417
  const now = this.now();
5052
4418
  const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
5053
4419
  const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
@@ -5064,18 +4430,9 @@ export class WorkflowService {
5064
4430
  ...(isTerminalCommit && preflight.activeDownstreamProof
5065
4431
  ? { activeDownstreamProof: preflight.activeDownstreamProof }
5066
4432
  : {}),
5067
- ...(isTerminalCommit && preflight.downstreamProofReplan
5068
- ? { downstreamProofReplan: preflight.downstreamProofReplan }
5069
- : {}),
5070
4433
  ...(isTerminalCommit && preflight.historicalStepProvenance
5071
4434
  ? { historicalStepProvenance: preflight.historicalStepProvenance }
5072
4435
  : {}),
5073
- ...(isTerminalCommit && preflight.planIntegrity
5074
- ? { planIntegrity: preflight.planIntegrity }
5075
- : {}),
5076
- ...(isTerminalCommit && preflight.rootCauseReplan
5077
- ? { rootCauseReplan: preflight.rootCauseReplan }
5078
- : {}),
5079
4436
  };
5080
4437
  });
5081
4438
  const nextTask = {
@@ -5091,62 +4448,7 @@ export class WorkflowService {
5091
4448
  ...recoveredRecords,
5092
4449
  ],
5093
4450
  };
5094
- if (recoveryMode !== 'plan-integrity' || !preflight.planIntegrity) {
5095
- return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
5096
- }
5097
- const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
5098
- const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
5099
- const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
5100
- let refreshedKnowledgeMap = null;
5101
- if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
5102
- const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
5103
- if (!knowledgeRefresh.eligible) {
5104
- throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
5105
- taskId,
5106
- dependencyKnowledgeRefresh: knowledgeRefresh,
5107
- });
5108
- }
5109
- const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
5110
- const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
5111
- if (!approved.approval) {
5112
- throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
5113
- }
5114
- refreshedKnowledgeMap = {
5115
- ...approved,
5116
- revision: currentKnowledgeMap.revision + 2,
5117
- createdAt: currentKnowledgeMap.createdAt,
5118
- updatedAt: now.toISOString(),
5119
- approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
5120
- };
5121
- }
5122
- const projectRoot = this.store.projectRoot(task.projectId);
5123
- const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
5124
- applyProjectTransaction({
5125
- projectRoot,
5126
- projectId: task.projectId,
5127
- transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
5128
- kind: 'plan-integrity-dependency-recovery',
5129
- now,
5130
- targets: [
5131
- ...(refreshedKnowledgeMap
5132
- ? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
5133
- : []),
5134
- {
5135
- path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
5136
- content: `${JSON.stringify(nextTask, null, 2)}\n`,
5137
- },
5138
- ],
5139
- validate: () => {
5140
- const saved = this.store.readTask(task.projectId, task.id);
5141
- if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
5142
- throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
5143
- }
5144
- if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
5145
- throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
5146
- }
5147
- },
5148
- });
5149
- return this.store.readTask(task.projectId, task.id);
4451
+ return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
5150
4452
  }
5151
4453
  milestoneProgress(repository, milestoneId) {
5152
4454
  const snapshot = this.observationSnapshot(repository);
@@ -5214,109 +4516,6 @@ export class WorkflowService {
5214
4516
  tasks,
5215
4517
  };
5216
4518
  }
5217
- alpha6StrictReviewRescuePreflight(repository) {
5218
- const context = this.#mutationContext(repository);
5219
- const dependency = inspectLocalDependency(context.identity.repositoryRoot);
5220
- const tasks = context.store.listTasks(context.identity.projectId);
5221
- const leases = context.locks.list();
5222
- const now = this.now().getTime();
5223
- const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
5224
- const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
5225
- const dirty = changedFiles(context.identity.repositoryRoot);
5226
- const currentHead = headCommit(context.identity.repositoryRoot);
5227
- const pendingCandidates = [];
5228
- const malformedCandidates = [];
5229
- for (const task of tasks) {
5230
- if (task.status !== 'in_progress')
5231
- continue;
5232
- const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
5233
- if (!planRiskAudit)
5234
- continue;
5235
- for (const step of task.steps) {
5236
- if (step.status !== 'in_progress' || !step.evidence)
5237
- continue;
5238
- if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
5239
- continue;
5240
- try {
5241
- const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
5242
- if (cycle.resolution !== 'pending')
5243
- continue;
5244
- validateTaskHistory(context.identity.repositoryRoot, task);
5245
- let knowledgeBindingStale = false;
5246
- try {
5247
- const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
5248
- const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
5249
- knowledgeBindingStale = knowledgeMap.status !== 'active'
5250
- || task.knowledgeMapRevision !== knowledgeMap.revision
5251
- || task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
5252
- }
5253
- catch {
5254
- knowledgeBindingStale = true;
5255
- }
5256
- pendingCandidates.push({
5257
- taskId: task.id,
5258
- taskRevision: task.revision,
5259
- stepId: step.id,
5260
- completionCommit: step.evidence.commitSha,
5261
- currentHead,
5262
- knowledgeBindingStale,
5263
- });
5264
- }
5265
- catch (error) {
5266
- malformedCandidates.push(error instanceof Error ? error.message : String(error));
5267
- }
5268
- }
5269
- }
5270
- const candidate = pendingCandidates.length === 1 ? pendingCandidates[0] : null;
5271
- const candidateLeaseIds = new Set(candidate ? [candidate.taskId] : []);
5272
- const blockers = [
5273
- ...(dependency.declared !== '2.0.0-alpha.6'
5274
- ? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
5275
- : []),
5276
- ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
5277
- ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
5278
- ...(pendingCandidates.length !== 1
5279
- ? [`Expected exactly one pending alpha.6 strict review; found ${pendingCandidates.length}.`]
5280
- : []),
5281
- ...(candidate && candidate.currentHead !== candidate.completionCommit
5282
- ? ['HEAD does not match the pending strict review completion commit.']
5283
- : []),
5284
- ...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
5285
- ? ['A stale writer lease exists outside the rescue Task.']
5286
- : []),
5287
- ...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
5288
- ];
5289
- const eligible = blockers.length === 0 && candidate !== null;
5290
- const requiredActions = eligible
5291
- ? [
5292
- ...staleLeases.map((lease) => ({
5293
- action: 'locks repair',
5294
- entityId: lease.entityId,
5295
- })),
5296
- {
5297
- action: 'task step-review',
5298
- taskId: candidate.taskId,
5299
- stepId: candidate.stepId,
5300
- expectedRevision: candidate.taskRevision,
5301
- },
5302
- ]
5303
- : [];
5304
- return {
5305
- eligible,
5306
- mutationFree: true,
5307
- runnerPackageVersion: PACKAGE_VERSION,
5308
- requiredProjectPackageVersion: '2.0.0-alpha.6',
5309
- declaredProjectPackageVersion: dependency.declared,
5310
- projectId: context.identity.projectId,
5311
- repositoryRoot: context.identity.repositoryRoot,
5312
- clean: dirty.length === 0,
5313
- activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
5314
- staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
5315
- candidate,
5316
- blockers,
5317
- requiredActions,
5318
- };
5319
- }
5320
4519
  scanKnowledgeMap(repository) {
5321
4520
  const { identity } = this.#mutationContext(repository);
5322
4521
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
@@ -5383,10 +4582,13 @@ export class WorkflowService {
5383
4582
  return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
5384
4583
  }
5385
4584
  graphStatus(repository, graphKind, mode = 'moderate') {
5386
- const { identity } = this.#mutationContext(repository);
4585
+ // Graph diagnostics must not register a virgin project or implicitly recover journals.
4586
+ // Surface the same structural boundary as status/next without loading or mutating Tasks.
4587
+ const identity = resolveRepositoryIdentity(repository);
4588
+ const { observation } = inspectWorkflowObservationBoundary({ store: this.store, identity, now: this.now() });
5387
4589
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
5388
4590
  const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
5389
- return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
4591
+ return { request, binding: binding ? inspectGraphBinding(binding, request) : null, observation };
5390
4592
  }
5391
4593
  bindGraph(repository, expectedRevision, request, evidence) {
5392
4594
  const { identity } = this.#mutationContext(repository);
@@ -5415,10 +4617,25 @@ export class WorkflowService {
5415
4617
  return explainChange(input, repository, effectiveTask, navigation);
5416
4618
  }
5417
4619
  next(repository, taskId = null) {
5418
- return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
4620
+ return withWorkflowBlockerRoute(withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository)))), PACKAGE_VERSION);
5419
4621
  }
5420
4622
  nextFromStatus(repository, taskId, status) {
5421
4623
  if (status.observation.kind === 'blocked') {
4624
+ // Observation stays write-free. Only an absent or proven virgin registration
4625
+ // may resume initialization; retained or unknown state remains blocked.
4626
+ if (taskId === null && status.observation.missingRegistration
4627
+ && isVirginRegistrationRoot(this.store.projectRoot(status.projectId), this.store.root, resolveRepositoryIdentity(repository), PACKAGE_VERSION)
4628
+ && status.observation.reasonCodes.every(code => code === 'PROJECT_REGISTRATION_MISSING' || code === 'ADOPTION_MISSING')) {
4629
+ return {
4630
+ scope: 'project', projectId: status.projectId,
4631
+ status: 'initialization-required', action: 'project-memory scan',
4632
+ observation: status.observation,
4633
+ navigation: {
4634
+ kind: 'executable', command: 'project-memory scan', cataloged: false,
4635
+ bindings: { projectId: status.projectId },
4636
+ },
4637
+ };
4638
+ }
5422
4639
  return {
5423
4640
  scope: 'project',
5424
4641
  projectId: status.projectId,
@@ -5426,7 +4643,16 @@ export class WorkflowService {
5426
4643
  action: 'blocked',
5427
4644
  observation: status.observation,
5428
4645
  navigation: {
5429
- kind: 'blocked',
4646
+ kind: status.observation.pendingProjectTransactions.some(transaction => transaction.ownerActive)
4647
+ || status.observation.reasonCodes.includes('TASK_OPERATION_IN_PROGRESS') ? 'wait' : 'blocked',
4648
+ ...(status.observation.pendingProjectTransactions.some(transaction => transaction.ownerActive) ? {
4649
+ reason: 'project-transaction-in-progress',
4650
+ waitFor: 'The current transaction owner must finish or exit before recovery; obtain fresh status and next afterward.',
4651
+ } : status.observation.reasonCodes.includes('TASK_OPERATION_IN_PROGRESS') ? {
4652
+ reason: 'core-operation-in-progress',
4653
+ waitFor: 'The active Core operation must finish or its lease expire; obtain fresh status and next afterward.',
4654
+ operations: status.observation.coreTaskOperations,
4655
+ } : {}),
5430
4656
  reasonCodes: status.observation.reasonCodes,
5431
4657
  availableRepairTransitionId: null,
5432
4658
  },
@@ -5563,6 +4789,46 @@ export class WorkflowService {
5563
4789
  const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
5564
4790
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
5565
4791
  const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
4792
+ const incompleteScope = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status)
4793
+ && (taskId === null || tasksById.get(taskId)?.milestoneId === milestone.id)
4794
+ && !assessMilestoneInitialAssembly({ store: this.store, projectId: status.projectId, milestone,
4795
+ listTasks: () => status.tasks }).eligible
4796
+ && assessMilestoneScopeCompletion(milestone, status.tasks).length > 0);
4797
+ if (incompleteScope) {
4798
+ const lease = this.inspectWriterLease(repositoryRoot, incompleteScope.id);
4799
+ if (lease)
4800
+ return withAdoption({ scope: 'milestone', id: incompleteScope.id, entityId: incompleteScope.id,
4801
+ revision: incompleteScope.revision, action: lease.stale ? 'locks repair' : 'blocked', lease,
4802
+ navigation: { kind: lease.stale ? 'repair' : 'wait', reason: 'milestone-authority-lease' } });
4803
+ if (incompleteScope.status === 'awaiting_final_acceptance')
4804
+ return withAdoption({
4805
+ scope: 'milestone', id: incompleteScope.id, revision: incompleteScope.revision, action: 'blocked',
4806
+ navigation: { kind: 'blocked', reason: 'retained-final-scope-gap' },
4807
+ reason: 'Retained final acceptance state contains unclassified linked Tasks; no safe scope transition is available.',
4808
+ scopeCompletion: { orphanTaskIds: assessMilestoneScopeCompletion(incompleteScope, status.tasks), executionAvailable: false },
4809
+ });
4810
+ const event = readMilestoneAutonomyEvents(this.store, status.projectId, incompleteScope.id).at(-1);
4811
+ let autonomous = null;
4812
+ if (event) {
4813
+ try {
4814
+ const authority = requireActiveMilestoneAutonomy({ store: this.store, projectId: status.projectId,
4815
+ milestone: incompleteScope, actor: event.delegate, now: this.now() });
4816
+ autonomous = { actor: event.delegate, grantId: authority.grant.id };
4817
+ }
4818
+ catch (error) {
4819
+ if (!(error instanceof WorkflowError) || !['TRANSITION_BLOCKED', 'NOT_FOUND'].includes(error.code))
4820
+ throw error;
4821
+ }
4822
+ }
4823
+ return withAdoption({ scope: 'milestone', id: incompleteScope.id, status: incompleteScope.status,
4824
+ revision: incompleteScope.revision, action: autonomous ? 'milestone autonomy-evolve' : 'milestone scope-change-prepare',
4825
+ scopeCompletion: { orphanTaskIds: assessMilestoneScopeCompletion(incompleteScope, status.tasks),
4826
+ requiresCandidatePlan: true, executionAvailable: false, preservesExistingScopeAndDependencies: true },
4827
+ ...(autonomous ? { autonomy: autonomous } : { scopeChangeBoundary: { state: 'human-decision-required',
4828
+ requiresCandidatePlan: true, prepareAction: 'milestone scope-change-prepare', applyAction: 'milestone scope-change-apply',
4829
+ requiresExplicitHumanConfirmation: true, confirmationCodeSource: 'scope-change-prepare-result',
4830
+ afterPreparation: 'await-human-confirmation-before-apply', applyRequiresSamePlanAndActor: true } }) });
4831
+ }
5566
4832
  const requestedTask = taskId === null
5567
4833
  ? null
5568
4834
  : status.tasks.find((task) => task.id === taskId) ?? null;
@@ -5604,23 +4870,27 @@ export class WorkflowService {
5604
4870
  ? [task]
5605
4871
  : [];
5606
4872
  }).at(0) ?? null;
4873
+ let prioritizedRefreshTask = null;
4874
+ if (!requestedTask) {
4875
+ let approved = null;
4876
+ try {
4877
+ approved = this.store.readKnowledgeMap(status.projectId);
4878
+ }
4879
+ catch (error) {
4880
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
4881
+ throw error;
4882
+ }
4883
+ if (approved?.status === 'active' && approved.approval) {
4884
+ const inspected = inspectCurrentKnowledgeMap();
4885
+ if (inspected.status === 'stale')
4886
+ prioritizedRefreshTask = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, approved, inspected)?.task ?? null;
4887
+ }
4888
+ }
5607
4889
  const activeTask = requestedTask
4890
+ ?? prioritizedRefreshTask
5608
4891
  ?? prioritizedRemediationTask
5609
4892
  ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
5610
4893
  if (activeTask) {
5611
- let pendingReviewDependencyRecovery = null;
5612
- try {
5613
- const reconstructed = pendingReviewSourceBinding(repositoryRoot, this.store, activeTask, runGit(repositoryRoot, ['rev-parse', 'HEAD^']), runGit(repositoryRoot, ['rev-parse', `${activeTask.baseBranch}^`]));
5614
- pendingReviewDependencyRecovery = this.pendingReviewDependencyPreflight(repositoryRoot, activeTask.id, reconstructed);
5615
- }
5616
- catch { /* A nonmatching shape follows ordinary navigation without a compatibility bypass. */ }
5617
- if (pendingReviewDependencyRecovery?.eligible) {
5618
- return withAdoption({ scope: 'task', id: activeTask.id, status: activeTask.status,
5619
- action: 'update pending-review-dependency-recover', revision: activeTask.revision,
5620
- pendingReviewDependencyRecovery,
5621
- sourceReceiptRequired: true,
5622
- });
5623
- }
5624
4894
  const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
5625
4895
  if (historicalStepProvenanceRecovery.eligible) {
5626
4896
  return withAdoption({
@@ -5648,22 +4918,6 @@ export class WorkflowService {
5648
4918
  },
5649
4919
  });
5650
4920
  }
5651
- const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
5652
- if (planIntegrityDependencyRecovery.eligible) {
5653
- return withAdoption({
5654
- scope: 'task',
5655
- id: activeTask.id,
5656
- status: activeTask.status,
5657
- action: 'update plan-integrity-dependency-recover',
5658
- revision: activeTask.revision,
5659
- dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
5660
- compatibilityBoundary: {
5661
- preservesWorktree: true,
5662
- prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
5663
- continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
5664
- },
5665
- });
5666
- }
5667
4921
  const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
5668
4922
  if (dependencyRecovery.eligible) {
5669
4923
  return withAdoption({
@@ -5675,6 +4929,18 @@ export class WorkflowService {
5675
4929
  dependencyProvenanceRecovery: dependencyRecovery,
5676
4930
  });
5677
4931
  }
4932
+ const navigationSourceHead = activeTask.authorizations.filter(item => item.kind === 'execution' && item.decision === 'approved').at(-1)?.headCommit;
4933
+ if (activeTask.status === 'ready' && navigationSourceHead
4934
+ && activeTask.dependencyProvenanceRecoveries?.some(item => item.planIntegrity?.mode === 'navigation-fix-dirty-carryover')) {
4935
+ const transport = inspectBoundedWorkflowDependencyCommit(repositoryRoot, headCommit(repositoryRoot));
4936
+ if (transport?.packageVersion === PACKAGE_VERSION && transport.parentCommitSha === navigationSourceHead) {
4937
+ return withAdoption({ scope: 'task', id: activeTask.id, revision: activeTask.revision,
4938
+ action: 'update navigation-transport-preflight',
4939
+ requiredInputs: ['--tarball <exact target artifact>', '--file <source receipt>'],
4940
+ reason: 'Verify the preserved source receipt before registering navigation dependency transport. Missing evidence blocks recovery.',
4941
+ });
4942
+ }
4943
+ }
5678
4944
  const activeDownstreamProofDependencyRecovery = this.activeDownstreamProofDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
5679
4945
  if (activeDownstreamProofDependencyRecovery.eligible) {
5680
4946
  return withAdoption({
@@ -5691,22 +4957,6 @@ export class WorkflowService {
5691
4957
  },
5692
4958
  });
5693
4959
  }
5694
- const downstreamProofReplanDependencyRecovery = this.downstreamProofReplanDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
5695
- if (downstreamProofReplanDependencyRecovery.eligible) {
5696
- return withAdoption({
5697
- scope: 'task',
5698
- id: activeTask.id,
5699
- status: activeTask.status,
5700
- action: 'update downstream-proof-replan-dependency-recover',
5701
- revision: activeTask.revision,
5702
- dependencyProvenanceRecovery: downstreamProofReplanDependencyRecovery,
5703
- compatibilityBoundary: {
5704
- preservesWorktree: true,
5705
- prerequisite: 'A newer package preflight approved the exact beta.12.6 stranded replan transport before dependency-only commits were created.',
5706
- continuation: 'Register this dependency commit, then run task downstream-proof-replan-recover.',
5707
- },
5708
- });
5709
- }
5710
4960
  const activeTaskMilestone = activeTask.milestoneId
5711
4961
  ? milestonesById.get(activeTask.milestoneId) ?? null
5712
4962
  : null;
@@ -5730,61 +4980,13 @@ export class WorkflowService {
5730
4980
  },
5731
4981
  }
5732
4982
  : value);
5733
- const currentPlanRiskAuditExists = activeTask.planHash
5734
- ? readPlanRiskAuditEvents(this.store, activeTask).some((event) => event.taskRevision === activeTask.revision && event.planHash === activeTask.planHash)
5735
- : false;
5736
- const downstreamReplanRecovery = assessDownstreamProofReplanRecovery(this.store, repositoryRoot, activeTask, { currentPlanRiskAuditExists });
5737
- if (downstreamReplanRecovery.applicable) {
5738
- const environmentBlockers = [
5739
- ...(!downstreamReplanRecovery.dependencyRecoveryCommit
5740
- ? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
5741
- : []),
5742
- ...(status.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
5743
- ...(status.observation.pendingProjectTransactions.length > 0
5744
- || status.observation.pendingMilestoneTransactions.length > 0
5745
- || status.observation.pendingTaskTransactions.length > 0
5746
- ? ['No pending Workflow transaction may exist.'] : []),
5747
- ...(status.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
5748
- ...(status.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
5749
- ];
5750
- return withAdoption(withMilestoneCancellation(downstreamReplanRecovery.eligible && environmentBlockers.length === 0
5751
- ? {
5752
- scope: 'task',
5753
- id: activeTask.id,
5754
- status: activeTask.status,
5755
- action: 'task downstream-proof-replan-recover',
5756
- revision: activeTask.revision,
5757
- downstreamProofReplanRecovery: {
5758
- state: 'eligible',
5759
- invalidationEventId: downstreamReplanRecovery.invalidation.eventId,
5760
- sourcePlanHash: downstreamReplanRecovery.invalidation.planHash,
5761
- reboundPlanHash: activeTask.planHash,
5762
- dirtyFiles: downstreamReplanRecovery.compatibility.dirtyFiles,
5763
- dirtyWorktreeHash: downstreamReplanRecovery.compatibility.dirtyWorktreeHash,
5764
- dependencyRecoveryCommit: downstreamReplanRecovery.dependencyRecoveryCommit,
5765
- boundedEffect: 'restore-needs-fix-and-return-same-task-to-plan-set',
5766
- },
5767
- }
5768
- : {
5769
- scope: 'task',
5770
- id: activeTask.id,
5771
- status: activeTask.status,
5772
- action: 'doctor',
5773
- blockedAction: 'task downstream-proof-replan-recover',
5774
- revision: activeTask.revision,
5775
- downstreamProofReplanRecovery: {
5776
- state: 'blocked',
5777
- blockers: [...downstreamReplanRecovery.blockers, ...environmentBlockers],
5778
- },
5779
- }));
5780
- }
5781
4983
  let activePlanRiskAudit = null;
5782
4984
  let planRiskAuditRecovery = null;
5783
4985
  const requiresReplacementPlan = activeTask.status === 'needs_fix'
5784
4986
  && !activeTask.steps.some((step) => step.status === 'failed');
5785
4987
  if (activeTask.planHash && !requiresReplacementPlan) {
5786
4988
  try {
5787
- activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
4989
+ activePlanRiskAudit = readCorrectivePlanningAudit(this.store, activeTask);
5788
4990
  }
5789
4991
  catch (error) {
5790
4992
  if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
@@ -5814,6 +5016,9 @@ export class WorkflowService {
5814
5016
  : new Map();
5815
5017
  const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
5816
5018
  const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
5019
+ && activeTask.planHash !== null
5020
+ && activeTask.workspaceOwner === null
5021
+ && activeTask.steps.every(step => step.status === 'planned')
5817
5022
  && remediationMilestone?.status === 'active'
5818
5023
  && remediationMembership?.disposition === 'required';
5819
5024
  const remediationAncestorIds = new Set();
@@ -5990,9 +5195,8 @@ export class WorkflowService {
5990
5195
  if (taskNext?.action === 'task step-complete'
5991
5196
  || taskNext?.action === 'task step-review'
5992
5197
  || taskNext?.action === 'task downstream-proof-recover'
5993
- || taskNext?.action === 'task remediation-mode-recover'
5994
5198
  || (taskNext?.action === 'task writer-credential-replace'
5995
- && ['task step-complete', 'task step-review', 'task remediation-mode-recover'].includes(String(taskNext?.blockedAction)))
5199
+ && ['task step-complete', 'task step-review'].includes(String(taskNext?.blockedAction)))
5996
5200
  || taskNext?.action === 'task plan-integrity-recover'
5997
5201
  || taskNext?.blockedAction === 'task plan-integrity-recover'
5998
5202
  || taskNext?.action === 'task corrective-yield'
@@ -6068,6 +5272,7 @@ export class WorkflowService {
6068
5272
  contextRefresh: {
6069
5273
  action: 'task context-refresh',
6070
5274
  mode: refreshAssessment.mode,
5275
+ authority: readFailedStepPlanningRecovery(this.store, activeTask) ? 'planning-only' : 'execution',
6071
5276
  addedSources: refreshAssessment.addedSources,
6072
5277
  options: contextRefreshOptions,
6073
5278
  },
@@ -6172,6 +5377,24 @@ export class WorkflowService {
6172
5377
  milestoneInitialAssembly: initialAssembly,
6173
5378
  });
6174
5379
  }
5380
+ // Only this Milestone's authority lease gates its own transitions.
5381
+ // Keep this outside validation admission: the executor already owns its lease there.
5382
+ const milestoneLease = this.inspectWriterLease(repositoryRoot, activeMilestone.id);
5383
+ if (milestoneLease) {
5384
+ return withAdoption({
5385
+ scope: 'milestone',
5386
+ id: activeMilestone.id,
5387
+ entityId: activeMilestone.id,
5388
+ status: activeMilestone.status,
5389
+ action: milestoneLease.stale ? 'locks repair' : 'blocked',
5390
+ ...(!milestoneLease.stale ? { navigation: { kind: 'wait', reason: 'active-milestone-writer-lease' } } : {}),
5391
+ revision: activeMilestone.revision,
5392
+ lease: milestoneLease,
5393
+ reason: milestoneLease.stale
5394
+ ? 'The expired Milestone authority lease requires explicit repair before another transition.'
5395
+ : 'Another writer owns this Milestone; wait for that operation to release its lease.',
5396
+ });
5397
+ }
6175
5398
  if (activeMilestone.status === 'awaiting_final_acceptance') {
6176
5399
  const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
6177
5400
  const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
@@ -6213,6 +5436,33 @@ export class WorkflowService {
6213
5436
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6214
5437
  });
6215
5438
  }
5439
+ let milestoneAction = nextForMilestone(activeMilestone);
5440
+ let validationBlock = null;
5441
+ if (activeMilestone.status === 'active') {
5442
+ try {
5443
+ this.assertMilestoneValidationReady(repositoryRoot, activeMilestone);
5444
+ milestoneAction = 'milestone validate';
5445
+ }
5446
+ catch (error) {
5447
+ if (!(error instanceof WorkflowError))
5448
+ throw error;
5449
+ if (error.code === 'GIT_PRECONDITION_FAILED') {
5450
+ milestoneAction = 'restore clean base, then milestone validate';
5451
+ validationBlock = { blockedAction: 'milestone validate', reason: error.message };
5452
+ }
5453
+ else if (error.code === 'TRANSITION_BLOCKED' && Array.isArray(error.details.taskIds)) {
5454
+ validationBlock = { blockedAction: 'milestone validate', incompleteTaskIds: error.details.taskIds };
5455
+ }
5456
+ else if (error.code === 'TRANSITION_BLOCKED') {
5457
+ milestoneAction = 'doctor';
5458
+ validationBlock = { blockedAction: 'milestone validate', reason: error.message };
5459
+ }
5460
+ else
5461
+ throw error;
5462
+ }
5463
+ }
5464
+ if (activeMilestone.status === 'blocked')
5465
+ milestoneAction = 'milestone scope-change-prepare';
6216
5466
  const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
6217
5467
  ? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
6218
5468
  : [];
@@ -6220,7 +5470,24 @@ export class WorkflowService {
6220
5470
  scope: 'milestone',
6221
5471
  id: activeMilestone.id,
6222
5472
  status: activeMilestone.status,
6223
- action: nextForMilestone(activeMilestone),
5473
+ action: milestoneAction,
5474
+ ...(validationBlock ?? {}),
5475
+ ...(activeMilestone.status === 'blocked' ? {
5476
+ options: [
5477
+ { action: 'milestone scope-change-prepare', eligible: true, requiresCandidatePlan: true },
5478
+ { action: 'milestone cancel', eligible: true, requiresExplicitCancellationIntent: true },
5479
+ ],
5480
+ scopeChangeBoundary: {
5481
+ state: 'human-decision-required',
5482
+ requiresCandidatePlan: true,
5483
+ prepareAction: 'milestone scope-change-prepare',
5484
+ applyAction: 'milestone scope-change-apply',
5485
+ requiresExplicitHumanConfirmation: true,
5486
+ confirmationCodeSource: 'scope-change-prepare-result',
5487
+ afterPreparation: 'await-human-confirmation-before-apply',
5488
+ applyRequiresSamePlanAndActor: true,
5489
+ },
5490
+ } : {}),
6224
5491
  revision: activeMilestone.revision,
6225
5492
  ...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
6226
5493
  ? {
@@ -6424,7 +5691,11 @@ export class WorkflowService {
6424
5691
  return { milestone, authorization, scopeChange };
6425
5692
  }
6426
5693
  nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
6427
- const transition = task.status === 'awaiting_execution_authorization'
5694
+ if (task.pendingBaseSyncValidation !== undefined)
5695
+ return this.taskGitNavigationOverride(task);
5696
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
5697
+ // A stopped rebound Plan can expose planning recovery, never an approval route.
5698
+ const transition = planningRecovery ? null : task.status === 'awaiting_execution_authorization'
6428
5699
  ? 'task.execution_authorize'
6429
5700
  : task.status === 'awaiting_final_acceptance'
6430
5701
  ? 'task.final_accept'
@@ -6438,49 +5709,44 @@ export class WorkflowService {
6438
5709
  const correctiveDerivedAuthority = transition && correctiveDerivedActor
6439
5710
  ? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
6440
5711
  : null;
6441
- let remediationModeRecovery = null;
6442
- let guardedCorrectivePosture = null;
6443
- if (planRiskAudit) {
6444
- try {
6445
- guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
6446
- }
6447
- catch (error) {
6448
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
6449
- if (!remediationModeRecovery)
6450
- throw error;
6451
- }
6452
- }
6453
- let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
6454
- ? guardedCorrectivePosture
5712
+ const guardedCorrectivePosture = planRiskAudit
5713
+ ? findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit)
6455
5714
  : null;
6456
- if (!remediationModeRecovery && planRiskAudit && guardedCorrectivePosture === null) {
6457
- try {
6458
- catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
6459
- }
6460
- catch (error) {
6461
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
6462
- if (!remediationModeRecovery)
6463
- throw error;
6464
- }
5715
+ let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
5716
+ ? guardedCorrectivePosture : null;
5717
+ if (planRiskAudit && guardedCorrectivePosture === null) {
5718
+ catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
6465
5719
  }
6466
5720
  const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
6467
5721
  const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
6468
5722
  const rootCauseStepId = nextStepId
6469
5723
  ?? task.steps.find((candidate) => candidate.status === 'planned')?.id
6470
5724
  ?? null;
5725
+ const hasDirtyFiles = changedFiles(repositoryRoot).length > 0;
5726
+ const nextStartPhase = stepStartPhase(task.steps.find(step => step.id === nextStepId)?.status, hasDirtyFiles);
5727
+ const rootCauseStartPhase = stepStartPhase(task.steps.find(step => step.id === rootCauseStepId)?.status, hasDirtyFiles);
6471
5728
  const downstreamProofCarryover = next.action === 'task run'
6472
5729
  && nextStepId
6473
- && changedFiles(repositoryRoot).length > 0
5730
+ && nextStartPhase === 'planned-dirty'
6474
5731
  ? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
6475
5732
  : null;
6476
5733
  const planIntegrityDirtyCarryover = next.action === 'task run'
6477
5734
  && nextStepId
6478
- && changedFiles(repositoryRoot).length > 0
5735
+ && nextStartPhase === 'planned-dirty'
6479
5736
  ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
6480
5737
  : null;
6481
- let rootCauseReplanDirtyCarryover = ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
5738
+ const navigationDirtyCarryover = next.action === 'task run' && rootCauseStartPhase === 'planned-dirty' && rootCauseStepId
5739
+ ? assessNavigationDirtyCarryover(this.store, repositoryRoot, task, rootCauseStepId)
5740
+ : null;
5741
+ const navigationDirtyOverlay = navigationDirtyCarryover?.applicable
5742
+ ? navigationDirtyCarryover.eligible
5743
+ ? { navigationDirtyCarryover: { state: 'validated', stepId: rootCauseStepId, dirtyFiles: navigationDirtyCarryover.compatibility?.dirtyFiles, dirtyWorktreeHash: navigationDirtyCarryover.compatibility?.dirtyWorktreeHash } }
5744
+ : { action: 'doctor', blockedAction: 'task run', navigationDirtyCarryover: { state: 'blocked', stepId: rootCauseStepId, blockers: navigationDirtyCarryover.blockers } }
5745
+ : {};
5746
+ let rootCauseReplanDirtyCarryover = !navigationDirtyCarryover?.applicable && ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
6482
5747
  && rootCauseStepId
6483
- && changedFiles(repositoryRoot).length > 0
5748
+ // Match runStep: historical carryover admits a replacement start, never a failed retry.
5749
+ && rootCauseStartPhase === 'planned-dirty'
6484
5750
  ? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
6485
5751
  : null;
6486
5752
  if (rootCauseReplanDirtyCarryover?.eligible) {
@@ -6546,14 +5812,14 @@ export class WorkflowService {
6546
5812
  }
6547
5813
  const handoffPosture = readTaskC1Posture(this.store, task);
6548
5814
  const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
6549
- const failedStepRemediations = failedStep && !remediationModeRecovery
5815
+ const failedStepRemediations = failedStep
6550
5816
  ? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
6551
5817
  : [];
6552
5818
  const failedStepReviewRouting = failedStep
6553
5819
  ? latestFailedStepReviewRouting(this.store, task, failedStep.id)
6554
5820
  : null;
6555
- const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
6556
- ? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
5821
+ const planIntegrityAssessment = planRiskAudit && failedStep
5822
+ ? this.assessPlanIntegrityRecoveryAdmission(repositoryRoot, task, failedStep.id)
6557
5823
  : null;
6558
5824
  const downstreamProofAssessment = assessDownstreamProofRecovery(this.store, repositoryRoot, task);
6559
5825
  const strictStepReviewStep = planRiskAudit
@@ -6711,28 +5977,10 @@ export class WorkflowService {
6711
5977
  },
6712
5978
  };
6713
5979
  }
6714
- if (remediationModeRecovery) {
6715
- guardedCorrectiveOverlay = {
6716
- action: 'task remediation-mode-recover',
6717
- stepId: remediationModeRecovery.remediation.stepId,
6718
- remediationModeRecovery: {
6719
- state: 'required',
6720
- remediationEventId: remediationModeRecovery.remediation.eventId,
6721
- remediationEventHash: remediationModeRecovery.remediation.eventHash,
6722
- correctiveDecisionEventId: remediationModeRecovery.decision.eventId,
6723
- correctiveDecisionRecoveryEventId: remediationModeRecovery.correctiveRecovery.eventId,
6724
- attemptOrdinal: remediationModeRecovery.remediation.attemptOrdinal,
6725
- fromMode: 'ordinary',
6726
- toMode: 'corrective',
6727
- requiredActor: remediationModeRecovery.actor,
6728
- },
6729
- };
6730
- }
6731
- if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
5980
+ if (guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
6732
5981
  guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
6733
5982
  }
6734
- if (!remediationModeRecovery
6735
- && guardedCorrectivePosture
5983
+ if (guardedCorrectivePosture
6736
5984
  && (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
6737
5985
  switch (guardedCorrectivePosture.posture) {
6738
5986
  case 'decision-required':
@@ -6913,62 +6161,42 @@ export class WorkflowService {
6913
6161
  retainedImplementation: 'journal-recoverable-1-to-1',
6914
6162
  diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
6915
6163
  structuralReplacementAvailable: false,
6164
+ productDecisionRequired: true,
6165
+ executionAvailable: false,
6166
+ decisionEffect: 'No decision authorizes structural replacement in this package.',
6167
+ productDecisionOptions: replacementMilestone && !['accepted', 'cancelled'].includes(replacementMilestone.status)
6168
+ ? [{
6169
+ command: 'milestone cancel',
6170
+ milestoneId: replacementMilestone.id,
6171
+ expectedRevision: replacementMilestone.revision,
6172
+ reasonRequired: true,
6173
+ effect: 'Cancels the whole Milestone; does not create or authorize a replacement Task.',
6174
+ }]
6175
+ : [],
6916
6176
  incidentDependencies,
6917
6177
  discoveryCandidates: replacementDiscoveries.map((discovery) => ({
6918
6178
  id: discovery.id,
6919
6179
  revision: discovery.revision,
6920
6180
  goal: discovery.goal,
6921
6181
  })),
6922
- requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
6182
+ requiredAction: 'The owner must decide whether to stop this Task or revise the Milestone scope through supported transitions; structural replacement is unavailable.',
6923
6183
  },
6924
6184
  };
6925
6185
  }
6926
6186
  break;
6927
6187
  case 'stop-escalate':
6928
- try {
6929
- this.assertStopOverrideRepositoryBoundary(resolveRepositoryIdentity(repositoryRoot), task);
6930
- prepareStopEscalateOverride(this.store, task, guardedCorrectivePosture.stepId, task.revision, 'user', 'navigation eligibility probe', headCommit(repositoryRoot));
6931
- guardedCorrectiveOverlay = {
6932
- action: 'task stop-override-prepare',
6933
- blockedAction: 'task run',
6934
- stepId: guardedCorrectivePosture.stepId,
6935
- correctiveDecisionGate: {
6936
- attemptCount: guardedCorrectivePosture.attemptCount,
6937
- decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
6938
- decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
6939
- humanApprovalRequired: true,
6940
- delegatedApprovalAllowed: false,
6941
- },
6942
- stopOverride: {
6943
- state: 'eligible',
6944
- policy: 'beta11-attempt4-stop-compatibility-v1',
6945
- prepareCommand: 'task stop-override-prepare',
6946
- applyCommand: 'task stop-override-apply',
6947
- expectedRevision: task.revision,
6948
- appendOnly: true,
6949
- preservesOriginalDecision: true,
6950
- requiredAction: 'Prepare with an explicit human actor and reason, then apply the exact confirmation code in a later user turn.',
6951
- },
6952
- };
6953
- }
6954
- catch (error) {
6955
- guardedCorrectiveOverlay = {
6956
- action: 'doctor',
6957
- blockedAction: 'task stop-override-prepare',
6958
- stepId: guardedCorrectivePosture.stepId,
6959
- correctiveDecisionGate: {
6960
- attemptCount: guardedCorrectivePosture.attemptCount,
6961
- decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
6962
- decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
6963
- humanApprovalRequired: true,
6964
- },
6965
- stopOverride: {
6966
- state: 'blocked',
6967
- policy: 'beta11-attempt4-stop-compatibility-v1',
6968
- reason: error instanceof Error ? error.message : String(error),
6969
- },
6970
- };
6971
- }
6188
+ guardedCorrectiveOverlay = {
6189
+ action: 'doctor',
6190
+ blockedAction: 'task run',
6191
+ stepId: guardedCorrectivePosture.stepId,
6192
+ correctiveDecisionGate: {
6193
+ attemptCount: guardedCorrectivePosture.attemptCount,
6194
+ decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
6195
+ decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
6196
+ humanApprovalRequired: true,
6197
+ },
6198
+ reason: 'The recorded stop decision blocks execution; legacy stop-override creation is no longer supported.',
6199
+ };
6972
6200
  break;
6973
6201
  default:
6974
6202
  break;
@@ -7119,6 +6347,7 @@ export class WorkflowService {
7119
6347
  }
7120
6348
  : {}),
7121
6349
  ...mechanicalFeasibilityOverlay,
6350
+ ...navigationDirtyOverlay,
7122
6351
  ...(downstreamProofCarryover?.applicable
7123
6352
  ? downstreamProofCarryover.eligible
7124
6353
  ? {
@@ -7187,10 +6416,11 @@ export class WorkflowService {
7187
6416
  rootCauseReplanDirtyCarryover: {
7188
6417
  state: 'awaiting-c1',
7189
6418
  stepId: rootCauseStepId,
7190
- requiredActor: defaultTaskWorkerActor(task.id),
6419
+ requiredActor: rootCauseReplanDirtyCarryover.requiredActor ?? defaultTaskWorkerActor(task.id),
7191
6420
  },
7192
6421
  }
7193
6422
  : handoffPosture.state === 'pending'
6423
+ && (!rootCauseReplanDirtyCarryover.requiredActor || handoffPosture.targetActor === rootCauseReplanDirtyCarryover.requiredActor)
7194
6424
  ? {}
7195
6425
  : {
7196
6426
  action: 'doctor',
@@ -7231,15 +6461,15 @@ export class WorkflowService {
7231
6461
  // fix review. Advertise the existing append-only decision route without turning
7232
6462
  // every ordinary repair into a mandatory replan or rewriting that review.
7233
6463
  const correctiveDecisionOrdinal = failedStepRemediations.length + 1;
7234
- const optionalCorrectiveDecision = result.action === 'task run'
7235
- && ['needs_fix', 'ready'].includes(task.status)
6464
+ const optionalCorrectiveDecision = (result.action === 'task run' || planningRecovery !== null)
6465
+ && (['needs_fix', 'ready'].includes(task.status) || planningRecovery !== null)
7236
6466
  && failedStep && task.planHash && planRiskAudit
7237
6467
  && planRiskAudit.reviewRequiredStepIds.includes(failedStep.id)
7238
6468
  && !task.steps.some(step => step.status === 'in_progress')
7239
6469
  && failedStepRemediations.length >= 2
7240
6470
  && handoffPosture.state !== 'pending'
7241
6471
  && !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
7242
- && currentBranch(repositoryRoot) === task.taskBranch && isClean(repositoryRoot)
6472
+ && correctiveDecisionCheckoutEligible(this.store, repositoryRoot, task, failedStep.id)
7243
6473
  ? {
7244
6474
  command: 'task corrective-decision',
7245
6475
  state: 'eligible',
@@ -7255,7 +6485,8 @@ export class WorkflowService {
7255
6485
  recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
7256
6486
  auditorMustDifferFromLatestStrictReviewer: true,
7257
6487
  requiredActor: handoffPosture.state === 'claimed' ? handoffPosture.claimant : null,
7258
- decisions: ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
6488
+ decisions: planningRecovery ? ['replan-required', 'split-required', 'stop-escalate']
6489
+ : ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
7259
6490
  authority: 'none',
7260
6491
  preservesOriginalReview: true,
7261
6492
  humanApprovalRequiredForDecision: false,
@@ -7264,11 +6495,17 @@ export class WorkflowService {
7264
6495
  }
7265
6496
  : null;
7266
6497
  const resultWithCorrectiveOption = optionalCorrectiveDecision
7267
- ? { ...result, correctiveDecisionOption: optionalCorrectiveDecision } : result;
6498
+ ? { ...result, correctiveDecisionOption: optionalCorrectiveDecision,
6499
+ ...(planningRecovery ? { action: 'task corrective-decision', blockedAction: 'task authorize',
6500
+ planningRecovery: { authority: 'planning-only', obstructionAuditHash: planningRecovery.current.audit.eventHash,
6501
+ baselineAuditHash: planningRecovery.baseline.eventHash } } : {}) }
6502
+ : planningRecovery && result.action === 'task authorize'
6503
+ ? { ...result, action: 'doctor', blockedAction: 'task authorize' } : result;
7268
6504
  const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
7269
6505
  || (rootCauseReplanDirtyCarryover.blockers.length === 1
7270
6506
  && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
7271
- if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
6507
+ if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim
6508
+ && (!rootCauseReplanDirtyCarryover?.requiredActor || handoffPosture.targetActor === rootCauseReplanDirtyCarryover.requiredActor)) {
7272
6509
  return this.withTaskNavigationContracts(task, {
7273
6510
  ...resultWithCorrectiveOption,
7274
6511
  action: 'task claim',
@@ -7301,28 +6538,43 @@ export class WorkflowService {
7301
6538
  },
7302
6539
  };
7303
6540
  }
7304
- assertTaskContextChangeAllowed(task) {
6541
+ taskContextChangeBlocker(task, operation) {
6542
+ const blocked = (error) => ({ error, activeStep: false });
7305
6543
  if (readCurrentPreExecutionReplan(this.store, task)) {
7306
- throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has a pending pre-execution replan; replace the implementation Plan before changing context.`, { taskId: task.id, requiredAction: 'task plan-set' });
6544
+ return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has a pending pre-execution replan; replace the implementation Plan before changing context.`, { taskId: task.id, requiredAction: 'task plan-set' }));
7307
6545
  }
7308
6546
  if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
7309
- throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
6547
+ return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
7310
6548
  taskId: task.id,
7311
6549
  requiredAction: 'Run task plan-set with the full unfinished remainder before requesting authorization.',
7312
- });
6550
+ }));
7313
6551
  }
7314
6552
  const posture = this.taskTerminalCorrectivePosture(task);
7315
- if (!posture)
7316
- return;
7317
- throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
7318
- taskId: task.id,
7319
- stepId: posture.stepId,
7320
- decision: posture.posture,
7321
- decisionEventId: posture.correctiveDecision?.eventId ?? null,
7322
- requiredAction: posture.posture === 'split-required'
7323
- ? 'Materialize an atomic replacement Task through the exact replacement route.'
7324
- : 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
7325
- });
6553
+ if (posture) {
6554
+ return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
6555
+ taskId: task.id,
6556
+ stepId: posture.stepId,
6557
+ decision: posture.posture,
6558
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
6559
+ requiredAction: posture.posture === 'split-required'
6560
+ ? 'This Task cannot resume after split-required; no executable replacement route is available. Request a human decision without changing retained state.'
6561
+ : 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
6562
+ }));
6563
+ }
6564
+ if (!taskCanRebindKnowledge(task)) {
6565
+ return blocked(new WorkflowError('TRANSITION_BLOCKED', operation === 'rebind'
6566
+ ? `Cannot rebind Task Plan knowledge while Task is ${task.status}.`
6567
+ : `Cannot refresh Task context while Task is ${task.status}.`));
6568
+ }
6569
+ const activeStep = task.steps.find(step => step.status === 'in_progress');
6570
+ if (activeStep)
6571
+ return {
6572
+ activeStep: true,
6573
+ error: new WorkflowError('TRANSITION_BLOCKED', operation === 'rebind'
6574
+ ? `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`
6575
+ : `Cannot refresh Task context while ${activeStep.id} is in progress.`),
6576
+ };
6577
+ return null;
7326
6578
  }
7327
6579
  stopOverrideContextRebindDecisionId(task, stepId, repositoryRoot) {
7328
6580
  return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
@@ -7346,7 +6598,15 @@ export class WorkflowService {
7346
6598
  taskTerminalCorrectivePosture(task) {
7347
6599
  if (!task.planHash)
7348
6600
  return null;
7349
- const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
6601
+ if (task.status === 'awaiting_execution_authorization' && task.authorizations.length === 0
6602
+ && task.baseCommit === null && task.taskBranch === null && task.workspaceOwner === null
6603
+ && task.mergedCommit === null && task.systemCommits.length === 0
6604
+ && task.resultHash === null && task.evidenceHash === null && task.review.status === 'pending'
6605
+ && task.steps.every((step) => (step.status === 'planned' || (step.status === 'blocked' && step.dependencies.length > 0))
6606
+ && step.evidence === null && !step.failurePause)
6607
+ && readPlanRiskAuditEvents(this.store, task).length === 0)
6608
+ return null;
6609
+ const planRiskAudit = readCurrentPlanningObstructionAudit(this.store, task);
7350
6610
  if (!planRiskAudit)
7351
6611
  return null;
7352
6612
  const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
@@ -7354,42 +6614,6 @@ export class WorkflowService {
7354
6614
  ? posture
7355
6615
  : null;
7356
6616
  }
7357
- assertStopOverrideRepositoryBoundary(identity, task, observedLeases) {
7358
- const blockers = [];
7359
- const dirty = changedFiles(identity.repositoryRoot);
7360
- if (dirty.length > 0)
7361
- blockers.push('Repository checkout must be clean.');
7362
- if (task.workspaceOwner !== 'local')
7363
- blockers.push('Stop override is limited to a local Task workspace.');
7364
- if (!task.taskBranch || currentBranch(identity.repositoryRoot) !== task.taskBranch) {
7365
- blockers.push('The current branch must be the recorded Task branch.');
7366
- }
7367
- if (task.steps.some((step) => step.status === 'in_progress'))
7368
- blockers.push('No Step may be in progress.');
7369
- const leases = observedLeases
7370
- ?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
7371
- if (leases.some((lease) => lease.entityId === task.id))
7372
- blockers.push('No active or stale Task writer lease may exist.');
7373
- const projectRoot = this.store.projectRoot(identity.projectId);
7374
- const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
7375
- const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
7376
- if (replanTransactions.pending.some((transaction) => transaction.taskId === task.id)
7377
- || replanTransactions.corrupt.some((transaction) => transaction.taskId === task.id)
7378
- || yieldTransactions.pending.some((transaction) => transaction.taskId === task.id)
7379
- || yieldTransactions.corrupt.some((transaction) => transaction.taskId === task.id)) {
7380
- blockers.push('No pending or corrupt Task transaction may exist.');
7381
- }
7382
- if (listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root)
7383
- .some((operation) => operation.entityId === task.id)) {
7384
- blockers.push('No Core Task operation may exist.');
7385
- }
7386
- if (blockers.length > 0) {
7387
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override repository preconditions are not satisfied.', {
7388
- taskId: task.id,
7389
- blockers,
7390
- });
7391
- }
7392
- }
7393
6617
  delegatedApprovalOptions(projectId, transition, target) {
7394
6618
  return this.listDelegations(projectId)
7395
6619
  .filter((grant) => {
@@ -7413,6 +6637,19 @@ export class WorkflowService {
7413
6637
  }));
7414
6638
  }
7415
6639
  delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
6640
+ let planningRecovery;
6641
+ try {
6642
+ planningRecovery = readFailedStepPlanningRecovery(this.store, task);
6643
+ if (!planningRecovery)
6644
+ readCurrentPlanRiskAudit(this.store, task);
6645
+ }
6646
+ catch (error) {
6647
+ if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
6648
+ return [];
6649
+ throw error;
6650
+ }
6651
+ if (planningRecovery && mode !== 'delegated-content-only')
6652
+ return [];
7416
6653
  return this.listDelegations(projectId)
7417
6654
  .filter((grant) => {
7418
6655
  try {
@@ -7432,8 +6669,8 @@ export class WorkflowService {
7432
6669
  grantId: grant.id,
7433
6670
  principal: grant.principal,
7434
6671
  delegate: grant.delegate,
7435
- transition: 'task.execution_authorize',
7436
- transitions: ['project_memory.approve', 'task.execution_authorize'],
6672
+ transition: planningRecovery ? 'project_memory.approve' : 'task.execution_authorize',
6673
+ transitions: planningRecovery ? ['project_memory.approve'] : ['project_memory.approve', 'task.execution_authorize'],
7437
6674
  scope: grant.scope,
7438
6675
  expiresAt: grant.expiresAt,
7439
6676
  policyHash: grant.policyHash,
@@ -7443,16 +6680,13 @@ export class WorkflowService {
7443
6680
  const tasksById = new Map(tasks.map((task) => [task.id, task]));
7444
6681
  for (const task of tasks) {
7445
6682
  if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
7446
- || !task.planHash
7447
- || !taskCanRebindKnowledge(task)
7448
- || (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed'))
7449
- || this.taskHasTerminalCorrectiveDecision(task))
6683
+ || !task.planHash)
7450
6684
  continue;
7451
6685
  const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
7452
6686
  if (!assessment.eligible)
7453
6687
  continue;
7454
6688
  const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
7455
- if (options.length > 0)
6689
+ if (options.length > 0 && this.taskContextChangeBlocker(task, 'refresh') === null)
7456
6690
  return { task, assessment, options };
7457
6691
  }
7458
6692
  return null;
@@ -7560,12 +6794,31 @@ export class WorkflowService {
7560
6794
  : inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
7561
6795
  }
7562
6796
  taskGitNavigationOverride(task) {
6797
+ if (task.pendingBaseSyncValidation !== undefined) {
6798
+ const assessment = assessPendingBaseSyncValidation(task);
6799
+ return { scope: 'task', id: task.id, status: task.status, revision: task.revision,
6800
+ action: assessment.reason ? 'blocked' : 'task sync-base',
6801
+ pendingBaseSyncValidation: task.pendingBaseSyncValidation,
6802
+ ...(assessment.reason ? { dirtyFiles: assessment.dirtyFiles,
6803
+ navigation: { kind: 'wait', reason: 'base-sync-validation-snapshot', waitFor: assessment.reason } } : {}),
6804
+ };
6805
+ }
7563
6806
  if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
7564
6807
  return null;
7565
6808
  }
7566
6809
  const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
7567
6810
  if (currentBase === task.baseCommit)
7568
6811
  return null;
6812
+ const taskHead = runGit(task.repositoryRoot, ['rev-parse', task.taskBranch]);
6813
+ const conflict = currentBaseSyncConflict(this.store, task, taskHead, currentBase);
6814
+ if (conflict)
6815
+ return {
6816
+ scope: 'task', id: task.id, status: task.status, revision: task.revision, action: 'blocked',
6817
+ baseSyncConflict: conflict,
6818
+ navigation: { kind: 'wait', reason: 'base-sync-conflict', externalDecisionRequired: true,
6819
+ waitFor: 'The base owner resolves the conflict on the base branch without changing the reviewed Task head.',
6820
+ resume: 'After the base HEAD changes, obtain fresh status and next; synchronization still requires review and acceptance.' },
6821
+ };
7569
6822
  return {
7570
6823
  scope: 'task',
7571
6824
  id: task.id,
@@ -7579,43 +6832,6 @@ export class WorkflowService {
7579
6832
  },
7580
6833
  };
7581
6834
  }
7582
- carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
7583
- let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
7584
- let dependencyCommitCount = 0;
7585
- while (!task.systemCommits.includes(boundaryCommit)) {
7586
- const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
7587
- if (!dependencyCommit)
7588
- return null;
7589
- boundaryCommit = dependencyCommit.parentCommitSha;
7590
- dependencyCommitCount += 1;
7591
- }
7592
- if (dependencyCommitCount === 0)
7593
- return null;
7594
- const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
7595
- const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
7596
- const compatibility = record.planIntegrity;
7597
- if (!compatibility
7598
- || record.commitSha !== boundaryCommit
7599
- || !task.systemCommits.includes(record.commitSha)
7600
- || compatibility.planHash !== task.planHash
7601
- || canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
7602
- || compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
7603
- return false;
7604
- const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
7605
- if (!step || step.status !== 'failed')
7606
- return false;
7607
- const remediationEventIds = readRemediationEvents(this.store, task)
7608
- .filter((event) => event.stepId === compatibility.stepId)
7609
- .map((event) => event.eventId);
7610
- if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
7611
- return false;
7612
- return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
7613
- && event.stepId === compatibility.stepId
7614
- && event.decision === 'replan-required')
7615
- && readTaskC1Posture(this.store, task).state === 'none';
7616
- });
7617
- return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
7618
- }
7619
6835
  postRebindCheckSupportCompatibility(snapshot, task) {
7620
6836
  if (task.status !== 'awaiting_execution_authorization'
7621
6837
  || !task.planHash
@@ -7790,9 +7006,12 @@ export class WorkflowService {
7790
7006
  blockers,
7791
7007
  };
7792
7008
  }
7793
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null, rootCauseReplan = null) {
7009
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, historicalStepProvenance = null) {
7794
7010
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
7795
7011
  const now = this.now().getTime();
7012
+ const navigationCarryover = !activeDownstreamProof && !historicalStepProvenance && changedFiles(snapshot.identity.repositoryRoot).length > 0
7013
+ ? assessNavigationDirtyCarryover(this.store, snapshot.identity.repositoryRoot, task, undefined, 'HEAD^')
7014
+ : null;
7796
7015
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
7797
7016
  activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
7798
7017
  staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
@@ -7810,7 +7029,7 @@ export class WorkflowService {
7810
7029
  currentBranch: versions.currentBranch,
7811
7030
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
7812
7031
  },
7813
- }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
7032
+ }, activeDownstreamProof, historicalStepProvenance, navigationCarryover?.eligible ? navigationCarryover.compatibility : null);
7814
7033
  }
7815
7034
  assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
7816
7035
  const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
@@ -7876,10 +7095,19 @@ export class WorkflowService {
7876
7095
  }
7877
7096
  const candidates = task.steps
7878
7097
  .filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
7879
- .flatMap((step) => [...new Set(readRemediationEvents(this.store, task)
7880
- .filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed')
7881
- .map((event) => event.failureEvidence.completionCommit))]
7882
- .map((historyHead) => assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead })))
7098
+ .flatMap((step) => {
7099
+ const failures = readRemediationEvents(this.store, task)
7100
+ .filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed');
7101
+ const terminal = failures.at(-1);
7102
+ if (!terminal)
7103
+ return [];
7104
+ const yields = readTaskHandoffEvents(this.store, task)
7105
+ .filter((event) => matchesTerminalCorrectiveYield(event, terminal, task.id));
7106
+ // Missing native boundary must remain visibly blocked, never fall through to task run.
7107
+ if (yields.length > 1)
7108
+ return [];
7109
+ return [assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead: terminal.failureEvidence.completionCommit })];
7110
+ })
7883
7111
  .filter((assessment) => assessment.applicable);
7884
7112
  const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
7885
7113
  if (eligible.length === 1)
@@ -7888,6 +7116,7 @@ export class WorkflowService {
7888
7116
  applicable: candidates.length > 0,
7889
7117
  eligible: false,
7890
7118
  compatibility: null,
7119
+ ...(candidates.length === 1 && candidates[0].requiredActor ? { requiredActor: candidates[0].requiredActor } : {}),
7891
7120
  blockers: [...new Set([
7892
7121
  ...candidates.flatMap((assessment) => assessment.blockers),
7893
7122
  ...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
@@ -8863,7 +8092,6 @@ function writerTokenContractForAction(action, claimedC1Posture = false, existing
8863
8092
  'task review-launch',
8864
8093
  'task review-sealed-record',
8865
8094
  'task corrective-decision-recover',
8866
- 'task remediation-mode-recover',
8867
8095
  'task plan-integrity-recover',
8868
8096
  'task corrective-decision',
8869
8097
  'task corrective-yield',