codex-workflow-v2 2.0.0-beta.13.3 → 2.0.0-beta.13.6

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 (44) hide show
  1. package/README.md +4 -1
  2. package/dist/reviewer-runtime-build.json +23 -11
  3. package/dist/src/alpha6/plan-risk.d.ts +1 -0
  4. package/dist/src/alpha6/plan-risk.js +9 -6
  5. package/dist/src/alpha6/plan-risk.js.map +1 -1
  6. package/dist/src/alpha6/preexecution-replan.d.ts +38 -0
  7. package/dist/src/alpha6/preexecution-replan.js +130 -0
  8. package/dist/src/alpha6/preexecution-replan.js.map +1 -0
  9. package/dist/src/alpha6/remediation.d.ts +4 -3
  10. package/dist/src/alpha6/remediation.js +147 -21
  11. package/dist/src/alpha6/remediation.js.map +1 -1
  12. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +17 -0
  13. package/dist/src/alpha6/root-cause-replan-carryover.js +372 -0
  14. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -0
  15. package/dist/src/cli-actions.d.ts +2 -2
  16. package/dist/src/cli-actions.js +2 -0
  17. package/dist/src/cli-actions.js.map +1 -1
  18. package/dist/src/cli.js +51 -0
  19. package/dist/src/cli.js.map +1 -1
  20. package/dist/src/contracts.d.ts +35 -0
  21. package/dist/src/dependency-provenance.d.ts +3 -2
  22. package/dist/src/dependency-provenance.js +33 -2
  23. package/dist/src/dependency-provenance.js.map +1 -1
  24. package/dist/src/domain/plan-semantics.d.ts +11 -0
  25. package/dist/src/domain/plan-semantics.js +49 -0
  26. package/dist/src/domain/plan-semantics.js.map +1 -0
  27. package/dist/src/gateway-handshake.js +1 -0
  28. package/dist/src/gateway-handshake.js.map +1 -1
  29. package/dist/src/version.d.ts +1 -1
  30. package/dist/src/version.js +1 -1
  31. package/dist/src/workflow.d.ts +21 -1
  32. package/dist/src/workflow.js +793 -89
  33. package/dist/src/workflow.js.map +1 -1
  34. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  35. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  36. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  37. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
  38. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
  39. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
  40. package/docs/release.md +72 -0
  41. package/package.json +1 -1
  42. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +72 -2
  43. package/schemas/preexecution-replan-event.schema.json +45 -0
  44. package/schemas/task.schema.json +55 -0
@@ -3,11 +3,12 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
3
3
  import { createHash } from 'node:crypto';
4
4
  import path from 'node:path';
5
5
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
6
- import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
6
+ import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, workflowDependencyVersionAt, } from './dependency-provenance.js';
7
7
  import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
8
8
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
9
9
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
10
10
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
11
+ import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from './domain/plan-semantics.js';
11
12
  import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
12
13
  import { pathAllowed, validatePlan } from './domain/validation.js';
13
14
  import { WorkflowError } from './errors.js';
@@ -17,8 +18,10 @@ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.j
17
18
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
18
19
  import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
19
20
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
20
- import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
21
- import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecision, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
21
+ import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
22
+ import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
23
+ import { assessRootCauseReplanDirtyCarryover, } from './alpha6/root-cause-replan-carryover.js';
24
+ import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
22
25
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
23
26
  import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
24
27
  import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
@@ -28,7 +31,7 @@ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, en
28
31
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
29
32
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
30
33
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
31
- import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
34
+ import { canonicalJsonStringify, prepareSidecarAppend, sha256Hex } from './alpha6/store-sidecars.js';
32
35
  import { ensureDirectory } from './fs-utils.js';
33
36
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
34
37
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
@@ -47,9 +50,11 @@ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
47
50
  export class WorkflowService {
48
51
  store;
49
52
  now;
50
- constructor(store = new FileStateStore(), now = () => new Date()) {
53
+ transactionHooks;
54
+ constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
51
55
  this.store = store;
52
56
  this.now = now;
57
+ this.transactionHooks = transactionHooks;
53
58
  }
54
59
  #mutationContext(repository) {
55
60
  const identity = resolveRepositoryIdentity(repository);
@@ -1601,6 +1606,10 @@ export class WorkflowService {
1601
1606
  this.validateKnowledgeTargets(knowledgeMap, plan);
1602
1607
  const task = this.store.readTask(identity.projectId, taskId);
1603
1608
  assertExpectedRevision(task, expectedRevision);
1609
+ const preExecutionReplan = readCurrentPreExecutionReplan(this.store, task);
1610
+ const preExecutionReplanAudits = preExecutionReplan
1611
+ ? resolvePreExecutionReplanAudits(this.store, task, preExecutionReplan)
1612
+ : null;
1604
1613
  const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
1605
1614
  const currentPlanCorrectiveDecisions = task.planHash
1606
1615
  ? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
@@ -1747,6 +1756,9 @@ export class WorkflowService {
1747
1756
  }
1748
1757
  const root = this.store.taskRoot(identity.projectId, taskId);
1749
1758
  const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
1759
+ const changesPlanSemantics = !task.planHash
1760
+ || normalizePlanKnowledgeBinding(preparedPlan.planBytes, 'candidate').semanticHash
1761
+ !== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(root, task.planHash, task.planHash), 'current').semanticHash;
1750
1762
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
1751
1763
  ? { ...authorization, decision: 'superseded' }
1752
1764
  : authorization);
@@ -1764,6 +1776,83 @@ export class WorkflowService {
1764
1776
  ...(task.invalidatedStepCommits ?? []),
1765
1777
  ...omittedStepEvidenceCommits(task, plannedSteps),
1766
1778
  ])];
1779
+ if (preExecutionReplan && preExecutionReplanAudits) {
1780
+ const replanC1Posture = readTaskC1Posture(this.store, task);
1781
+ const replanWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
1782
+ if (replanC1Posture.state !== 'none' || replanWriterLease) {
1783
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replacement Plan requires no C1 posture or writer lease.', {
1784
+ taskId: task.id,
1785
+ c1Posture: replanC1Posture.state,
1786
+ writerLeasePresent: replanWriterLease !== null,
1787
+ });
1788
+ }
1789
+ if (!planRiskAudit) {
1790
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires an atomic replacement Plan Risk Audit.');
1791
+ }
1792
+ if (task.planObjective !== plan.objective
1793
+ || canonicalJsonStringify(task.requirements) !== canonicalJsonStringify(plan.requirements)
1794
+ || canonicalJsonStringify(task.acceptance) !== canonicalJsonStringify(plan.acceptance)) {
1795
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot change Task objective, requirements, or acceptance.', {
1796
+ taskId: task.id,
1797
+ });
1798
+ }
1799
+ if (!changesPlanSemantics) {
1800
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic Plan change.', { taskId: task.id });
1801
+ }
1802
+ const candidateSteps = new Map(plan.steps.map((step) => [step.id, step]));
1803
+ const missingStepIds = task.steps.filter((step) => !candidateSteps.has(step.id)).map((step) => step.id);
1804
+ if (missingStepIds.length > 0) {
1805
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must retain every existing Step identifier.', {
1806
+ taskId: task.id,
1807
+ missingStepIds,
1808
+ });
1809
+ }
1810
+ const completedStepIds = task.steps.filter((step) => step.status === 'completed').map((step) => step.id);
1811
+ const lostCompletedStepIds = completedStepIds.filter((stepId) => !retainedCompletedSteps.has(stepId));
1812
+ if (lostCompletedStepIds.length > 0) {
1813
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must preserve the exact completed Step subgraph and evidence.', {
1814
+ taskId: task.id,
1815
+ lostCompletedStepIds,
1816
+ });
1817
+ }
1818
+ const weakenedForbiddenScope = task.steps.flatMap((step) => {
1819
+ const candidate = candidateSteps.get(step.id);
1820
+ const missing = step.forbiddenScope.filter((scope) => !candidate.forbiddenScope.includes(scope));
1821
+ return missing.length > 0 ? [{ stepId: step.id, missing }] : [];
1822
+ });
1823
+ if (weakenedForbiddenScope.length > 0) {
1824
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken existing forbiddenScope boundaries.', {
1825
+ taskId: task.id,
1826
+ weakenedForbiddenScope,
1827
+ });
1828
+ }
1829
+ const missingRisks = task.risks.filter((risk) => !plan.risks.includes(risk));
1830
+ if (missingRisks.length > 0) {
1831
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot suppress existing Plan risks.', {
1832
+ taskId: task.id,
1833
+ missingRisks,
1834
+ });
1835
+ }
1836
+ assertPlanRiskSafetyPreserved(preExecutionReplanAudits.obstructionAudit, planRiskAudit, 'Replacement Plan Risk Audit');
1837
+ const candidateEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => canonicalJsonStringify(edge)));
1838
+ const missingProductionEdges = (task.productionEdges ?? [])
1839
+ .filter((edge) => !candidateEdgeKeys.has(canonicalJsonStringify(edge)));
1840
+ if ((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
1841
+ || missingProductionEdges.length > 0) {
1842
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken production-edge authority.', {
1843
+ taskId: task.id,
1844
+ missingProductionEdges,
1845
+ });
1846
+ }
1847
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== preExecutionReplan.sourceHeadCommit) {
1848
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replacement Plan requires the exact clean recorded HEAD.', {
1849
+ taskId: task.id,
1850
+ expectedHead: preExecutionReplan.sourceHeadCommit,
1851
+ actualHead: headCommit(identity.repositoryRoot),
1852
+ changedFiles: changedFiles(identity.repositoryRoot),
1853
+ });
1854
+ }
1855
+ }
1767
1856
  if (task.status === 'needs_fix' &&
1768
1857
  task.review.status === 'failed' &&
1769
1858
  plannedSteps.every((step) => step.status === 'completed')) {
@@ -1787,7 +1876,7 @@ export class WorkflowService {
1787
1876
  : []),
1788
1877
  ...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
1789
1878
  ...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
1790
- ...(preparedPlan.planHash === task.planHash
1879
+ ...(!changesPlanSemantics
1791
1880
  ? ['task.rolling-replan.semantic-plan-change']
1792
1881
  : []),
1793
1882
  ...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
@@ -1860,6 +1949,61 @@ export class WorkflowService {
1860
1949
  };
1861
1950
  if (planRiskAudit)
1862
1951
  validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
1952
+ if (preExecutionReplan && preExecutionReplanAudits && planRiskAudit) {
1953
+ const now = this.now();
1954
+ const resultTask = {
1955
+ ...candidate,
1956
+ revision: task.revision + 1,
1957
+ updatedAt: now.toISOString(),
1958
+ };
1959
+ const preparedAuditBase = buildCandidatePlanRiskAuditEvent(this.store, resultTask, planRiskAudit, now.toISOString(), now.getTime());
1960
+ const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
1961
+ const projectRoot = this.store.projectRoot(task.projectId);
1962
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
1963
+ const versionFile = path.join(root, '.versions', `plan.md.${preparedPlan.planHash}`);
1964
+ if (existsSync(versionFile) && sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash) {
1965
+ throw new WorkflowError('STATE_CORRUPT', 'Immutable replacement Plan version is corrupt.', {
1966
+ taskId: task.id,
1967
+ planHash: preparedPlan.planHash,
1968
+ });
1969
+ }
1970
+ applyProjectTransaction({
1971
+ projectRoot,
1972
+ projectId: task.projectId,
1973
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
1974
+ kind: 'preexecution-replacement-plan-set',
1975
+ now,
1976
+ targets: [
1977
+ { path: relative(path.join(root, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
1978
+ { path: relative(path.join(root, 'plan.md')), content: preparedPlan.planBytes },
1979
+ { path: relative(versionFile), content: preparedPlan.planBytes },
1980
+ { path: relative(path.join(root, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
1981
+ ],
1982
+ ...(this.transactionHooks.afterPreExecutionReplanTargetWrite
1983
+ ? { afterTargetWrite: this.transactionHooks.afterPreExecutionReplanTargetWrite }
1984
+ : {}),
1985
+ validate: () => {
1986
+ const saved = this.store.readTask(task.projectId, task.id);
1987
+ const currentAudit = readCurrentPlanRiskAudit(this.store, saved, { requireCurrentTaskRevision: true });
1988
+ if (saved.revision !== resultTask.revision
1989
+ || saved.status !== 'awaiting_execution_authorization'
1990
+ || saved.planHash !== preparedPlan.planHash
1991
+ || this.store.hashArtifact(root, 'plan.md') !== preparedPlan.planHash
1992
+ || sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash
1993
+ || currentAudit?.audit.eventHash !== preparedAudit.event.eventHash
1994
+ || hashCompletedSteps(saved) !== preExecutionReplan.completedStepsHash
1995
+ || readCurrentPreExecutionReplan(this.store, saved) !== null
1996
+ || saved.authorizations.some((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved')) {
1997
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replacement Plan transaction readback failed.', {
1998
+ taskId: task.id,
1999
+ });
2000
+ }
2001
+ if (saved.baseCommit)
2002
+ validateTaskHistory(identity.repositoryRoot, saved);
2003
+ },
2004
+ });
2005
+ return this.store.readTask(task.projectId, task.id);
2006
+ }
1863
2007
  const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
1864
2008
  if (stagedPlan.hash !== preparedPlan.planHash) {
1865
2009
  throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
@@ -2068,6 +2212,9 @@ export class WorkflowService {
2068
2212
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2069
2213
  const task = this.store.readTask(identity.projectId, taskId);
2070
2214
  assertExpectedRevision(task, expectedRevision);
2215
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2216
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic replacement Plan before execution authorization.', { taskId, requiredAction: 'task plan-set' });
2217
+ }
2071
2218
  if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
2072
2219
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting execution authorization.`);
2073
2220
  }
@@ -2125,9 +2272,143 @@ export class WorkflowService {
2125
2272
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2126
2273
  const task = this.store.readTask(identity.projectId, taskId);
2127
2274
  assertExpectedRevision(task, expectedRevision);
2275
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2276
+ 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' });
2277
+ }
2128
2278
  this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
2129
2279
  return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
2130
2280
  }
2281
+ recordPreExecutionReplan(repository, taskId, expectedRevision, actor, reason, binding, input) {
2282
+ const context = this.#mutationContext(repository);
2283
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
2284
+ const task = this.store.readTask(context.identity.projectId, taskId);
2285
+ assertExpectedRevision(task, expectedRevision);
2286
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2287
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task already has a pending pre-execution replan.', {
2288
+ taskId,
2289
+ requiredAction: 'task plan-set',
2290
+ });
2291
+ }
2292
+ if (task.status !== 'ready' || task.steps.some((step) => ['in_progress', 'failed'].includes(step.status))) {
2293
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a ready Task with no Step in progress or failed.', {
2294
+ taskId,
2295
+ status: task.status,
2296
+ });
2297
+ }
2298
+ if (!isClean(context.identity.repositoryRoot)) {
2299
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replan requires a clean checkout.', {
2300
+ changedFiles: changedFiles(context.identity.repositoryRoot),
2301
+ });
2302
+ }
2303
+ const c1Posture = readTaskC1Posture(this.store, task);
2304
+ const writerLease = context.locks.inspect(task.id);
2305
+ if (c1Posture.state !== 'none' || writerLease) {
2306
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a clean authority boundary without C1 or writer lease.', {
2307
+ taskId,
2308
+ c1Posture: c1Posture.state,
2309
+ writerLeasePresent: writerLease !== null,
2310
+ });
2311
+ }
2312
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2313
+ if (task.baseCommit) {
2314
+ validateTaskHistory(context.identity.repositoryRoot, task);
2315
+ }
2316
+ else if (task.workspaceOwner !== null || task.taskBranch !== null
2317
+ || task.steps.some((step) => step.status === 'completed')) {
2318
+ throw new WorkflowError('STATE_CORRUPT', 'Unstarted pre-execution replan Task has inconsistent execution history.', {
2319
+ taskId,
2320
+ });
2321
+ }
2322
+ const authorization = this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2323
+ const sourceAudit = readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: false });
2324
+ if (!sourceAudit || !['approved', 'approved-with-rationale'].includes(sourceAudit.audit.decision)) {
2325
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires the current positive Plan Risk Audit.');
2326
+ }
2327
+ const currentHead = headCommit(context.identity.repositoryRoot);
2328
+ const actualBinding = {
2329
+ expectedBriefHash: task.briefHash,
2330
+ expectedPlanHash: task.planHash,
2331
+ expectedPlanRiskAuditEventHash: sourceAudit.audit.eventHash,
2332
+ expectedHeadCommit: currentHead,
2333
+ };
2334
+ if (canonicalJsonStringify(binding) !== canonicalJsonStringify(actualBinding)) {
2335
+ throw new WorkflowError('STATE_CONFLICT', 'Pre-execution replan source binding is stale.', {
2336
+ expected: binding,
2337
+ actual: actualBinding,
2338
+ });
2339
+ }
2340
+ if (input.decision !== 'stop-escalate') {
2341
+ throw new WorkflowError('INVALID_ARGUMENT', 'Pre-execution replan requires a stop-escalate Plan Risk Audit.');
2342
+ }
2343
+ if (actor.trim() !== input.auditor.trim() || input.planner.trim() !== sourceAudit.audit.planner) {
2344
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must be recorded by the independent auditor against the original planner.', {
2345
+ actor: actor.trim(),
2346
+ auditor: input.auditor.trim(),
2347
+ expectedPlanner: sourceAudit.audit.planner,
2348
+ actualPlanner: input.planner.trim(),
2349
+ });
2350
+ }
2351
+ assertPlanRiskSafetyPreserved(sourceAudit.audit, input, 'Pre-execution replan audit');
2352
+ const now = this.now();
2353
+ const preparedAuditBase = buildPlanRiskAuditEvent(this.store, task, input, now.toISOString(), now.getTime());
2354
+ const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
2355
+ const preparedMarker = preparePreExecutionReplanEvent(this.store, task, {
2356
+ sourceAudit: sourceAudit.audit,
2357
+ obstructionAudit: preparedAudit.event,
2358
+ authorization,
2359
+ sourceHeadCommit: currentHead,
2360
+ actor,
2361
+ reason,
2362
+ }, now);
2363
+ const resultTask = {
2364
+ ...task,
2365
+ revision: task.revision + 1,
2366
+ updatedAt: now.toISOString(),
2367
+ status: 'awaiting_execution_authorization',
2368
+ authorizations: task.authorizations.map((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved'
2369
+ ? { ...candidate, decision: 'superseded' }
2370
+ : candidate),
2371
+ blockReason: reason.trim(),
2372
+ };
2373
+ const projectRoot = this.store.projectRoot(task.projectId);
2374
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2375
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2376
+ applyProjectTransaction({
2377
+ projectRoot,
2378
+ projectId: task.projectId,
2379
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2380
+ kind: 'preexecution-plan-authority-withdrawal',
2381
+ now,
2382
+ targets: [
2383
+ { path: relative(path.join(taskRoot, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
2384
+ { path: relative(path.join(taskRoot, preExecutionReplanSidecar())), content: preparedMarker.postimage },
2385
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
2386
+ ],
2387
+ validate: () => {
2388
+ const saved = this.store.readTask(task.projectId, task.id);
2389
+ const latestAudit = readPlanRiskAuditEvents(this.store, saved).at(-1) ?? null;
2390
+ const marker = readCurrentPreExecutionReplan(this.store, saved);
2391
+ if (saved.revision !== resultTask.revision
2392
+ || saved.status !== 'awaiting_execution_authorization'
2393
+ || saved.planHash !== task.planHash
2394
+ || latestAudit?.eventHash !== preparedAudit.event.eventHash
2395
+ || marker?.eventHash !== preparedMarker.event.eventHash
2396
+ || saved.authorizations.some((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved')
2397
+ || hashCompletedSteps(saved) !== preparedMarker.event.completedStepsHash
2398
+ || headCommit(context.identity.repositoryRoot) !== currentHead
2399
+ || !isClean(context.identity.repositoryRoot)) {
2400
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan transaction readback failed.', { taskId });
2401
+ }
2402
+ if (saved.baseCommit)
2403
+ validateTaskHistory(context.identity.repositoryRoot, saved);
2404
+ },
2405
+ });
2406
+ return {
2407
+ task: this.store.readTask(task.projectId, task.id),
2408
+ audit: preparedAudit.event,
2409
+ event: preparedMarker.event,
2410
+ };
2411
+ }
2131
2412
  showTaskHandoff(repository, taskId) {
2132
2413
  const { identity } = this.#mutationContext(repository);
2133
2414
  const task = this.store.readTask(identity.projectId, taskId);
@@ -2141,6 +2422,12 @@ export class WorkflowService {
2141
2422
  const context = this.#mutationContext(repository);
2142
2423
  const task = this.store.readTask(context.identity.projectId, taskId);
2143
2424
  assertExpectedRevision(task, expectedRevision);
2425
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2426
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
2427
+ taskId,
2428
+ requiredAction: 'task plan-set',
2429
+ });
2430
+ }
2144
2431
  const normalizedActor = actor.trim();
2145
2432
  if (!normalizedActor) {
2146
2433
  throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
@@ -2470,11 +2757,15 @@ export class WorkflowService {
2470
2757
  const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2471
2758
  ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2472
2759
  : null;
2760
+ const rootCauseReplanDirtyCarryover = step.status === 'planned' && dirty.length > 0
2761
+ ? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2762
+ : null;
2473
2763
  if (step.status === 'planned'
2474
2764
  && dirty.length > 0
2475
2765
  && !resumesCorrectiveReplan
2476
2766
  && !downstreamProofCarryover?.eligible
2477
- && !planIntegrityDirtyCarryover?.eligible) {
2767
+ && !planIntegrityDirtyCarryover?.eligible
2768
+ && !rootCauseReplanDirtyCarryover?.eligible) {
2478
2769
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2479
2770
  changedFiles: dirty,
2480
2771
  ...(downstreamProofCarryover?.applicable
@@ -2483,6 +2774,9 @@ export class WorkflowService {
2483
2774
  ...(planIntegrityDirtyCarryover?.applicable
2484
2775
  ? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
2485
2776
  : {}),
2777
+ ...(rootCauseReplanDirtyCarryover?.applicable
2778
+ ? { rootCauseReplanDirtyCarryoverBlockers: rootCauseReplanDirtyCarryover.blockers }
2779
+ : {}),
2486
2780
  });
2487
2781
  }
2488
2782
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
@@ -2748,7 +3042,7 @@ export class WorkflowService {
2748
3042
  const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
2749
3043
  ? stepHistory
2750
3044
  : [...stepHistory, preparedRemediation.event];
2751
- const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3045
+ const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
2752
3046
  const c1Posture = readTaskC1Posture(this.store, task);
2753
3047
  const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
2754
3048
  ? prepareTaskCorrectiveYield(this.store, paused, {
@@ -3062,7 +3356,7 @@ export class WorkflowService {
3062
3356
  const resultingHistory = preparedRemediation.postimage === null
3063
3357
  ? stepHistory
3064
3358
  : [...stepHistory, preparedRemediation.event];
3065
- const rollingCauseDecision = deriveCurrentCauseDecision(resultingHistory);
3359
+ const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
3066
3360
  const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
3067
3361
  const paused = {
3068
3362
  ...resolved,
@@ -3809,6 +4103,9 @@ export class WorkflowService {
3809
4103
  const now = this.now().getTime();
3810
4104
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
3811
4105
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4106
+ const pendingPreExecutionReplanTaskIds = tasks
4107
+ .filter((task) => readCurrentPreExecutionReplan(this.store, task) !== null)
4108
+ .map((task) => task.id);
3812
4109
  const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
3813
4110
  const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
3814
4111
  const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
@@ -3836,6 +4133,9 @@ export class WorkflowService {
3836
4133
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
3837
4134
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
3838
4135
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
4136
+ ...(pendingPreExecutionReplanTaskIds.length > 0
4137
+ ? [`Complete the pending pre-execution Plan replacement and reauthorization first: ${pendingPreExecutionReplanTaskIds.join(', ')}.`]
4138
+ : []),
3839
4139
  ...versionBlockers,
3840
4140
  ...observationPreflightBlockers(snapshot.observation),
3841
4141
  ];
@@ -3861,10 +4161,12 @@ export class WorkflowService {
3861
4161
  milestoneBases,
3862
4162
  },
3863
4163
  dependencyTransport: {
3864
- safe: versionBlockers.length === 0,
3865
- requiredAction: versionBlockers.length === 0
3866
- ? null
3867
- : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
4164
+ safe: versionBlockers.length === 0 && pendingPreExecutionReplanTaskIds.length === 0,
4165
+ requiredAction: pendingPreExecutionReplanTaskIds.length > 0
4166
+ ? 'Complete each pending pre-execution task plan-set and reauthorization before changing Workflow dependency HEAD.'
4167
+ : versionBlockers.length === 0
4168
+ ? null
4169
+ : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
3868
4170
  },
3869
4171
  adoption: snapshot.adoption,
3870
4172
  observation: snapshot.observation,
@@ -3880,7 +4182,145 @@ export class WorkflowService {
3880
4182
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3881
4183
  if (!task)
3882
4184
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3883
- return this.inspectDependencyProvenanceCandidate(snapshot, task);
4185
+ const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4186
+ const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
4187
+ if (!preflight.rootCauseReplan)
4188
+ return preflight;
4189
+ let baseTip = null;
4190
+ let baseDependency = null;
4191
+ try {
4192
+ baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
4193
+ baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
4194
+ }
4195
+ catch {
4196
+ // The blocker below keeps a missing or unreadable base reference fail closed.
4197
+ }
4198
+ const bridgeBlockers = [
4199
+ ...(preflight.dependencyCommits.length !== 1
4200
+ ? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
4201
+ ...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
4202
+ ? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
4203
+ ...(preflight.parentCommitSha
4204
+ && workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.5'
4205
+ ? ['Corrective carryover dependency parent must declare exact source 2.0.0-beta.13.5.'] : []),
4206
+ ...(preflight.dependencyCommits.at(-1)?.packageVersion !== '2.0.0-beta.13.6'
4207
+ ? ['Corrective carryover dependency commit must declare exact target 2.0.0-beta.13.6.'] : []),
4208
+ ...(!baseDependency
4209
+ || baseDependency.commitSha !== baseTip
4210
+ || baseDependency.packageVersion !== '2.0.0-beta.13.6'
4211
+ || workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha) !== '2.0.0-beta.13.5'
4212
+ ? ['Corrective carryover Milestone-base tip must be one exact 2.0.0-beta.13.5 to 2.0.0-beta.13.6 dependency-only commit.'] : []),
4213
+ ];
4214
+ return bridgeBlockers.length === 0
4215
+ ? preflight
4216
+ : {
4217
+ ...preflight,
4218
+ eligible: false,
4219
+ action: null,
4220
+ commitSha: null,
4221
+ parentCommitSha: null,
4222
+ blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
4223
+ };
4224
+ }
4225
+ correctiveCarryoverUpdatePreflight(repository, taskId) {
4226
+ const snapshot = this.observationSnapshot(repository);
4227
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4228
+ if (!task)
4229
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4230
+ const sourceVersion = '2.0.0-beta.13.5';
4231
+ const targetVersion = '2.0.0-beta.13.6';
4232
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4233
+ const sourceSurfaces = {
4234
+ declared: versions.declared,
4235
+ locked: versions.locked,
4236
+ installed: versions.installed,
4237
+ currentBranch: versions.currentBranch,
4238
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4239
+ };
4240
+ const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4241
+ const c1 = readTaskC1Posture(this.store, task);
4242
+ const now = this.now().getTime();
4243
+ const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
4244
+ const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4245
+ const transactionCount = snapshot.observation.pendingProjectTransactions.length
4246
+ + snapshot.observation.pendingMilestoneTransactions.length
4247
+ + snapshot.observation.pendingTaskTransactions.length;
4248
+ const authorizationBlockers = [];
4249
+ if (assessment.eligible) {
4250
+ try {
4251
+ this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
4252
+ }
4253
+ catch (error) {
4254
+ authorizationBlockers.push(error instanceof Error ? error.message : String(error));
4255
+ }
4256
+ }
4257
+ const baseBlockers = [
4258
+ ...assessment.blockers,
4259
+ ...authorizationBlockers,
4260
+ ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4261
+ ...Object.entries(sourceSurfaces)
4262
+ .filter(([, version]) => version !== sourceVersion)
4263
+ .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4264
+ ...observationPreflightBlockers(snapshot.observation),
4265
+ ...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
4266
+ ...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
4267
+ ...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
4268
+ ...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
4269
+ ? ['The exact original Worker C1 claim must remain current.'] : []),
4270
+ ...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
4271
+ ...(staleLeases.some((lease) => lease.entityId !== task.id
4272
+ || c1.state !== 'claimed' || lease.owner !== c1.claimant
4273
+ || createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
4274
+ ? ['No unrelated or differently owned stale writer lease may exist.'] : []),
4275
+ ...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
4276
+ ];
4277
+ const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
4278
+ ? staleLeases[0]
4279
+ : null;
4280
+ const blockers = [
4281
+ ...baseBlockers,
4282
+ ...(repairableStale
4283
+ ? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
4284
+ : []),
4285
+ ];
4286
+ const compatibility = assessment.compatibility;
4287
+ return {
4288
+ readOnly: true,
4289
+ eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
4290
+ action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
4291
+ ? 'update corrective-carryover-transport'
4292
+ : null,
4293
+ projectId: task.projectId,
4294
+ taskId: task.id,
4295
+ taskRevision: task.revision,
4296
+ stepId: compatibility?.stepId ?? null,
4297
+ sourceVersion,
4298
+ targetVersion,
4299
+ sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
4300
+ taskBranch: task.taskBranch,
4301
+ baseBranch: task.baseBranch,
4302
+ dirtyFiles: compatibility?.dirtyFiles ?? [],
4303
+ dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
4304
+ compatibilityHash: compatibility?.compatibilityHash ?? null,
4305
+ c1Posture: c1.state === 'claimed' ? {
4306
+ state: 'claimed',
4307
+ claimant: c1.claimant,
4308
+ claimEventId: compatibility?.c1ClaimEventId ?? null,
4309
+ claimEventHash: compatibility?.c1ClaimEventHash ?? null,
4310
+ } : null,
4311
+ staleLeaseRepair: repairableStale ? [{
4312
+ action: 'locks repair',
4313
+ entityId: task.id,
4314
+ owner: repairableStale.owner,
4315
+ expiresAt: repairableStale.expiresAt,
4316
+ }] : [],
4317
+ requiredTransport: [
4318
+ 'Commit only package.json and package-lock.json on the active Milestone base.',
4319
+ 'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
4320
+ 'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
4321
+ ],
4322
+ blockers,
4323
+ };
3884
4324
  }
3885
4325
  historicalStepProvenanceRecoveryPreflight(repository, taskId) {
3886
4326
  const snapshot = this.observationSnapshot(repository);
@@ -4351,7 +4791,9 @@ export class WorkflowService {
4351
4791
  ? Boolean(record.historicalStepProvenance)
4352
4792
  : recoveryMode === 'plan-integrity'
4353
4793
  ? Boolean(record.planIntegrity)
4354
- : Boolean(record.activeDownstreamProof) === recoveryMode));
4794
+ : recoveryMode === false && record.rootCauseReplan
4795
+ ? true
4796
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
4355
4797
  if (prior && current.systemCommits.includes(currentHead))
4356
4798
  return current;
4357
4799
  assertExpectedRevision(current, expectedRevision);
@@ -4367,13 +4809,26 @@ export class WorkflowService {
4367
4809
  ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
4368
4810
  : recoveryMode
4369
4811
  ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4370
- : this.inspectDependencyProvenanceCandidate(snapshot, task);
4812
+ : this.dependencyProvenanceRecoveryPreflight(repository, taskId);
4371
4813
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
4372
4814
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
4373
4815
  taskId,
4374
4816
  blockers: preflight.blockers,
4375
4817
  });
4376
4818
  }
4819
+ if (preflight.rootCauseReplan) {
4820
+ const posture = readTaskC1Posture(this.store, task);
4821
+ if (posture.state !== 'claimed'
4822
+ || posture.claimant !== preflight.rootCauseReplan.claimant
4823
+ || normalizedActor !== posture.claimant) {
4824
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
4825
+ taskId,
4826
+ actor: normalizedActor,
4827
+ requiredActor: preflight.rootCauseReplan.claimant,
4828
+ posture: posture.state,
4829
+ });
4830
+ }
4831
+ }
4377
4832
  const now = this.now();
4378
4833
  const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
4379
4834
  const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
@@ -4399,6 +4854,9 @@ export class WorkflowService {
4399
4854
  ...(isTerminalCommit && preflight.planIntegrity
4400
4855
  ? { planIntegrity: preflight.planIntegrity }
4401
4856
  : {}),
4857
+ ...(isTerminalCommit && preflight.rootCauseReplan
4858
+ ? { rootCauseReplan: preflight.rootCauseReplan }
4859
+ : {}),
4402
4860
  };
4403
4861
  });
4404
4862
  const nextTask = {
@@ -5071,6 +5529,11 @@ export class WorkflowService {
5071
5529
  throw error;
5072
5530
  const blockingDecision = error.details.decision;
5073
5531
  const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
5532
+ const preExecutionReplan = readCurrentPreExecutionReplan(this.store, activeTask);
5533
+ const verifiedPreExecutionReplan = preExecutionReplan
5534
+ && blockingAudit?.eventHash === preExecutionReplan.obstructionPlanRiskAuditEventHash
5535
+ ? resolvePreExecutionReplanAudits(this.store, activeTask, preExecutionReplan)
5536
+ : null;
5074
5537
  const existingRemediationEvent = blockingAudit && activeTask.milestoneId
5075
5538
  ? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
5076
5539
  .reverse()
@@ -5104,50 +5567,34 @@ export class WorkflowService {
5104
5567
  const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
5105
5568
  .filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
5106
5569
  .sort();
5107
- planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
5570
+ planRiskAuditRecovery = verifiedPreExecutionReplan && preExecutionReplan
5108
5571
  ? {
5109
5572
  scope: 'task',
5110
5573
  id: activeTask.id,
5111
5574
  status: activeTask.status,
5112
- action: existingRemediationTask?.status === 'merged'
5113
- ? 'task plan-set'
5114
- : existingRemediationTask
5115
- ? 'doctor'
5116
- : remediationDiscovery?.status === 'ready_to_materialize'
5117
- ? 'milestone remediation-materialize'
5118
- : remediationDiscovery
5119
- ? 'discovery update'
5120
- : 'discovery start',
5575
+ action: 'task plan-set',
5121
5576
  blockedAction: 'task authorize',
5122
5577
  revision: activeTask.revision,
5123
- planRiskAuditDecision: {
5124
- state: 'recorded-blocking',
5125
- decision: blockingDecision,
5126
- reason: error.message,
5127
- requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
5578
+ preExecutionReplan: {
5579
+ state: 'recorded',
5580
+ eventId: preExecutionReplan.eventId,
5581
+ eventHash: preExecutionReplan.eventHash,
5582
+ sourcePlanHash: preExecutionReplan.sourcePlanHash,
5583
+ sourceHeadCommit: preExecutionReplan.sourceHeadCommit,
5584
+ obstructionPlanRiskAuditEventId: preExecutionReplan.obstructionPlanRiskAuditEventId,
5585
+ obstructionPlanRiskAuditEventHash: preExecutionReplan.obstructionPlanRiskAuditEventHash,
5586
+ preservesTaskScope: true,
5587
+ preservesCompletedStepAuthority: true,
5588
+ requiredAction: 'Replace the obstructed implementation Plan through ordinary task plan-set.',
5128
5589
  },
5129
- auditedRemediation: {
5130
- state: existingRemediationTask?.status === 'merged'
5131
- ? 'merged-replan-required'
5132
- : existingRemediationTask
5133
- ? 'materialized-not-merged'
5134
- : remediationDiscovery?.status === 'ready_to_materialize'
5135
- ? 'ready-to-materialize'
5136
- : 'discovery-required',
5137
- humanApprovalRequired: false,
5138
- blockedTaskId: activeTask.id,
5139
- blockedTaskRevision: activeTask.revision,
5140
- milestoneId: remediationMilestone.id,
5141
- milestoneRevision: remediationMilestone.revision,
5142
- planRiskAuditEventId: blockingAudit?.eventId ?? null,
5143
- planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
5144
- discoveryId: remediationDiscovery?.id ?? null,
5145
- discoveryRevision: remediationDiscovery?.revision ?? null,
5146
- remediationTaskId: existingRemediationTask?.id ?? null,
5147
- remediationTaskStatus: existingRemediationTask?.status ?? null,
5148
- requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
5149
- eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
5150
- command: existingRemediationTask?.status === 'merged'
5590
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5591
+ }
5592
+ : blockingDecision === 'stop-escalate' && auditedRemediationEligible
5593
+ ? {
5594
+ scope: 'task',
5595
+ id: activeTask.id,
5596
+ status: activeTask.status,
5597
+ action: existingRemediationTask?.status === 'merged'
5151
5598
  ? 'task plan-set'
5152
5599
  : existingRemediationTask
5153
5600
  ? 'doctor'
@@ -5156,46 +5603,84 @@ export class WorkflowService {
5156
5603
  : remediationDiscovery
5157
5604
  ? 'discovery update'
5158
5605
  : 'discovery start',
5159
- boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5160
- preservesMilestoneSemantics: true,
5161
- },
5162
- taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5163
- }
5164
- : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5165
- ? {
5166
- scope: 'task',
5167
- id: activeTask.id,
5168
- status: activeTask.status,
5169
- action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5170
5606
  blockedAction: 'task authorize',
5171
5607
  revision: activeTask.revision,
5172
5608
  planRiskAuditDecision: {
5173
5609
  state: 'recorded-blocking',
5174
5610
  decision: blockingDecision,
5175
5611
  reason: error.message,
5176
- requiredAction: blockingDecision === 'stop-escalate'
5177
- ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5178
- : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5612
+ requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
5179
5613
  },
5180
- ...(blockingDecision === 'stop-escalate'
5181
- ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5182
- : {}),
5183
- }
5184
- : {
5185
- scope: 'task',
5186
- id: activeTask.id,
5187
- status: activeTask.status,
5188
- action: 'task plan-risk-audit',
5189
- revision: activeTask.revision,
5190
- planRiskAuditRecovery: {
5191
- state: 'required',
5192
- reason: error.message,
5193
- currentBriefHash: activeTask.briefHash,
5194
- currentPlanHash: activeTask.planHash,
5195
- knowledgeMapRevision: activeTask.knowledgeMapRevision,
5196
- knowledgeMapHash: activeTask.knowledgeMapHash,
5614
+ auditedRemediation: {
5615
+ state: existingRemediationTask?.status === 'merged'
5616
+ ? 'merged-replan-required'
5617
+ : existingRemediationTask
5618
+ ? 'materialized-not-merged'
5619
+ : remediationDiscovery?.status === 'ready_to_materialize'
5620
+ ? 'ready-to-materialize'
5621
+ : 'discovery-required',
5622
+ humanApprovalRequired: false,
5623
+ blockedTaskId: activeTask.id,
5624
+ blockedTaskRevision: activeTask.revision,
5625
+ milestoneId: remediationMilestone.id,
5626
+ milestoneRevision: remediationMilestone.revision,
5627
+ planRiskAuditEventId: blockingAudit?.eventId ?? null,
5628
+ planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
5629
+ discoveryId: remediationDiscovery?.id ?? null,
5630
+ discoveryRevision: remediationDiscovery?.revision ?? null,
5631
+ remediationTaskId: existingRemediationTask?.id ?? null,
5632
+ remediationTaskStatus: existingRemediationTask?.status ?? null,
5633
+ requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
5634
+ eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
5635
+ command: existingRemediationTask?.status === 'merged'
5636
+ ? 'task plan-set'
5637
+ : existingRemediationTask
5638
+ ? 'doctor'
5639
+ : remediationDiscovery?.status === 'ready_to_materialize'
5640
+ ? 'milestone remediation-materialize'
5641
+ : remediationDiscovery
5642
+ ? 'discovery update'
5643
+ : 'discovery start',
5644
+ boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5645
+ preservesMilestoneSemantics: true,
5197
5646
  },
5198
- };
5647
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5648
+ }
5649
+ : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5650
+ ? {
5651
+ scope: 'task',
5652
+ id: activeTask.id,
5653
+ status: activeTask.status,
5654
+ action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5655
+ blockedAction: 'task authorize',
5656
+ revision: activeTask.revision,
5657
+ planRiskAuditDecision: {
5658
+ state: 'recorded-blocking',
5659
+ decision: blockingDecision,
5660
+ reason: error.message,
5661
+ requiredAction: blockingDecision === 'stop-escalate'
5662
+ ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5663
+ : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5664
+ },
5665
+ ...(blockingDecision === 'stop-escalate'
5666
+ ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5667
+ : {}),
5668
+ }
5669
+ : {
5670
+ scope: 'task',
5671
+ id: activeTask.id,
5672
+ status: activeTask.status,
5673
+ action: 'task plan-risk-audit',
5674
+ revision: activeTask.revision,
5675
+ planRiskAuditRecovery: {
5676
+ state: 'required',
5677
+ reason: error.message,
5678
+ currentBriefHash: activeTask.briefHash,
5679
+ currentPlanHash: activeTask.planHash,
5680
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
5681
+ knowledgeMapHash: activeTask.knowledgeMapHash,
5682
+ },
5683
+ };
5199
5684
  }
5200
5685
  }
5201
5686
  const taskNext = activeTask.status === 'planning'
@@ -5718,6 +6203,9 @@ export class WorkflowService {
5718
6203
  }
5719
6204
  const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
5720
6205
  const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
6206
+ const rootCauseStepId = nextStepId
6207
+ ?? task.steps.find((candidate) => candidate.status === 'planned')?.id
6208
+ ?? null;
5721
6209
  const downstreamProofCarryover = next.action === 'task run'
5722
6210
  && nextStepId
5723
6211
  && changedFiles(repositoryRoot).length > 0
@@ -5728,6 +6216,24 @@ export class WorkflowService {
5728
6216
  && changedFiles(repositoryRoot).length > 0
5729
6217
  ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
5730
6218
  : null;
6219
+ let rootCauseReplanDirtyCarryover = ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
6220
+ && rootCauseStepId
6221
+ && changedFiles(repositoryRoot).length > 0
6222
+ ? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
6223
+ : null;
6224
+ if (rootCauseReplanDirtyCarryover?.eligible) {
6225
+ try {
6226
+ this.assertExecutionAuthorizationFresh(task, repositoryRoot);
6227
+ }
6228
+ catch (error) {
6229
+ rootCauseReplanDirtyCarryover = {
6230
+ applicable: true,
6231
+ eligible: false,
6232
+ compatibility: null,
6233
+ blockers: [error instanceof Error ? error.message : String(error)],
6234
+ };
6235
+ }
6236
+ }
5731
6237
  let mechanicalFeasibilityOverlay = {};
5732
6238
  if (task.status === 'awaiting_execution_authorization' && task.planHash) {
5733
6239
  try {
@@ -6236,9 +6742,42 @@ export class WorkflowService {
6236
6742
  },
6237
6743
  }
6238
6744
  : {};
6745
+ const preExecutionReplanBlockers = task.status === 'ready'
6746
+ ? [
6747
+ ...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may be in progress.'] : []),
6748
+ ...(task.steps.some((step) => step.status === 'failed') ? ['No failed Step may require corrective recovery.'] : []),
6749
+ ...(handoffPosture.state !== 'none' ? ['No C1 claim or pending handoff may exist.'] : []),
6750
+ ...(inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
6751
+ ? ['No active or stale writer lease may exist.']
6752
+ : []),
6753
+ ...(!isClean(repositoryRoot) ? ['Repository checkout must be clean.'] : []),
6754
+ ...(!planRiskAudit || !['approved', 'approved-with-rationale'].includes(planRiskAudit.audit.decision)
6755
+ ? ['A current positive Plan Risk Audit is required.']
6756
+ : []),
6757
+ ]
6758
+ : [];
6759
+ const preExecutionReplanOption = task.status === 'ready' && task.planHash && planRiskAudit
6760
+ ? {
6761
+ command: 'task preexecution-replan',
6762
+ state: preExecutionReplanBlockers.length === 0 ? 'eligible' : 'blocked',
6763
+ expectedRevision: task.revision,
6764
+ expectedBriefHash: task.briefHash,
6765
+ expectedPlanHash: task.planHash,
6766
+ expectedPlanRiskAuditEventHash: planRiskAudit.audit.eventHash,
6767
+ expectedHeadCommit: headCommit(repositoryRoot),
6768
+ auditDecision: 'stop-escalate',
6769
+ actorMustEqualAuditAuditor: true,
6770
+ humanApprovalRequired: false,
6771
+ boundedEffect: 'record-current-plan-obstruction-and-withdraw-execution-authorization',
6772
+ preservesTaskScope: true,
6773
+ preservesCompletedStepAuthority: true,
6774
+ blockers: preExecutionReplanBlockers,
6775
+ }
6776
+ : null;
6239
6777
  const result = {
6240
6778
  ...next,
6241
6779
  ...handoffOverlay,
6780
+ ...(preExecutionReplanOption ? { preExecutionReplanOption } : {}),
6242
6781
  ...(strictStepReviewStep
6243
6782
  ? {
6244
6783
  action: 'task step-review',
@@ -6324,7 +6863,7 @@ export class WorkflowService {
6324
6863
  downstreamProofCarryover: {
6325
6864
  state: 'validated',
6326
6865
  invalidationEventId: downstreamProofCarryover.invalidationEventId,
6327
- stepId: nextStepId,
6866
+ stepId: rootCauseStepId,
6328
6867
  dirtyFiles: downstreamProofCarryover.dirtyFiles,
6329
6868
  dirtyWorktreeHash: downstreamProofCarryover.dirtyWorktreeHash,
6330
6869
  meaning: 'The replacement Step may adopt exactly these preserved uncommitted bytes on task run.',
@@ -6364,6 +6903,52 @@ export class WorkflowService {
6364
6903
  },
6365
6904
  }
6366
6905
  : {}),
6906
+ ...(rootCauseReplanDirtyCarryover?.applicable
6907
+ ? rootCauseReplanDirtyCarryover.eligible && rootCauseReplanDirtyCarryover.compatibility
6908
+ ? {
6909
+ action: 'task run',
6910
+ rootCauseReplanDirtyCarryover: {
6911
+ state: 'validated',
6912
+ stepId: nextStepId,
6913
+ compatibilityHash: rootCauseReplanDirtyCarryover.compatibility.compatibilityHash,
6914
+ dirtyFiles: rootCauseReplanDirtyCarryover.compatibility.dirtyFiles,
6915
+ dirtyWorktreeHash: rootCauseReplanDirtyCarryover.compatibility.dirtyWorktreeHash,
6916
+ meaning: 'The replacement Step may adopt the exact retained root-cause failure bytes.',
6917
+ },
6918
+ }
6919
+ : rootCauseReplanDirtyCarryover.blockers.length === 1
6920
+ && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1')
6921
+ ? handoffPosture.state === 'none'
6922
+ ? {
6923
+ action: 'task handoff-prepare',
6924
+ blockedAction: 'task run',
6925
+ rootCauseReplanDirtyCarryover: {
6926
+ state: 'awaiting-c1',
6927
+ stepId: rootCauseStepId,
6928
+ requiredActor: defaultTaskWorkerActor(task.id),
6929
+ },
6930
+ }
6931
+ : handoffPosture.state === 'pending'
6932
+ ? {}
6933
+ : {
6934
+ action: 'doctor',
6935
+ blockedAction: 'task run',
6936
+ rootCauseReplanDirtyCarryover: {
6937
+ state: 'blocked',
6938
+ stepId: rootCauseStepId,
6939
+ blockers: rootCauseReplanDirtyCarryover.blockers,
6940
+ },
6941
+ }
6942
+ : {
6943
+ action: 'doctor',
6944
+ blockedAction: 'task run',
6945
+ rootCauseReplanDirtyCarryover: {
6946
+ state: 'blocked',
6947
+ stepId: rootCauseStepId,
6948
+ blockers: rootCauseReplanDirtyCarryover.blockers,
6949
+ },
6950
+ }
6951
+ : {}),
6367
6952
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6368
6953
  ...(correctiveDerivedAuthority && correctiveDerivedActor
6369
6954
  ? {
@@ -6380,7 +6965,10 @@ export class WorkflowService {
6380
6965
  }
6381
6966
  : {}),
6382
6967
  };
6383
- if (handoffPosture.state === 'pending') {
6968
+ const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
6969
+ || (rootCauseReplanDirtyCarryover.blockers.length === 1
6970
+ && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
6971
+ if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
6384
6972
  return this.withTaskNavigationContracts(task, {
6385
6973
  ...result,
6386
6974
  action: 'task claim',
@@ -6414,6 +7002,9 @@ export class WorkflowService {
6414
7002
  };
6415
7003
  }
6416
7004
  assertTaskContextChangeAllowed(task) {
7005
+ if (readCurrentPreExecutionReplan(this.store, task)) {
7006
+ 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' });
7007
+ }
6417
7008
  if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
6418
7009
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
6419
7010
  taskId: task.id,
@@ -6899,7 +7490,7 @@ export class WorkflowService {
6899
7490
  blockers,
6900
7491
  };
6901
7492
  }
6902
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
7493
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null, rootCauseReplan = null) {
6903
7494
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
6904
7495
  const now = this.now().getTime();
6905
7496
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
@@ -6919,7 +7510,43 @@ export class WorkflowService {
6919
7510
  currentBranch: versions.currentBranch,
6920
7511
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
6921
7512
  },
6922
- }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
7513
+ }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
7514
+ }
7515
+ assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId) {
7516
+ const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
7517
+ && task.systemCommits.includes(record.commitSha))?.rootCauseReplan ?? null;
7518
+ if (recorded) {
7519
+ if (stepId && stepId !== recorded.stepId) {
7520
+ return {
7521
+ applicable: true,
7522
+ eligible: false,
7523
+ compatibility: null,
7524
+ blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
7525
+ };
7526
+ }
7527
+ return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
7528
+ binding: recorded,
7529
+ });
7530
+ }
7531
+ const candidates = task.steps
7532
+ .filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
7533
+ .flatMap((step) => [...new Set(readRemediationEvents(this.store, task)
7534
+ .filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed')
7535
+ .map((event) => event.failureEvidence.completionCommit))]
7536
+ .map((historyHead) => assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead })))
7537
+ .filter((assessment) => assessment.applicable);
7538
+ const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
7539
+ if (eligible.length === 1)
7540
+ return eligible[0];
7541
+ return {
7542
+ applicable: candidates.length > 0,
7543
+ eligible: false,
7544
+ compatibility: null,
7545
+ blockers: [...new Set([
7546
+ ...candidates.flatMap((assessment) => assessment.blockers),
7547
+ ...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
7548
+ ])],
7549
+ };
6923
7550
  }
6924
7551
  inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
6925
7552
  const now = this.now().getTime();
@@ -7623,6 +8250,83 @@ function taskHasStarted(task) {
7623
8250
  'merged',
7624
8251
  ].includes(task.status);
7625
8252
  }
8253
+ function resolvePreExecutionReplanAudits(store, task, marker) {
8254
+ const audits = readPlanRiskAuditEvents(store, task);
8255
+ const sourceAudit = audits.find((audit) => audit.eventId === marker.sourcePlanRiskAuditEventId
8256
+ && audit.eventHash === marker.sourcePlanRiskAuditEventHash) ?? null;
8257
+ const obstructionAudit = audits.find((audit) => audit.eventId === marker.obstructionPlanRiskAuditEventId
8258
+ && audit.eventHash === marker.obstructionPlanRiskAuditEventHash) ?? null;
8259
+ const sourceAuthorization = task.authorizations.find((authorization) => authorization.kind === 'execution'
8260
+ && authorization.decision === 'superseded'
8261
+ && hashExecutionAuthorization({ ...authorization, decision: 'approved' }) === marker.sourceExecutionAuthorizationHash);
8262
+ if (!sourceAudit || !obstructionAudit || audits.at(-1)?.eventHash !== obstructionAudit.eventHash
8263
+ || !['approved', 'approved-with-rationale'].includes(sourceAudit.decision)
8264
+ || obstructionAudit.decision !== 'stop-escalate'
8265
+ || sourceAudit.planHash !== task.planHash || obstructionAudit.planHash !== task.planHash
8266
+ || !sourceAuthorization) {
8267
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan provenance chain is incomplete or inconsistent.', {
8268
+ taskId: task.id,
8269
+ eventId: marker.eventId,
8270
+ });
8271
+ }
8272
+ return { sourceAudit, obstructionAudit };
8273
+ }
8274
+ function assertPlanRiskSafetyPreserved(source, candidate, label) {
8275
+ const candidateByStep = new Map(candidate.stepClassifications.map((classification) => [classification.stepId, classification]));
8276
+ const missing = [];
8277
+ for (const sourceClassification of source.stepClassifications) {
8278
+ const candidateClassification = candidateByStep.get(sourceClassification.stepId);
8279
+ if (!candidateClassification) {
8280
+ missing.push({ stepId: sourceClassification.stepId, field: 'classification', values: [] });
8281
+ continue;
8282
+ }
8283
+ const preserveStrings = (field, sourceValues, candidateValues) => {
8284
+ const absent = sourceValues.filter((value) => !candidateValues.includes(value));
8285
+ if (absent.length > 0)
8286
+ missing.push({ stepId: sourceClassification.stepId, field, values: absent });
8287
+ };
8288
+ preserveStrings('categories', sourceClassification.categories, candidateClassification.categories);
8289
+ preserveStrings('failureModes', sourceClassification.failureModes, candidateClassification.failureModes);
8290
+ preserveStrings('requiredEvidence', sourceClassification.requiredEvidence, candidateClassification.requiredEvidence);
8291
+ if (sourceClassification.reviewRequired && !candidateClassification.reviewRequired) {
8292
+ missing.push({ stepId: sourceClassification.stepId, field: 'reviewRequired', values: [true] });
8293
+ }
8294
+ const proofBinding = (proof) => canonicalJsonStringify({ failureMode: proof.failureMode, evidence: proof.evidence });
8295
+ const candidateProofs = new Set((candidateClassification.proofObligations ?? []).map(proofBinding));
8296
+ const absentProofs = (sourceClassification.proofObligations ?? [])
8297
+ .filter((proof) => !candidateProofs.has(proofBinding(proof)));
8298
+ if (absentProofs.length > 0) {
8299
+ missing.push({ stepId: sourceClassification.stepId, field: 'proofObligations', values: absentProofs });
8300
+ }
8301
+ }
8302
+ if (missing.length > 0) {
8303
+ throw new WorkflowError('TRANSITION_BLOCKED', `${label} cannot suppress existing guarded risk or proof authority.`, {
8304
+ missing,
8305
+ });
8306
+ }
8307
+ const sourceProofText = new Map(source.stepClassifications.flatMap((classification) => (classification.proofObligations ?? []).map((proof) => [
8308
+ canonicalJsonStringify({ stepId: classification.stepId, failureMode: proof.failureMode, evidence: proof.evidence }),
8309
+ canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult }),
8310
+ ])));
8311
+ const revisedProofText = candidate.stepClassifications.some((classification) => (classification.proofObligations ?? []).some((proof) => {
8312
+ const binding = canonicalJsonStringify({
8313
+ stepId: classification.stepId,
8314
+ failureMode: proof.failureMode,
8315
+ evidence: proof.evidence,
8316
+ });
8317
+ const previous = sourceProofText.get(binding);
8318
+ return previous !== undefined
8319
+ && previous !== canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult });
8320
+ }));
8321
+ if (revisedProofText && (candidate.decision !== 'approved-with-rationale'
8322
+ || candidate.summary.trim() === source.summary.trim())) {
8323
+ throw new WorkflowError('TRANSITION_BLOCKED', `${label} may revise proof prose only with approved-with-rationale and a new audit summary.`, {
8324
+ sourceSummary: source.summary,
8325
+ candidateSummary: candidate.summary,
8326
+ candidateDecision: candidate.decision,
8327
+ });
8328
+ }
8329
+ }
7626
8330
  function taskCanRebindKnowledge(task) {
7627
8331
  return [
7628
8332
  'awaiting_execution_authorization',