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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +98 -34
  3. package/dist/src/alpha6/adoption.d.ts +3 -1
  4. package/dist/src/alpha6/adoption.js +48 -8
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +53 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  19. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  20. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  21. package/dist/src/alpha6/milestone.d.ts +2 -0
  22. package/dist/src/alpha6/milestone.js +10 -1
  23. package/dist/src/alpha6/milestone.js.map +1 -1
  24. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  25. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  26. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  27. package/dist/src/alpha6/plan-integrity.js +7 -5
  28. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  29. package/dist/src/alpha6/remediation.d.ts +8 -21
  30. package/dist/src/alpha6/remediation.js +225 -474
  31. package/dist/src/alpha6/remediation.js.map +1 -1
  32. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  33. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  34. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  35. package/dist/src/alpha7/autonomy.d.ts +9 -0
  36. package/dist/src/alpha7/autonomy.js +40 -1
  37. package/dist/src/alpha7/autonomy.js.map +1 -1
  38. package/dist/src/beta1/project-transaction.d.ts +3 -1
  39. package/dist/src/beta1/project-transaction.js +20 -6
  40. package/dist/src/beta1/project-transaction.js.map +1 -1
  41. package/dist/src/checks/runner.d.ts +26 -0
  42. package/dist/src/checks/runner.js +223 -0
  43. package/dist/src/checks/runner.js.map +1 -0
  44. package/dist/src/checks/task-sync.d.ts +10 -0
  45. package/dist/src/checks/task-sync.js +41 -0
  46. package/dist/src/checks/task-sync.js.map +1 -0
  47. package/dist/src/checks/worker.d.ts +1 -0
  48. package/dist/src/checks/worker.js +154 -0
  49. package/dist/src/checks/worker.js.map +1 -0
  50. package/dist/src/cli-actions.d.ts +3 -3
  51. package/dist/src/cli-actions.js +8 -15
  52. package/dist/src/cli-actions.js.map +1 -1
  53. package/dist/src/cli.js +43 -97
  54. package/dist/src/cli.js.map +1 -1
  55. package/dist/src/contracts.d.ts +54 -34
  56. package/dist/src/delegation.d.ts +5 -1
  57. package/dist/src/delegation.js +4 -0
  58. package/dist/src/delegation.js.map +1 -1
  59. package/dist/src/dependency-provenance.d.ts +2 -2
  60. package/dist/src/dependency-provenance.js +12 -69
  61. package/dist/src/dependency-provenance.js.map +1 -1
  62. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  63. package/dist/src/domain/base-sync-conflict.js +32 -0
  64. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  65. package/dist/src/domain/step-start-admission.d.ts +4 -0
  66. package/dist/src/domain/step-start-admission.js +9 -0
  67. package/dist/src/domain/step-start-admission.js.map +1 -0
  68. package/dist/src/domain/virgin-registration.d.ts +3 -0
  69. package/dist/src/domain/virgin-registration.js +65 -0
  70. package/dist/src/domain/virgin-registration.js.map +1 -0
  71. package/dist/src/errors.d.ts +1 -1
  72. package/dist/src/errors.js.map +1 -1
  73. package/dist/src/failed-checkpoint-update.d.ts +69 -0
  74. package/dist/src/failed-checkpoint-update.js +171 -0
  75. package/dist/src/failed-checkpoint-update.js.map +1 -0
  76. package/dist/src/gateway-handshake.js +0 -8
  77. package/dist/src/gateway-handshake.js.map +1 -1
  78. package/dist/src/git.d.ts +3 -1
  79. package/dist/src/git.js +41 -14
  80. package/dist/src/git.js.map +1 -1
  81. package/dist/src/graph.js +25 -3
  82. package/dist/src/graph.js.map +1 -1
  83. package/dist/src/index.d.ts +1 -0
  84. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +2 -0
  85. package/dist/src/lifecycle/corrective-replan-credential-core.js +45 -9
  86. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -1
  87. package/dist/src/lifecycle/corrective-replan.js +12 -8
  88. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  89. package/dist/src/lifecycle/semantic-registry.js +4 -2
  90. package/dist/src/lifecycle/semantic-registry.js.map +1 -1
  91. package/dist/src/navigation-actions.d.ts +9 -0
  92. package/dist/src/navigation-actions.js +72 -0
  93. package/dist/src/navigation-actions.js.map +1 -0
  94. package/dist/src/navigation-update-artifact.d.ts +5 -0
  95. package/dist/src/navigation-update-artifact.js +227 -0
  96. package/dist/src/navigation-update-artifact.js.map +1 -0
  97. package/dist/src/navigation-update.d.ts +39 -0
  98. package/dist/src/navigation-update.js +82 -0
  99. package/dist/src/navigation-update.js.map +1 -0
  100. package/dist/src/observation.js +52 -29
  101. package/dist/src/observation.js.map +1 -1
  102. package/dist/src/observed-routes.js +10 -6
  103. package/dist/src/observed-routes.js.map +1 -1
  104. package/dist/src/pending-review-update.d.ts +0 -13
  105. package/dist/src/pending-review-update.js +1 -6
  106. package/dist/src/pending-review-update.js.map +1 -1
  107. package/dist/src/reviewer.d.ts +1 -1
  108. package/dist/src/reviewer.js +20 -17
  109. package/dist/src/reviewer.js.map +1 -1
  110. package/dist/src/runtime.d.ts +3 -0
  111. package/dist/src/runtime.js +6 -0
  112. package/dist/src/runtime.js.map +1 -0
  113. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  114. package/dist/src/state/corrective-replan-executor.js +38 -1
  115. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  116. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  117. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  118. package/dist/src/state/corrective-replan-public.d.ts +9 -1
  119. package/dist/src/state/corrective-replan-public.js +50 -8
  120. package/dist/src/state/corrective-replan-public.js.map +1 -1
  121. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  122. package/dist/src/state/corrective-replan-transaction.js +12 -11
  123. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  124. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  125. package/dist/src/state/corrective-yield-transaction.js +11 -10
  126. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  127. package/dist/src/state/lock.d.ts +4 -0
  128. package/dist/src/state/lock.js +21 -0
  129. package/dist/src/state/lock.js.map +1 -1
  130. package/dist/src/version.d.ts +1 -1
  131. package/dist/src/version.js +1 -1
  132. package/dist/src/workflow-blocker-route.d.ts +8 -0
  133. package/dist/src/workflow-blocker-route.js +106 -0
  134. package/dist/src/workflow-blocker-route.js.map +1 -0
  135. package/dist/src/workflow.d.ts +88 -64
  136. package/dist/src/workflow.js +832 -1456
  137. package/dist/src/workflow.js.map +1 -1
  138. package/docs/autonomy-guardrails.md +26 -304
  139. package/docs/decisions.md +14 -112
  140. package/docs/development-flow.md +36 -237
  141. package/docs/project-memory.md +31 -50
  142. package/docs/release-app-evidence.md +190 -0
  143. package/docs/release.md +101 -388
  144. package/docs/updating-existing-project.md +65 -726
  145. package/package.json +11 -13
  146. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  147. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +88 -200
  148. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  149. package/plugins/codex-workflow-gateway/references/protocol.md +51 -458
  150. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +20 -4
  151. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +21 -34
  152. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  153. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +92 -778
  154. package/references/state-machine.md +7 -5
  155. package/roles/delivery-coordinator.md +31 -11
  156. package/roles/scope-lead.md +1 -1
  157. package/roles/technical-planner.md +5 -2
  158. package/schemas/adoption-posture-event.schema.json +11 -0
  159. package/schemas/authorization-event.schema.json +1 -1
  160. package/schemas/corrective-decision-event.schema.json +26 -4
  161. package/schemas/corrective-replan-credentials.private.schema.json +8 -5
  162. package/schemas/corrective-replan-execution-event.private.schema.json +9 -0
  163. package/schemas/corrective-replan-public.schema.json +2 -1
  164. package/schemas/delegation-grant.schema.json +1 -1
  165. package/schemas/project-knowledge-map.schema.json +3 -1
  166. package/schemas/remediation-event.schema.json +10 -1
  167. package/schemas/task.schema.json +64 -2
  168. package/schemas/transition-definition.schema.json +4 -0
  169. package/schemas/transition-payloads.schema.json +16 -1
  170. package/src/alpha6/adoption.ts +1146 -0
  171. package/src/alpha6/captured-check-evidence.ts +132 -0
  172. package/src/alpha6/check-support-anchor.ts +128 -0
  173. package/src/alpha6/component-owner.ts +168 -0
  174. package/src/alpha6/corrective-decision-boundary.ts +45 -0
  175. package/src/alpha6/downstream-proof.ts +520 -0
  176. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  177. package/src/alpha6/handoff.ts +1443 -0
  178. package/src/alpha6/journal.ts +473 -0
  179. package/src/alpha6/literal-test-invocation.ts +84 -0
  180. package/src/alpha6/mechanical-feasibility.ts +488 -0
  181. package/src/alpha6/milestone.ts +2192 -0
  182. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  183. package/src/alpha6/npm-check-contract.ts +47 -0
  184. package/src/alpha6/plan-integrity.ts +298 -0
  185. package/src/alpha6/plan-risk.ts +1480 -0
  186. package/src/alpha6/preexecution-replan.ts +187 -0
  187. package/src/alpha6/remediation-cause.ts +98 -0
  188. package/src/alpha6/remediation.ts +2505 -0
  189. package/src/alpha6/review.ts +1198 -0
  190. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  191. package/src/alpha6/store-sidecars.ts +335 -0
  192. package/src/alpha7/autonomy.ts +449 -0
  193. package/src/alpha7/corrective-recovery.ts +1332 -0
  194. package/src/artifacts.ts +130 -0
  195. package/src/beta1/project-transaction.ts +364 -0
  196. package/src/change-explanation.ts +153 -0
  197. package/src/checks/runner.ts +245 -0
  198. package/src/checks/task-sync.ts +41 -0
  199. package/src/checks/worker.ts +149 -0
  200. package/src/cli-actions.ts +112 -0
  201. package/src/cli.ts +1490 -0
  202. package/src/contracts.ts +1512 -0
  203. package/src/credential-output.ts +89 -0
  204. package/src/credential-transport.ts +215 -0
  205. package/src/delegation.ts +194 -0
  206. package/src/dependency-provenance.ts +472 -0
  207. package/src/diagnostics.ts +93 -0
  208. package/src/domain/base-sync-conflict.ts +35 -0
  209. package/src/domain/completed-step-carryover.ts +76 -0
  210. package/src/domain/discovery.ts +27 -0
  211. package/src/domain/plan-semantics.ts +58 -0
  212. package/src/domain/step-start-admission.ts +10 -0
  213. package/src/domain/validation.ts +177 -0
  214. package/src/domain/virgin-registration.ts +46 -0
  215. package/src/errors.ts +24 -0
  216. package/src/failed-checkpoint-update.ts +203 -0
  217. package/src/fs-utils.ts +61 -0
  218. package/src/gateway-handshake.ts +95 -0
  219. package/src/git.ts +183 -0
  220. package/src/graph.ts +342 -0
  221. package/src/historical-step-provenance.ts +136 -0
  222. package/src/index.ts +23 -0
  223. package/src/lifecycle/canonical-hash.ts +28 -0
  224. package/src/lifecycle/catalog.ts +202 -0
  225. package/src/lifecycle/compiler-inspection.ts +29 -0
  226. package/src/lifecycle/core-static-readiness.ts +132 -0
  227. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  228. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  229. package/src/lifecycle/corrective-replan-credential-core.ts +437 -0
  230. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  231. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  232. package/src/lifecycle/corrective-replan.ts +847 -0
  233. package/src/lifecycle/evaluator.ts +48 -0
  234. package/src/lifecycle/fingerprint.ts +488 -0
  235. package/src/lifecycle/immutable.ts +8 -0
  236. package/src/lifecycle/implementation-table.ts +118 -0
  237. package/src/lifecycle/index.ts +8 -0
  238. package/src/lifecycle/schema-artifact.ts +263 -0
  239. package/src/lifecycle/semantic-registry.ts +574 -0
  240. package/src/lifecycle/types.ts +838 -0
  241. package/src/memory.ts +273 -0
  242. package/src/migration.ts +161 -0
  243. package/src/navigation-actions.ts +70 -0
  244. package/src/navigation-update-artifact.ts +198 -0
  245. package/src/navigation-update.ts +121 -0
  246. package/src/observation.ts +225 -0
  247. package/src/observed-routes.ts +665 -0
  248. package/src/operational-contract.ts +125 -0
  249. package/src/pending-review-update.ts +175 -0
  250. package/src/repository.ts +99 -0
  251. package/src/reviewer.ts +1879 -0
  252. package/src/runtime.ts +6 -0
  253. package/src/state/corrective-replan-executor.ts +819 -0
  254. package/src/state/corrective-replan-public-schema.ts +83 -0
  255. package/src/state/corrective-replan-public.ts +952 -0
  256. package/src/state/corrective-replan-transaction.ts +949 -0
  257. package/src/state/corrective-yield-executor.ts +327 -0
  258. package/src/state/corrective-yield-transaction.ts +730 -0
  259. package/src/state/lock.ts +902 -0
  260. package/src/state/store.ts +567 -0
  261. package/src/transition-core.ts +330 -0
  262. package/src/ulid.ts +24 -0
  263. package/src/version.ts +2 -0
  264. package/src/workflow-blocker-route.ts +109 -0
  265. package/src/workflow.ts +10342 -0
  266. package/docs/alpha7.1-implementation-brief.md +0 -268
  267. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  268. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  269. package/docs/beta1-stabilization-brief.md +0 -165
  270. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  271. package/docs/beta13.2-signal-review-recovery.md +0 -38
  272. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  273. package/docs/change-model.md +0 -118
  274. package/docs/delegated-approval.md +0 -254
  275. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  276. package/docs/pdf/README.md +0 -24
  277. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  278. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  279. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  280. package/docs/pdf/requirements.txt +0 -1
  281. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  282. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  283. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  284. package/docs/pending-review-update.md +0 -15
  285. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  286. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  287. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  288. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  289. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  290. package/docs/problem-briefs/README.md +0 -120
  291. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  292. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  293. package/docs/split-required-recovery.md +0 -47
  294. package/docs/stable-release-defect-register.md +0 -730
  295. package/docs/validation-report.md +0 -182
  296. package/scripts/generate-pdf-docs.py +0 -524
  297. package/scripts/run-pdf-docs.mjs +0 -62
@@ -1,23 +1,22 @@
1
- import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from '../domain/plan-semantics.js';
3
+ import { assertKnowledgeBindingOnlyPlanChange, normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from '../domain/plan-semantics.js';
4
4
  import { pathAllowed } from '../domain/validation.js';
5
5
  import { WorkflowError } from '../errors.js';
6
6
  import { hashCanonical } from '../lifecycle/fingerprint.js';
7
7
  import { createUlid } from '../ulid.js';
8
- import { PACKAGE_VERSION } from '../version.js';
9
8
  import { readPlanRiskAuditEvents } from './plan-risk.js';
10
9
  import { assertHash64, canonicalJsonStringify, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
11
- import { readTaskC1Posture } from './handoff.js';
12
10
  import { buildCurrentStrictStepReviewCycle, latestPassedVerifiedStepReviewEvent, readReviewerAttestations, readStepReviewEvents, } from './review.js';
13
- import { semanticBoundaryHash } from './component-owner.js';
11
+ import { correctiveReplanBoundaryMatchesStep, readCompletedCorrectiveReplanHistory } from '../state/corrective-replan-executor.js';
12
+ import { semanticBoundaryHash, validLiteralTestInvocationEvidence } from './component-owner.js';
14
13
  import { reviewerCauseFingerprint } from './remediation-cause.js';
14
+ import { readFailedStepPlanningRecovery } from './failed-step-planning-recovery.js';
15
15
  export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
16
16
  export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
17
17
  export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
18
18
  export const REMEDIATION_MODE_RECOVERIES_SIDECAR = 'remediation-mode-recoveries.jsonl';
19
19
  export const STOP_ESCALATE_OVERRIDES_SIDECAR = 'stop-escalate-overrides.jsonl';
20
- const REMEDIATION_MODE_RECOVERY_LOCK = '.remediation-mode-recovery.lock';
21
20
  const STOP_ESCALATE_OVERRIDE_LOCK = '.stop-escalate-override.lock';
22
21
  const LEGACY_STOP_OVERRIDE_POLICY = 'beta11-attempt4-stop-compatibility-v1';
23
22
  const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
@@ -83,7 +82,7 @@ function validateRemediationHistory(store, task) {
83
82
  const overriddenStopDecisionEventIds = validateStopEscalateOverrideHistory(task, remediationEvents, correctiveDecisionEvents, readRawStopEscalateOverrideEvents(store, task));
84
83
  validateRemediationAttemptOrdinals(remediationEvents, task);
85
84
  validateRemediationReviewReferences(store, task, remediationEvents);
86
- validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
85
+ validateCorrectiveDecisionCoverage(store, task, correctiveDecisionEvents, remediationEvents);
87
86
  const recoveredEventIds = validateRemediationModeRecoveries(store, task, remediationEvents, correctiveDecisionEvents, modeRecoveries);
88
87
  validateHistoricalRemediationModesForAllSteps(store, task, remediationEvents, correctiveDecisionEvents, recoveredEventIds, overriddenStopDecisionEventIds);
89
88
  return {
@@ -139,7 +138,7 @@ export function readStopEscalateOverrideEvents(store, task) {
139
138
  const decisions = readRawCorrectiveDecisionEvents(store, task);
140
139
  const overrides = readRawStopEscalateOverrideEvents(store, task);
141
140
  validateRemediationAttemptOrdinals(remediations, task);
142
- validateCorrectiveDecisionCoverage(task, decisions, remediations);
141
+ validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
143
142
  validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
144
143
  return overrides;
145
144
  }
@@ -148,7 +147,7 @@ export function stopEscalateOverrideDecisionEventIds(store, task) {
148
147
  const decisions = readRawCorrectiveDecisionEvents(store, task);
149
148
  const overrides = readRawStopEscalateOverrideEvents(store, task);
150
149
  validateRemediationAttemptOrdinals(remediations, task);
151
- validateCorrectiveDecisionCoverage(task, decisions, remediations);
150
+ validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
152
151
  return validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
153
152
  }
154
153
  export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null, causeBindings = []) {
@@ -185,7 +184,7 @@ export function prepareRemediationEvent(store, task, stepId, failureKind, failur
185
184
  }
186
185
  const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
187
186
  const cycle = selectCurrentPlanRemediationCycle(store, task, priorEvents, correctiveDecisions);
188
- const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
187
+ const gate = deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
189
188
  recoveredDecisionEventId,
190
189
  reboundStopOverrideDecisionEventId,
191
190
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
@@ -206,7 +205,7 @@ export function prepareRemediationEvent(store, task, stepId, failureKind, failur
206
205
  return { event: reused, postimage: null };
207
206
  }
208
207
  const cycle = selectCurrentPlanRemediationCycle(store, task, existing, correctiveDecisions);
209
- const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
208
+ const gate = deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
210
209
  recoveredDecisionEventId,
211
210
  reboundStopOverrideDecisionEventId,
212
211
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
@@ -253,281 +252,31 @@ function findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId) {
253
252
  }
254
253
  return recoveries[0]?.correctiveDecisionEventId ?? null;
255
254
  }
256
- export function findRemediationModeRecoveryCandidate(store, task, options = {}) {
257
- const raw = readRawRemediationEvents(store, task);
258
- const candidates = raw.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
259
- if (candidates.length === 0)
255
+ /** A first failed execution plus immutable rebound obstruction is planning evidence, not permission to retry. */
256
+ export function firstFailurePlanningObstructionEvidence(store, task, stepId) {
257
+ const failures = readRemediationEvents(store, task).filter(event => event.stepId === stepId);
258
+ if (failures.length !== 1 || task.steps.find(step => step.id === stepId)?.status !== 'failed')
259
+ return null;
260
+ const recovery = readFailedStepPlanningRecovery(store, task);
261
+ if (!recovery || !recovery.current.reviewRequiredStepIds.includes(stepId))
260
262
  return null;
261
- if (candidates.length !== 1) {
262
- throw new WorkflowError('STATE_CORRUPT', 'Multiple malformed remediation modes are not recoverable.', {
263
- taskId: task.id,
264
- eventIds: candidates.map((event) => event.eventId),
265
- });
266
- }
267
- const posture = readTaskC1Posture(store, task);
268
- const lease = readWriterLeaseForModeRecovery(store, task);
269
- const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
270
- return assessRemediationModeRecovery(store, task, candidates[0].stepId, actor, lease?.token ?? null, options);
271
- }
272
- export function assessRemediationModeRecovery(store, task, stepId, actor, writerToken, options = {}) {
273
- const normalizedActor = actor.trim();
274
- if (!normalizedActor)
275
- throw new WorkflowError('INVALID_ARGUMENT', 'Remediation mode recovery actor is required.');
276
- const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
277
- if (!step)
278
- throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
279
- if (task.status !== 'needs_fix' || step.status !== 'failed') {
280
- throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the exact failed Task/Step posture.', {
281
- taskId: task.id,
282
- taskStatus: task.status,
283
- stepId,
284
- stepStatus: step.status,
285
- });
286
- }
287
- if (!task.planHash)
288
- throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
289
- const remediations = readRawRemediationEvents(store, task);
290
- const decisions = readRawCorrectiveDecisionEvents(store, task);
291
- validateRemediationAttemptOrdinals(remediations, task);
292
- validateRemediationReviewReferences(store, task, remediations);
293
- validateCorrectiveDecisionCoverage(task, decisions, remediations);
294
- const malformed = remediations.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
295
- if (malformed.length !== 1 || malformed[0].stepId !== stepId || malformed[0].attemptOrdinal !== 3) {
296
- throw new WorkflowError('STATE_CORRUPT', 'State does not contain the one exact alpha.7.2 remediation mode omission.', {
297
- taskId: task.id,
298
- stepId,
299
- malformedEventIds: malformed.map((event) => event.eventId),
300
- });
301
- }
302
- const stepRemediations = remediations.filter((event) => event.stepId === stepId);
303
- if (stepRemediations.length !== 3
304
- || stepRemediations[0].mode !== 'ordinary'
305
- || stepRemediations[1].mode !== 'ordinary'
306
- || stepRemediations[2].eventId !== malformed[0].eventId) {
307
- throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires exactly two ordinary attempts and one malformed third attempt.', {
308
- taskId: task.id,
309
- stepId,
310
- });
311
- }
312
- const decisionMatches = decisions.filter((event) => event.stepId === stepId
313
- && event.triggeringAttemptCount === 3
314
- && event.decision === 'continue-fix'
315
- && sameStringArray(event.coveredEventIds, stepRemediations.slice(0, 2).map((event) => event.eventId)));
316
- if (decisionMatches.length !== 1) {
317
- throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact continue-fix decision.', {
318
- taskId: task.id,
319
- stepId,
320
- decisionEventIds: decisionMatches.map((event) => event.eventId),
321
- });
322
- }
323
- const decision = decisionMatches[0];
324
- const correctiveRecoveries = readRawCorrectiveDecisionRecoveries(store, task).filter((event) => event.stepId === stepId
325
- && event.triggeringAttemptCount === 3
326
- && event.correctiveDecisionEventId === decision.eventId
327
- && event.correctiveDecisionEventHash === decision.eventHash
328
- && event.reboundPlanHash === task.planHash);
329
- if (correctiveRecoveries.length !== 1) {
330
- throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact corrective-decision recovery.', {
331
- taskId: task.id,
332
- stepId,
333
- recoveryEventIds: correctiveRecoveries.map((event) => event.eventId),
334
- });
335
- }
336
- const correctiveRecovery = correctiveRecoveries[0];
337
- const authority = options.authorityMode === 'observe'
338
- ? observeRemediationModeRecoveryAuthority(store, task)
339
- : requireRemediationModeRecoveryAuthority(store, task, normalizedActor, writerToken);
340
- const existing = readRemediationModeRecoveryEvents(store, task).filter((event) => event.remediationEventId === malformed[0].eventId);
341
- if (existing.length > 1) {
342
- throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation mode recoveries target one event.', {
343
- taskId: task.id,
344
- remediationEventId: malformed[0].eventId,
345
- });
346
- }
347
- if (existing[0]) {
348
- validateRemediationModeRecoveryBinding(existing[0], malformed[0], decision, correctiveRecovery);
349
- const retryMismatches = [
350
- existing[0].taskRevision !== task.revision ? 'taskRevision' : null,
351
- existing[0].planHash !== task.planHash ? 'planHash' : null,
352
- existing[0].actor !== normalizedActor ? 'actor' : null,
353
- existing[0].c1ClaimEventHash !== authority.c1ClaimEventHash ? 'c1ClaimEventHash' : null,
354
- existing[0].writerLeaseTokenHash !== authority.writerLeaseTokenHash ? 'writerLeaseTokenHash' : null,
355
- ].filter((value) => value !== null);
356
- if (retryMismatches.length > 0) {
357
- throw new WorkflowError('STATE_CONFLICT', 'Existing remediation mode recovery conflicts with the requested retry binding.', {
358
- taskId: task.id,
359
- stepId,
360
- recoveryEventId: existing[0].eventId,
361
- mismatches: retryMismatches,
362
- });
363
- }
364
- }
365
- const evidenceFingerprint = sha256Hex(canonicalJsonStringify({
366
- taskId: task.id,
367
- taskRevision: task.revision,
368
- stepId,
369
- planHash: task.planHash,
370
- remediationEventHash: malformed[0].eventHash,
371
- correctiveDecisionEventHash: decision.eventHash,
372
- correctiveDecisionRecoveryEventHash: correctiveRecovery.eventHash,
373
- actor: normalizedActor,
374
- c1ClaimEventHash: authority.c1ClaimEventHash,
375
- writerLeaseTokenHash: authority.writerLeaseTokenHash,
376
- }));
377
- return {
378
- task,
379
- remediation: malformed[0],
380
- decision,
381
- correctiveRecovery,
382
- existingRecovery: existing[0] ?? null,
383
- actor: normalizedActor,
384
- c1ClaimEventHash: authority.c1ClaimEventHash,
385
- writerLeaseTokenHash: authority.writerLeaseTokenHash,
386
- evidenceFingerprint,
387
- };
388
- }
389
- function observeRemediationModeRecoveryAuthority(store, task) {
390
- const posture = readTaskC1Posture(store, task);
391
- const lease = readWriterLeaseForModeRecovery(store, task);
392
- return {
393
- c1ClaimEventHash: posture.state === 'none' ? null : posture.latestEvent.eventHash,
394
- writerLeaseTokenHash: lease ? sha256Hex(lease.token) : null,
395
- };
396
- }
397
- export function appendRemediationModeRecovery(store, task, stepId, expectedRevision, actor, writerToken, now) {
398
- if (task.revision !== expectedRevision) {
399
- throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
400
- expectedRevision,
401
- actualRevision: task.revision,
402
- });
403
- }
404
- const preflight = assessRemediationModeRecovery(store, task, stepId, actor, writerToken);
405
- const root = store.taskRoot(task.projectId, task.id);
406
- const lockFile = path.join(root, REMEDIATION_MODE_RECOVERY_LOCK);
407
- let descriptor;
408
- try {
409
- descriptor = openSync(lockFile, 'wx', 0o600);
410
- closeSync(descriptor);
411
- }
412
- catch (error) {
413
- if (existsSync(lockFile)) {
414
- throw new WorkflowError('LOCKED', `Remediation mode recovery for ${task.id} is already being serialized.`, {
415
- taskId: task.id,
416
- stepId,
417
- });
418
- }
419
- throw error;
420
- }
421
- try {
422
- const currentTask = store.readTask(task.projectId, task.id);
423
- if (currentTask.revision !== expectedRevision) {
424
- throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
425
- expectedRevision,
426
- actualRevision: currentTask.revision,
427
- });
428
- }
429
- let underLock;
430
- try {
431
- underLock = assessRemediationModeRecovery(store, currentTask, stepId, actor, writerToken);
432
- }
433
- catch (error) {
434
- throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
435
- taskId: task.id,
436
- stepId,
437
- cause: error instanceof Error ? error.message : String(error),
438
- });
439
- }
440
- if (underLock.existingRecovery)
441
- return underLock.existingRecovery;
442
- if (underLock.evidenceFingerprint !== preflight.evidenceFingerprint) {
443
- throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
444
- taskId: task.id,
445
- stepId,
446
- });
447
- }
448
- return store.appendSidecarEvent(root, REMEDIATION_MODE_RECOVERIES_SIDECAR, {
449
- eventId: `RMR-${createUlid(now.getTime())}`,
450
- recordedAt: now.toISOString(),
451
- taskId: task.id,
452
- stepId,
453
- taskRevision: expectedRevision,
454
- remediationEventId: underLock.remediation.eventId,
455
- remediationEventHash: underLock.remediation.eventHash,
456
- correctiveDecisionEventId: underLock.decision.eventId,
457
- correctiveDecisionEventHash: underLock.decision.eventHash,
458
- correctiveDecisionRecoveryEventId: underLock.correctiveRecovery.eventId,
459
- correctiveDecisionRecoveryEventHash: underLock.correctiveRecovery.eventHash,
460
- attemptOrdinal: 3,
461
- planHash: currentTask.planHash,
462
- actor: underLock.actor,
463
- c1ClaimEventHash: underLock.c1ClaimEventHash,
464
- writerLeaseTokenHash: underLock.writerLeaseTokenHash,
465
- fromMode: 'ordinary',
466
- toMode: 'corrective',
467
- reason: 'alpha7.2-recovered-continue-fix-mode-omission',
468
- }, {
469
- validateEvent: (event, context) => validateStoredRemediationModeRecovery(event, currentTask, context.file, context.line),
470
- });
471
- }
472
- finally {
473
- if (existsSync(lockFile))
474
- unlinkSync(lockFile);
475
- }
476
- }
477
- function requireRemediationModeRecoveryAuthority(store, task, actor, writerToken) {
478
- const posture = readTaskC1Posture(store, task);
479
- const lease = readWriterLeaseForModeRecovery(store, task);
480
- if (posture.state === 'pending') {
481
- throw new WorkflowError('TRANSITION_BLOCKED', 'Pending C1 handoff must be claimed before remediation mode recovery.');
482
- }
483
- if (posture.state === 'claimed') {
484
- const tokenHash = writerToken ? sha256Hex(writerToken) : null;
485
- if (!lease
486
- || actor !== posture.claimant
487
- || lease.owner !== posture.claimant
488
- || !writerToken
489
- || lease.token !== writerToken
490
- || tokenHash !== posture.writerLeaseTokenHash) {
491
- throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the claimed C1 actor and exact writer token.', {
492
- taskId: task.id,
493
- claimant: posture.claimant,
494
- leaseOwner: lease?.owner ?? null,
495
- });
496
- }
497
- return { c1ClaimEventHash: posture.latestEvent.eventHash, writerLeaseTokenHash: tokenHash };
498
- }
499
- if (lease && (lease.owner !== actor || !writerToken || lease.token !== writerToken)) {
500
- throw new WorkflowError('TRANSITION_BLOCKED', 'Existing writer lease is not owned by the remediation recovery actor/token.');
501
- }
502
- if (!lease && writerToken) {
503
- throw new WorkflowError('TRANSITION_BLOCKED', 'A writer token was provided without an existing writer lease.');
504
- }
505
263
  return {
506
- c1ClaimEventHash: posture.latestEvent?.eventHash ?? null,
507
- writerLeaseTokenHash: lease ? sha256Hex(lease.token) : null,
264
+ kind: 'first-failure-rebound-planning-obstruction', taskRevision: task.revision,
265
+ sourcePlanHash: task.planHash, baselinePlanHash: recovery.baseline.planHash,
266
+ baselineAuditEventId: recovery.baseline.eventId, baselineAuditEventHash: recovery.baseline.eventHash,
267
+ obstructionAuditEventId: recovery.current.audit.eventId, obstructionAuditEventHash: recovery.current.audit.eventHash,
268
+ priorAuthorizationHash: hashCanonical(recovery.priorAuthorization),
269
+ remediationEventId: failures[0].eventId, remediationEventHash: failures[0].eventHash,
270
+ knowledgeRebindCount: task.knowledgeRebinds.length, knowledgeRebindHash: hashCanonical(task.knowledgeRebinds),
508
271
  };
509
272
  }
510
- function readWriterLeaseForModeRecovery(store, task) {
511
- const file = path.join(store.lockRoot(task.projectId), `${task.id}.json`);
512
- if (!existsSync(file))
513
- return null;
514
- let lease;
515
- try {
516
- lease = JSON.parse(readFileSync(file, 'utf8'));
517
- }
518
- catch {
519
- throw new WorkflowError('STATE_CORRUPT', `Writer lease is unreadable: ${file}`);
520
- }
521
- if (lease.entityId !== task.id || typeof lease.owner !== 'string' || typeof lease.token !== 'string') {
522
- throw new WorkflowError('STATE_CORRUPT', `Writer lease does not match Task ${task.id}.`);
523
- }
524
- return lease;
525
- }
526
273
  export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAudit, planRiskAudit, planHash, now) {
527
274
  validateCorrectiveDecisionAudit(correctiveAudit);
528
275
  const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
529
276
  const triggeringAttemptCount = remediationEvents.length + 1;
530
- if (triggeringAttemptCount < 3) {
277
+ const planningObstructionEvidence = triggeringAttemptCount === 2 && correctiveAudit.decision === 'replan-required'
278
+ ? firstFailurePlanningObstructionEvidence(store, task, stepId) : null;
279
+ if (triggeringAttemptCount < 3 && !planningObstructionEvidence) {
531
280
  throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision is not available for ${stepId} before two guarded remediation failures exist.`, {
532
281
  taskId: task.id,
533
282
  stepId,
@@ -581,6 +330,7 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
581
330
  stepId,
582
331
  triggeringAttemptCount,
583
332
  decision: correctiveAudit.decision,
333
+ ...(planningObstructionEvidence ? { recoveryBasis: 'planning-obstruction', planningObstructionEvidence } : {}),
584
334
  auditor: correctiveAudit.auditor.trim(),
585
335
  coveredFindingIds,
586
336
  coveredEventIds,
@@ -589,176 +339,6 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
589
339
  validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
590
340
  });
591
341
  }
592
- export function prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit) {
593
- if (task.revision !== expectedRevision) {
594
- throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
595
- expectedRevision,
596
- actualRevision: task.revision,
597
- });
598
- }
599
- const normalizedActor = actor.trim();
600
- const normalizedReason = reason.trim();
601
- if (!normalizedActor)
602
- throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a human actor.');
603
- if (!normalizedReason)
604
- throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a reason.');
605
- if (!COMMIT_SHA_PATTERN.test(headCommit)) {
606
- throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires the exact current Git HEAD.');
607
- }
608
- if (task.status !== 'needs_fix') {
609
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a needs_fix Task.', {
610
- taskId: task.id,
611
- taskStatus: task.status,
612
- });
613
- }
614
- const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
615
- if (!step)
616
- throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
617
- if (step.status !== 'failed') {
618
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires the exact failed Step.', {
619
- taskId: task.id,
620
- stepId,
621
- stepStatus: step.status,
622
- });
623
- }
624
- if (!task.planHash)
625
- throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
626
- const history = readOpenGuardedRemediationHistory(store, task, stepId);
627
- const remediations = history.remediationEvents;
628
- if (remediations.length !== 3
629
- || remediations[0]?.attemptOrdinal !== 1
630
- || remediations[0]?.mode !== 'ordinary'
631
- || remediations[1]?.attemptOrdinal !== 2
632
- || remediations[1]?.mode !== 'ordinary'
633
- || remediations[2]?.attemptOrdinal !== 3
634
- || remediations[2]?.mode !== 'corrective') {
635
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override is limited to the exact beta.11 three-failure history.', {
636
- taskId: task.id,
637
- stepId,
638
- attempts: remediations.map((event) => ({ eventId: event.eventId, ordinal: event.attemptOrdinal, mode: event.mode })),
639
- });
640
- }
641
- const priorContinueMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 3
642
- && event.decision === 'continue-fix'
643
- && event.planHash === task.planHash
644
- && sameStringArray(event.coveredEventIds, remediations.slice(0, 2).map((candidate) => candidate.eventId)));
645
- const stopMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 4
646
- && event.decision === 'stop-escalate'
647
- && event.planHash === task.planHash
648
- && sameStringArray(event.coveredEventIds, remediations.map((candidate) => candidate.eventId)));
649
- if (priorContinueMatches.length !== 1 || stopMatches.length !== 1) {
650
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires one exact attempt-3 continue decision and one exact attempt-4 stop decision.', {
651
- taskId: task.id,
652
- stepId,
653
- priorContinueDecisionEventIds: priorContinueMatches.map((event) => event.eventId),
654
- stopDecisionEventIds: stopMatches.map((event) => event.eventId),
655
- });
656
- }
657
- const priorContinue = priorContinueMatches[0];
658
- const stopDecision = stopMatches[0];
659
- const binding = {
660
- taskId: task.id,
661
- taskRevision: task.revision,
662
- stepId,
663
- stopDecisionEventId: stopDecision.eventId,
664
- stopDecisionEventHash: stopDecision.eventHash,
665
- priorContinueDecisionEventId: priorContinue.eventId,
666
- priorContinueDecisionEventHash: priorContinue.eventHash,
667
- triggeringAttemptCount: 4,
668
- coveredEventIds: remediations.map((event) => event.eventId),
669
- planHash: task.planHash,
670
- headCommit,
671
- actor: normalizedActor,
672
- reason: normalizedReason,
673
- packageVersion: PACKAGE_VERSION,
674
- policy: LEGACY_STOP_OVERRIDE_POLICY,
675
- };
676
- const confirmationCodeBindingHash = stopEscalateOverrideBindingHash(binding);
677
- const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === stopDecision.eventId) ?? null;
678
- if (existing && existing.confirmationCodeBindingHash !== confirmationCodeBindingHash) {
679
- throw new WorkflowError('STATE_CONFLICT', 'The stop decision already has an override with a different human binding.', {
680
- taskId: task.id,
681
- stepId,
682
- overrideEventId: existing.eventId,
683
- });
684
- }
685
- return {
686
- eligible: true,
687
- action: 'task stop-override-apply',
688
- ...binding,
689
- confirmationCodeBindingHash,
690
- confirmationCode: `SOO-${confirmationCodeBindingHash.slice(0, 24).toUpperCase()}`,
691
- appendOnly: true,
692
- preservesOriginalDecision: true,
693
- delegatedApprovalAllowed: false,
694
- };
695
- }
696
- export function appendStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit, confirmationCode, now) {
697
- const prepared = prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit);
698
- if (confirmationCode.trim() !== prepared.confirmationCode) {
699
- throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override confirmation code does not match the current state binding.', {
700
- taskId: task.id,
701
- stepId,
702
- confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
703
- });
704
- }
705
- const root = store.taskRoot(task.projectId, task.id);
706
- const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === prepared.stopDecisionEventId) ?? null;
707
- if (existing)
708
- return existing;
709
- const lockFile = path.join(root, STOP_ESCALATE_OVERRIDE_LOCK);
710
- let descriptor;
711
- try {
712
- descriptor = openSync(lockFile, 'wx', 0o600);
713
- closeSync(descriptor);
714
- }
715
- catch (error) {
716
- if (existsSync(lockFile)) {
717
- throw new WorkflowError('LOCKED', `Stop override for ${task.id} is already being serialized.`, {
718
- taskId: task.id,
719
- stepId,
720
- });
721
- }
722
- throw error;
723
- }
724
- try {
725
- const currentTask = store.readTask(task.projectId, task.id);
726
- const underLock = prepareStopEscalateOverride(store, currentTask, stepId, expectedRevision, actor, reason, headCommit);
727
- if (underLock.confirmationCode !== prepared.confirmationCode
728
- || underLock.confirmationCodeBindingHash !== prepared.confirmationCodeBindingHash) {
729
- throw new WorkflowError('STATE_CONFLICT', 'Stop override evidence changed after preparation.', {
730
- taskId: task.id,
731
- stepId,
732
- });
733
- }
734
- return store.appendSidecarEvent(root, STOP_ESCALATE_OVERRIDES_SIDECAR, {
735
- eventId: `SOO-${createUlid(now.getTime())}`,
736
- recordedAt: now.toISOString(),
737
- taskId: underLock.taskId,
738
- taskRevision: underLock.taskRevision,
739
- stepId: underLock.stepId,
740
- stopDecisionEventId: underLock.stopDecisionEventId,
741
- stopDecisionEventHash: underLock.stopDecisionEventHash,
742
- priorContinueDecisionEventId: underLock.priorContinueDecisionEventId,
743
- priorContinueDecisionEventHash: underLock.priorContinueDecisionEventHash,
744
- triggeringAttemptCount: 4,
745
- coveredEventIds: underLock.coveredEventIds,
746
- planHash: underLock.planHash,
747
- headCommit: underLock.headCommit,
748
- actor: underLock.actor,
749
- reason: underLock.reason,
750
- packageVersion: underLock.packageVersion,
751
- confirmationCodeBindingHash: underLock.confirmationCodeBindingHash,
752
- policy: LEGACY_STOP_OVERRIDE_POLICY,
753
- }, {
754
- validateEvent: (event, context) => validateStoredStopEscalateOverride(event, currentTask, context.file, context.line),
755
- });
756
- }
757
- finally {
758
- if (existsSync(lockFile))
759
- unlinkSync(lockFile);
760
- }
761
- }
762
342
  export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor, evidence, now) {
763
343
  if (!task.planHash) {
764
344
  throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no Plan hash for Plan-integrity recovery.`);
@@ -766,7 +346,11 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
766
346
  if (!auditor.trim()) {
767
347
  throw new WorkflowError('INVALID_ARGUMENT', 'Plan-integrity recovery requires a derived auditor identity.');
768
348
  }
769
- const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
349
+ const allRemediationEvents = readRemediationEvents(store, task).filter(event => event.stepId === stepId);
350
+ const support = evidence.kind === 'check-support-anchor-outside-allowed-writes' || evidence.kind === 'failed-check-component-owner-outside-allowed-writes';
351
+ const remediationEvents = support ? selectPlanIntegrityRemediations(store, task, allRemediationEvents, readCorrectiveDecisionEvents(store, task).filter(event => event.stepId === stepId)) : allRemediationEvents;
352
+ if (!remediationEvents)
353
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Mechanical recovery lacks a verified current Plan cycle.');
770
354
  const expectedBindings = evidence.kind === 'missing-npm-script-outside-allowed-writes'
771
355
  ? [{
772
356
  eventId: evidence.remediationEventId,
@@ -777,8 +361,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
777
361
  if (remediationEvents.length !== expectedBindings.length
778
362
  || remediationEvents.some((event, index) => {
779
363
  const binding = expectedBindings[index];
780
- return event.attemptOrdinal !== index + 1
781
- || event.failureKind !== 'checks-failed'
364
+ return event.failureKind !== 'checks-failed'
782
365
  || event.eventId !== binding?.eventId
783
366
  || event.eventHash !== binding.eventHash
784
367
  || event.attemptOrdinal !== binding.attemptOrdinal;
@@ -789,7 +372,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
789
372
  remediationEventIds: remediationEvents.map((event) => event.eventId),
790
373
  });
791
374
  }
792
- const triggeringAttemptCount = remediationEvents.length + 1;
375
+ const triggeringAttemptCount = allRemediationEvents.length + 1;
793
376
  const recoveryDecision = evidence.kind === 'check-support-anchor-outside-allowed-writes'
794
377
  || evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
795
378
  ? 'continue-fix'
@@ -826,7 +409,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
826
409
  decision: recoveryDecision,
827
410
  auditor: auditor.trim(),
828
411
  coveredFindingIds: [findingId],
829
- coveredEventIds: remediationEvents.map((event) => event.eventId),
412
+ coveredEventIds: allRemediationEvents.map((event) => event.eventId),
830
413
  planHash: task.planHash,
831
414
  recoveryBasis: 'plan-integrity',
832
415
  planIntegrityEvidence: evidence,
@@ -836,7 +419,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
836
419
  }
837
420
  export function assertGuardedRemediationAttemptAllowed(store, task, stepId, planRiskAudit, recoveredDecisionEventId = null, reboundStopOverrideDecisionEventId = null) {
838
421
  const history = readOpenGuardedRemediationHistory(store, task, stepId);
839
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
422
+ return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
840
423
  enforceRuntimeGate: true,
841
424
  recoveredDecisionEventId,
842
425
  reboundStopOverrideDecisionEventId,
@@ -861,12 +444,17 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
861
444
  ? findCurrentCorrectiveDecision(task, stepId, 2, history.correctiveDecisions)
862
445
  : null;
863
446
  const currentDecision = findCurrentCorrectiveDecision(task, stepId, history.nextAttemptOrdinal, history.correctiveDecisions);
447
+ if (remediationEvents.length === 1 && currentDecision === null
448
+ && firstFailurePlanningObstructionEvidence(store, task, stepId)) {
449
+ return { stepId, guardedCategory: requireGuardedCategory(planRiskAudit, stepId), attemptCount: 1,
450
+ nextAttemptOrdinal: history.nextAttemptOrdinal, mode: 'corrective', correctiveDecision: null, posture: 'decision-required' };
451
+ }
864
452
  if (remediationEvents.length < 2 && earlyPlanIntegrityDecision?.recoveryBasis !== 'plan-integrity'
865
453
  && currentDecision === null
866
454
  && deriveCurrentCauseDecision(remediationEvents, history.priorRemediationEvents) !== 'root-cause-replan')
867
455
  return null;
868
456
  if (step.status === 'failed' || step.status === 'planned') {
869
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
457
+ return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
870
458
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
871
459
  priorRemediationEvents: history.priorRemediationEvents,
872
460
  nextAttemptOrdinal: history.nextAttemptOrdinal,
@@ -877,7 +465,7 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
877
465
  const currentCycle = buildCurrentStrictStepReviewCycle(store, task, stepId);
878
466
  if (currentCycle.resolution !== 'unverified')
879
467
  return null;
880
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
468
+ return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
881
469
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
882
470
  priorRemediationEvents: history.priorRemediationEvents,
883
471
  nextAttemptOrdinal: history.nextAttemptOrdinal,
@@ -898,7 +486,7 @@ function readOpenGuardedRemediationHistory(store, task, stepId) {
898
486
  nextAttemptOrdinal: allStepRemediations.length + 1,
899
487
  };
900
488
  }
901
- function selectCurrentPlanRemediationCycle(store, task, remediationEvents, correctiveDecisions) {
489
+ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, correctiveDecisions, canonicalPlanHash = task.planHash) {
902
490
  const unchanged = () => ({
903
491
  remediationEvents: [...remediationEvents],
904
492
  priorRemediationEvents: [],
@@ -916,13 +504,13 @@ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, corre
916
504
  const taskRoot = store.taskRoot(task.projectId, task.id);
917
505
  let currentSemanticHash;
918
506
  try {
919
- currentSemanticHash = normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(taskRoot, task.planHash, task.planHash), 'current').semanticHash;
507
+ currentSemanticHash = normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(taskRoot, task.planHash, canonicalPlanHash), 'current').semanticHash;
920
508
  }
921
509
  catch {
922
510
  return unchanged();
923
511
  }
924
512
  const reviews = readStepReviewEvents(store, task);
925
- const bindings = remediationEvents.map((event) => resolveRemediationPlanCycleBinding(taskRoot, task.planHash, event, reviews, audits));
513
+ const bindings = remediationEvents.map((event) => resolveRemediationPlanCycleBinding(taskRoot, canonicalPlanHash, event, reviews, audits));
926
514
  if (bindings.some((binding) => binding === null))
927
515
  return unchanged();
928
516
  let cycleStart = remediationEvents.length;
@@ -939,6 +527,71 @@ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, corre
939
527
  correctiveDecisions: [...correctiveDecisions],
940
528
  };
941
529
  }
530
+ /** Scope mechanical support evidence only; global corrective authority is never discarded. */
531
+ export function selectPlanIntegrityRemediations(store, task, events, decisions, canonicalPlanHash = task.planHash) {
532
+ if (events.length === 0)
533
+ return null;
534
+ const stepId = events[0].stepId;
535
+ if (events.some(event => event.stepId !== stepId) || decisions.some(event => event.stepId !== stepId))
536
+ return null;
537
+ const cycle = selectCurrentPlanRemediationCycle(store, task, events, decisions, canonicalPlanHash);
538
+ if (cycle.priorRemediationEvents.length === 0)
539
+ return decisions.length === 0 ? [...events] : null;
540
+ if (!task.planHash || cycle.remediationEvents.length === 0
541
+ || decisions.some(d => d.decision === 'stop-escalate' || d.decision === 'split-required'))
542
+ return null;
543
+ const root = store.taskRoot(task.projectId, task.id);
544
+ const history = readCompletedCorrectiveReplanHistory(store, task);
545
+ const semantic = (hash) => {
546
+ const saved = history.flatMap(boundary => [boundary.beforePlan, boundary.afterPlan]).find(bytes => sha256Hex(bytes) === hash);
547
+ return normalizePlanKnowledgeBinding(saved ?? readVerifiedPlanArtifact(root, hash, canonicalPlanHash), 'cycle').semanticHash;
548
+ };
549
+ const currentSemantic = semantic(task.planHash);
550
+ const audits = readPlanRiskAuditEvents(store, task), reviews = readStepReviewEvents(store, task);
551
+ const priorBindings = cycle.priorRemediationEvents.map(event => resolveRemediationPlanCycleBinding(root, canonicalPlanHash, event, reviews, audits));
552
+ const currentBindings = cycle.remediationEvents.map(event => resolveRemediationPlanCycleBinding(root, canonicalPlanHash, event, reviews, audits));
553
+ if (priorBindings.some(b => !b) || currentBindings.some(b => !b))
554
+ return null;
555
+ const boundaries = history.filter(boundary => correctiveReplanBoundaryMatchesStep(boundary.before, stepId)
556
+ && boundary.after.planHash && boundary.before.planHash
557
+ && semantic(boundary.after.planHash) === currentSemantic && semantic(boundary.before.planHash) !== currentSemantic
558
+ && boundary.after.revision <= task.revision
559
+ && priorBindings.every(b => b.sourceTaskRevision < boundary.after.revision)
560
+ && currentBindings.every(b => b.sourceTaskRevision > boundary.after.revision));
561
+ if (boundaries.length !== 1)
562
+ return null;
563
+ const boundary = boundaries[0];
564
+ let cursor = task.planHash;
565
+ const visited = new Set();
566
+ const planBytes = (hash) => history.flatMap(b => [b.beforePlan, b.afterPlan]).find(bytes => sha256Hex(bytes) === hash)
567
+ ?? readVerifiedPlanArtifact(root, hash, canonicalPlanHash);
568
+ while (cursor !== boundary.after.planHash) {
569
+ if (visited.has(cursor))
570
+ return null;
571
+ visited.add(cursor);
572
+ const links = task.knowledgeRebinds.filter(link => link.reboundPlanHash === cursor);
573
+ if (links.length !== 1 || !links[0].previousPlanHash)
574
+ return null;
575
+ const link = links[0];
576
+ assertKnowledgeBindingOnlyPlanChange(planBytes(link.previousPlanHash), planBytes(cursor));
577
+ if (!planBytes(link.previousPlanHash).includes(`- Knowledge map: revision ${link.fromKnowledgeMapRevision}, hash ${link.fromKnowledgeMapHash}\n`)
578
+ || !planBytes(cursor).includes(`- Knowledge map: revision ${link.toKnowledgeMapRevision}, hash ${link.toKnowledgeMapHash}\n`))
579
+ return null;
580
+ cursor = link.previousPlanHash;
581
+ }
582
+ if (!task.authorizations.some(a => a.kind === 'execution' && ['approved', 'superseded'].includes(a.decision)
583
+ && a.planHash === task.planHash && a.briefHash === task.briefHash
584
+ && a.recordedAt >= boundary.admittedAt))
585
+ return null;
586
+ if (decisions.length === 0 || decisions.some(d => d.recordedAt > boundary.admittedAt
587
+ || semantic(d.planHash) === currentSemantic))
588
+ return null;
589
+ const last = decisions.at(-1);
590
+ if (last.decision !== 'replan-required' || last.triggeringAttemptCount !== cycle.priorRemediationEvents.length + 1
591
+ || semantic(last.planHash) !== semantic(boundary.before.planHash))
592
+ return null;
593
+ return cycle.remediationEvents;
594
+ }
942
595
  function resolveRemediationPlanCycleBinding(taskRoot, currentPlanHash, event, reviews, audits) {
943
596
  let planHash = null;
944
597
  let sourceTaskRevision = null;
@@ -994,16 +647,22 @@ export function findFailedGuardedStepRequiringCorrectiveDecision(store, task, pl
994
647
  ? posture
995
648
  : null;
996
649
  }
997
- function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
650
+ function deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
998
651
  const attemptCount = remediationEvents.length + (options.priorRemediationEvents?.length ?? 0);
999
652
  const nextAttemptOrdinal = options.nextAttemptOrdinal ?? attemptCount + 1;
1000
653
  const guardedCategory = requireGuardedCategory(planRiskAudit, stepId);
1001
654
  const rollingCauseDecision = deriveCurrentCauseDecision(remediationEvents, options.priorRemediationEvents ?? []);
1002
655
  const currentCorrectiveDecision = findCurrentCorrectiveDecision(task, stepId, nextAttemptOrdinal, correctiveDecisions);
656
+ if (currentCorrectiveDecision?.recoveryBasis === 'planning-obstruction') {
657
+ return { stepId, guardedCategory, attemptCount, nextAttemptOrdinal, mode: 'ordinary',
658
+ correctiveDecision: currentCorrectiveDecision,
659
+ posture: currentCorrectiveDecision.planHash === task.planHash ? 'replan-required' : 'replan-ready' };
660
+ }
1003
661
  const planIntegrityDecision = currentCorrectiveDecision;
1004
662
  if (planIntegrityDecision?.recoveryBasis === 'plan-integrity'
1005
- && (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required')) {
1006
- if (planIntegrityDecision.planHash !== task.planHash) {
663
+ && (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required'
664
+ || isKnowledgeReboundSupportDecision(store, task, planIntegrityDecision))) {
665
+ if (planIntegrityDecision.planHash !== task.planHash && planIntegrityDecision.decision === 'replan-required') {
1007
666
  return {
1008
667
  stepId,
1009
668
  guardedCategory,
@@ -1310,6 +969,17 @@ function validateStoredRemediationEvent(event, task, file, line) {
1310
969
  if (hasDirtyWorktreeHash) {
1311
970
  assertHash64(event.failureEvidence.dirtyWorktreeHash, `Remediation dirty-worktree hash at ${file}:${line}`);
1312
971
  }
972
+ const boundary = event.failureEvidence.unclaimedExecutionBoundary;
973
+ if (boundary !== undefined) {
974
+ if (!boundary || typeof boundary !== 'object' || Array.isArray(boundary)
975
+ || Object.keys(boundary).sort().join(',') !== 'actor,kind,priorHandoffEventHash'
976
+ || boundary.kind !== 'native-unclaimed-check-failure'
977
+ || typeof boundary.actor !== 'string' || !boundary.actor.trim() || boundary.actor !== boundary.actor.trim()
978
+ || !(boundary.priorHandoffEventHash === null || typeof boundary.priorHandoffEventHash === 'string' && /^[a-f0-9]{64}$/.test(boundary.priorHandoffEventHash))
979
+ || event.failureKind !== 'checks-failed' || !hasExecutionRevision || !hasDirtyWorktreeHash) {
980
+ throw new WorkflowError('STATE_CORRUPT', 'Invalid native unclaimed execution boundary.', { file, line, eventId: event.eventId });
981
+ }
982
+ }
1313
983
  if (!Number.isInteger(event.attemptOrdinal) || event.attemptOrdinal <= 0) {
1314
984
  throw new WorkflowError('STATE_CORRUPT', 'Remediation event attemptOrdinal must be a positive integer.', {
1315
985
  file,
@@ -1419,7 +1089,21 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
1419
1089
  normalizeStringList(event.coveredFindingIds, 'Corrective decision coveredFindingIds');
1420
1090
  normalizeStringList(event.coveredEventIds, 'Corrective decision coveredEventIds');
1421
1091
  assertHash64(event.planHash, `Corrective decision planHash at ${file}:${line}`);
1422
- if (event.recoveryBasis === 'plan-integrity' || event.planIntegrityEvidence !== undefined) {
1092
+ if (event.recoveryBasis === 'planning-obstruction' || event.planningObstructionEvidence !== undefined) {
1093
+ const evidence = event.planningObstructionEvidence;
1094
+ if (!evidence || event.recoveryBasis !== 'planning-obstruction' || event.planIntegrityEvidence !== undefined
1095
+ || evidence.kind !== 'first-failure-rebound-planning-obstruction' || event.decision !== 'replan-required'
1096
+ || event.triggeringAttemptCount !== 2 || evidence.sourcePlanHash !== event.planHash
1097
+ || !Number.isSafeInteger(evidence.taskRevision) || evidence.taskRevision < 1
1098
+ || !Number.isSafeInteger(evidence.knowledgeRebindCount) || evidence.knowledgeRebindCount < 1
1099
+ || event.coveredEventIds.length !== 1 || event.coveredEventIds[0] !== evidence.remediationEventId) {
1100
+ throw new WorkflowError('STATE_CORRUPT', 'First-failure planning obstruction requires exact bounded proof.', { file, line, taskId: task.id });
1101
+ }
1102
+ for (const key of ['sourcePlanHash', 'baselinePlanHash', 'baselineAuditEventHash', 'obstructionAuditEventHash',
1103
+ 'priorAuthorizationHash', 'remediationEventHash', 'knowledgeRebindHash'])
1104
+ assertHash64(evidence[key], `Planning obstruction ${key}`);
1105
+ }
1106
+ else if (event.recoveryBasis === 'plan-integrity' || event.planIntegrityEvidence !== undefined) {
1423
1107
  validatePlanIntegrityDecision(event, task, file, line);
1424
1108
  }
1425
1109
  else if (event.triggeringAttemptCount === 2) {
@@ -1638,9 +1322,9 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1638
1322
  const validateCurrentPlanBinding = event.planHash === task.planHash;
1639
1323
  if (event.decision !== 'continue-fix'
1640
1324
  || evidence.remediationEvents.length === 0
1641
- || event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
1325
+ || event.triggeringAttemptCount !== evidence.remediationEvents.at(-1).attemptOrdinal + 1
1642
1326
  || evidence.remediationEvents.some((binding, index) => {
1643
- if (!binding.eventId || binding.attemptOrdinal !== index + 1)
1327
+ if (!binding.eventId || binding.attemptOrdinal !== evidence.remediationEvents[0].attemptOrdinal + index)
1644
1328
  return true;
1645
1329
  assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
1646
1330
  return false;
@@ -1681,9 +1365,9 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1681
1365
  assertHash64(evidence.failedCheckCauseFingerprint, `Plan-integrity failedCheckCauseFingerprint at ${file}:${line}`);
1682
1366
  if (event.decision !== 'continue-fix'
1683
1367
  || evidence.remediationEvents.length === 0
1684
- || event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
1368
+ || event.triggeringAttemptCount !== evidence.remediationEvents.at(-1).attemptOrdinal + 1
1685
1369
  || evidence.remediationEvents.some((binding, index) => {
1686
- if (!binding.eventId || binding.attemptOrdinal !== index + 1)
1370
+ if (!binding.eventId || binding.attemptOrdinal !== evidence.remediationEvents[0].attemptOrdinal + index)
1687
1371
  return true;
1688
1372
  assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
1689
1373
  return false;
@@ -1706,9 +1390,10 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1706
1390
  || !conflict.componentRoot.trim()
1707
1391
  || !(conflict.ownerPath === conflict.componentRoot
1708
1392
  || conflict.ownerPath.startsWith(`${conflict.componentRoot}/`))
1709
- || importChain.length < 2
1393
+ || (conflict.testInvocation
1394
+ ? !validLiteralTestInvocationEvidence(conflict) || importChain.length !== 1
1395
+ : importChain.length < 2 || importChain.at(-1) !== conflict.ownerPath)
1710
1396
  || importChain[0] !== conflict.runnerPath
1711
- || importChain.at(-1) !== conflict.ownerPath
1712
1397
  || !evidence.scopeExtensions.includes(conflict.ownerPath);
1713
1398
  })) {
1714
1399
  throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v4 evidence has invalid component-owner bindings.', {
@@ -1744,13 +1429,54 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1744
1429
  throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity changedFiles must be normalized.', { file, line });
1745
1430
  }
1746
1431
  }
1432
+ /** A validated support decision survives only a hash-verified Knowledge-only Plan chain.
1433
+ * This preserves scope evidence, never execution authorization or a semantic replan. */
1434
+ export function isKnowledgeReboundSupportDecision(store, task, event) {
1435
+ const evidence = event.planIntegrityEvidence;
1436
+ if (!task.planHash || event.planHash === task.planHash || event.taskId !== task.id
1437
+ || event.recoveryBasis !== 'plan-integrity' || event.decision !== 'continue-fix'
1438
+ || (evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
1439
+ && evidence?.kind !== 'failed-check-component-owner-outside-allowed-writes'))
1440
+ return false;
1441
+ const step = task.steps.find(s => s.id === event.stepId);
1442
+ if (!step || evidence.stepDefinitionHash !== hashCanonical({
1443
+ id: step.id, title: step.title, objective: step.objective, requirements: step.requirements,
1444
+ dependencies: step.dependencies, expectedOutputs: step.expectedOutputs,
1445
+ allowedWrites: step.allowedWrites, forbiddenScope: step.forbiddenScope, checks: step.checks,
1446
+ }) || evidence.scopeExtensions.some(p => pathAllowed(p, step.allowedWrites) || pathAllowed(p, step.forbiddenScope))
1447
+ || (evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
1448
+ && evidence.semanticBoundaryHash !== semanticBoundaryHash(task)))
1449
+ return false;
1450
+ const root = store.taskRoot(task.projectId, task.id);
1451
+ let cursor = task.planHash;
1452
+ const visited = new Set();
1453
+ while (cursor !== event.planHash) {
1454
+ if (visited.has(cursor))
1455
+ return false;
1456
+ visited.add(cursor);
1457
+ const links = task.knowledgeRebinds.filter(link => link.reboundPlanHash === cursor);
1458
+ if (links.length !== 1 || !links[0].previousPlanHash)
1459
+ return false;
1460
+ const link = links[0];
1461
+ const before = readVerifiedPlanArtifact(root, link.previousPlanHash, task.planHash);
1462
+ const after = readVerifiedPlanArtifact(root, cursor, task.planHash);
1463
+ assertKnowledgeBindingOnlyPlanChange(before, after);
1464
+ const from = normalizePlanKnowledgeBinding(before, 'source').binding;
1465
+ const to = normalizePlanKnowledgeBinding(after, 'target').binding;
1466
+ if (from.revision !== link.fromKnowledgeMapRevision || from.hash !== link.fromKnowledgeMapHash
1467
+ || to.revision !== link.toKnowledgeMapRevision || to.hash !== link.toKnowledgeMapHash)
1468
+ return false;
1469
+ cursor = link.previousPlanHash;
1470
+ }
1471
+ return true;
1472
+ }
1747
1473
  export function effectiveStepAllowedWrites(store, task, stepId) {
1748
1474
  const step = task.steps.find((candidate) => candidate.id === stepId);
1749
1475
  if (!step)
1750
1476
  throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
1751
1477
  const extensions = readCorrectiveDecisionEvents(store, task)
1752
1478
  .filter((event) => event.stepId === stepId
1753
- && event.planHash === task.planHash
1479
+ && (event.planHash === task.planHash || isKnowledgeReboundSupportDecision(store, task, event))
1754
1480
  && event.decision === 'continue-fix'
1755
1481
  && event.recoveryBasis === 'plan-integrity')
1756
1482
  .flatMap((event) => event.planIntegrityEvidence?.kind === 'check-support-anchor-outside-allowed-writes'
@@ -2020,7 +1746,7 @@ function validateRemediationReviewReferences(store, task, events) {
2020
1746
  }
2021
1747
  }
2022
1748
  }
2023
- function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents) {
1749
+ function validateCorrectiveDecisionCoverage(store, task, decisions, remediationEvents) {
2024
1750
  const remediationEventsById = new Map(remediationEvents.map((event) => [event.eventId, event]));
2025
1751
  const decisionBindings = new Set();
2026
1752
  for (const decision of decisions) {
@@ -2066,14 +1792,39 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
2066
1792
  });
2067
1793
  }
2068
1794
  }
1795
+ if (decision.recoveryBasis === 'planning-obstruction') {
1796
+ const proof = decision.planningObstructionEvidence;
1797
+ const audits = readPlanRiskAuditEvents(store, task);
1798
+ const baseline = audits.find(event => event.eventId === proof.baselineAuditEventId && event.eventHash === proof.baselineAuditEventHash);
1799
+ const obstruction = audits.find(event => event.eventId === proof.obstructionAuditEventId && event.eventHash === proof.obstructionAuditEventHash);
1800
+ const failed = priorEvents[0];
1801
+ if (!baseline || !obstruction || !['approved', 'approved-with-rationale'].includes(baseline.decision)
1802
+ || obstruction.decision !== 'stop-escalate' || audits.indexOf(baseline) >= audits.indexOf(obstruction)
1803
+ || baseline.planHash !== proof.baselinePlanHash || obstruction.planHash !== proof.sourcePlanHash
1804
+ || obstruction.taskRevision !== proof.taskRevision
1805
+ || baseline.briefHash !== obstruction.briefHash || baseline.briefHash !== task.briefHash
1806
+ || !failed || priorEvents.length !== 1 || failed.eventId !== proof.remediationEventId || failed.eventHash !== proof.remediationEventHash
1807
+ || !task.authorizations.some(authorization => authorization.kind === 'execution' && authorization.decision === 'superseded'
1808
+ && authorization.planHash === baseline.planHash && authorization.briefHash === baseline.briefHash
1809
+ && hashCanonical(authorization) === proof.priorAuthorizationHash)
1810
+ || task.knowledgeRebinds.length < proof.knowledgeRebindCount
1811
+ || hashCanonical(task.knowledgeRebinds.slice(0, proof.knowledgeRebindCount)) !== proof.knowledgeRebindHash) {
1812
+ throw new WorkflowError('STATE_CORRUPT', 'Planning-obstruction decision does not match immutable audit, authorization, rebind and failure history.', { taskId: task.id, decisionEventId: decision.eventId });
1813
+ }
1814
+ }
2069
1815
  if (decision.recoveryBasis === 'plan-integrity') {
2070
1816
  const evidence = decision.planIntegrityEvidence;
2071
1817
  const bindings = evidence?.kind === 'missing-npm-script-outside-allowed-writes'
2072
1818
  ? [{ eventId: evidence.remediationEventId, eventHash: evidence.remediationEventHash, attemptOrdinal: 1 }]
2073
1819
  : evidence?.remediationEvents ?? [];
2074
- if (!evidence
2075
- || bindings.length !== priorEvents.length
2076
- || priorEvents.some((linked, index) => linked.failureKind !== 'checks-failed'
1820
+ const support = evidence?.kind === 'check-support-anchor-outside-allowed-writes'
1821
+ || evidence?.kind === 'failed-check-component-owner-outside-allowed-writes';
1822
+ const scoped = support && bindings[0]?.attemptOrdinal !== 1
1823
+ ? selectPlanIntegrityRemediations(store, { ...task, planHash: decision.planHash, revision: evidence.taskRevision }, priorEvents, decisions.filter(d => d.stepId === decision.stepId && d.triggeringAttemptCount < decision.triggeringAttemptCount), task.planHash)
1824
+ : priorEvents;
1825
+ if (!evidence || !scoped
1826
+ || bindings.length !== scoped.length
1827
+ || scoped.some((linked, index) => linked.failureKind !== 'checks-failed'
2077
1828
  || linked.eventId !== bindings[index]?.eventId
2078
1829
  || linked.eventHash !== bindings[index]?.eventHash
2079
1830
  || linked.attemptOrdinal !== bindings[index]?.attemptOrdinal)) {