codex-workflow-v2 2.0.0-beta.13.2 → 2.0.0-beta.13.5

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +18 -10
  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 -2
  10. package/dist/src/alpha6/remediation.js +155 -20
  11. package/dist/src/alpha6/remediation.js.map +1 -1
  12. package/dist/src/cli-actions.d.ts +1 -1
  13. package/dist/src/cli-actions.js +1 -0
  14. package/dist/src/cli-actions.js.map +1 -1
  15. package/dist/src/cli.js +44 -0
  16. package/dist/src/cli.js.map +1 -1
  17. package/dist/src/domain/plan-semantics.d.ts +11 -0
  18. package/dist/src/domain/plan-semantics.js +49 -0
  19. package/dist/src/domain/plan-semantics.js.map +1 -0
  20. package/dist/src/state/corrective-yield-executor.js +10 -1
  21. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  22. package/dist/src/version.d.ts +1 -1
  23. package/dist/src/version.js +1 -1
  24. package/dist/src/workflow.d.ts +19 -1
  25. package/dist/src/workflow.js +518 -84
  26. package/dist/src/workflow.js.map +1 -1
  27. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  28. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  29. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  30. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
  31. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
  32. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +3 -3
  33. package/docs/release.md +59 -2
  34. package/docs/updating-existing-project.md +10 -2
  35. package/package.json +1 -1
  36. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +128 -0
  37. package/plugins/codex-workflow-gateway/references/protocol.md +4 -0
  38. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +185 -0
  39. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
  40. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +50 -3
  41. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +63 -6
  42. package/schemas/preexecution-replan-event.schema.json +45 -0
@@ -8,6 +8,7 @@ import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, }
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,9 @@ 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, 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 { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
22
24
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
23
25
  import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
24
26
  import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
@@ -28,7 +30,7 @@ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, en
28
30
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
29
31
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
30
32
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
31
- import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
33
+ import { canonicalJsonStringify, prepareSidecarAppend, sha256Hex } from './alpha6/store-sidecars.js';
32
34
  import { ensureDirectory } from './fs-utils.js';
33
35
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
34
36
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
@@ -47,9 +49,11 @@ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
47
49
  export class WorkflowService {
48
50
  store;
49
51
  now;
50
- constructor(store = new FileStateStore(), now = () => new Date()) {
52
+ transactionHooks;
53
+ constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
51
54
  this.store = store;
52
55
  this.now = now;
56
+ this.transactionHooks = transactionHooks;
53
57
  }
54
58
  #mutationContext(repository) {
55
59
  const identity = resolveRepositoryIdentity(repository);
@@ -1601,6 +1605,10 @@ export class WorkflowService {
1601
1605
  this.validateKnowledgeTargets(knowledgeMap, plan);
1602
1606
  const task = this.store.readTask(identity.projectId, taskId);
1603
1607
  assertExpectedRevision(task, expectedRevision);
1608
+ const preExecutionReplan = readCurrentPreExecutionReplan(this.store, task);
1609
+ const preExecutionReplanAudits = preExecutionReplan
1610
+ ? resolvePreExecutionReplanAudits(this.store, task, preExecutionReplan)
1611
+ : null;
1604
1612
  const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
1605
1613
  const currentPlanCorrectiveDecisions = task.planHash
1606
1614
  ? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
@@ -1747,6 +1755,9 @@ export class WorkflowService {
1747
1755
  }
1748
1756
  const root = this.store.taskRoot(identity.projectId, taskId);
1749
1757
  const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
1758
+ const changesPlanSemantics = !task.planHash
1759
+ || normalizePlanKnowledgeBinding(preparedPlan.planBytes, 'candidate').semanticHash
1760
+ !== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(root, task.planHash, task.planHash), 'current').semanticHash;
1750
1761
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
1751
1762
  ? { ...authorization, decision: 'superseded' }
1752
1763
  : authorization);
@@ -1764,6 +1775,83 @@ export class WorkflowService {
1764
1775
  ...(task.invalidatedStepCommits ?? []),
1765
1776
  ...omittedStepEvidenceCommits(task, plannedSteps),
1766
1777
  ])];
1778
+ if (preExecutionReplan && preExecutionReplanAudits) {
1779
+ const replanC1Posture = readTaskC1Posture(this.store, task);
1780
+ const replanWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
1781
+ if (replanC1Posture.state !== 'none' || replanWriterLease) {
1782
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replacement Plan requires no C1 posture or writer lease.', {
1783
+ taskId: task.id,
1784
+ c1Posture: replanC1Posture.state,
1785
+ writerLeasePresent: replanWriterLease !== null,
1786
+ });
1787
+ }
1788
+ if (!planRiskAudit) {
1789
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires an atomic replacement Plan Risk Audit.');
1790
+ }
1791
+ if (task.planObjective !== plan.objective
1792
+ || canonicalJsonStringify(task.requirements) !== canonicalJsonStringify(plan.requirements)
1793
+ || canonicalJsonStringify(task.acceptance) !== canonicalJsonStringify(plan.acceptance)) {
1794
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot change Task objective, requirements, or acceptance.', {
1795
+ taskId: task.id,
1796
+ });
1797
+ }
1798
+ if (!changesPlanSemantics) {
1799
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic Plan change.', { taskId: task.id });
1800
+ }
1801
+ const candidateSteps = new Map(plan.steps.map((step) => [step.id, step]));
1802
+ const missingStepIds = task.steps.filter((step) => !candidateSteps.has(step.id)).map((step) => step.id);
1803
+ if (missingStepIds.length > 0) {
1804
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must retain every existing Step identifier.', {
1805
+ taskId: task.id,
1806
+ missingStepIds,
1807
+ });
1808
+ }
1809
+ const completedStepIds = task.steps.filter((step) => step.status === 'completed').map((step) => step.id);
1810
+ const lostCompletedStepIds = completedStepIds.filter((stepId) => !retainedCompletedSteps.has(stepId));
1811
+ if (lostCompletedStepIds.length > 0) {
1812
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must preserve the exact completed Step subgraph and evidence.', {
1813
+ taskId: task.id,
1814
+ lostCompletedStepIds,
1815
+ });
1816
+ }
1817
+ const weakenedForbiddenScope = task.steps.flatMap((step) => {
1818
+ const candidate = candidateSteps.get(step.id);
1819
+ const missing = step.forbiddenScope.filter((scope) => !candidate.forbiddenScope.includes(scope));
1820
+ return missing.length > 0 ? [{ stepId: step.id, missing }] : [];
1821
+ });
1822
+ if (weakenedForbiddenScope.length > 0) {
1823
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken existing forbiddenScope boundaries.', {
1824
+ taskId: task.id,
1825
+ weakenedForbiddenScope,
1826
+ });
1827
+ }
1828
+ const missingRisks = task.risks.filter((risk) => !plan.risks.includes(risk));
1829
+ if (missingRisks.length > 0) {
1830
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot suppress existing Plan risks.', {
1831
+ taskId: task.id,
1832
+ missingRisks,
1833
+ });
1834
+ }
1835
+ assertPlanRiskSafetyPreserved(preExecutionReplanAudits.obstructionAudit, planRiskAudit, 'Replacement Plan Risk Audit');
1836
+ const candidateEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => canonicalJsonStringify(edge)));
1837
+ const missingProductionEdges = (task.productionEdges ?? [])
1838
+ .filter((edge) => !candidateEdgeKeys.has(canonicalJsonStringify(edge)));
1839
+ if ((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
1840
+ || missingProductionEdges.length > 0) {
1841
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken production-edge authority.', {
1842
+ taskId: task.id,
1843
+ missingProductionEdges,
1844
+ });
1845
+ }
1846
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== preExecutionReplan.sourceHeadCommit) {
1847
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replacement Plan requires the exact clean recorded HEAD.', {
1848
+ taskId: task.id,
1849
+ expectedHead: preExecutionReplan.sourceHeadCommit,
1850
+ actualHead: headCommit(identity.repositoryRoot),
1851
+ changedFiles: changedFiles(identity.repositoryRoot),
1852
+ });
1853
+ }
1854
+ }
1767
1855
  if (task.status === 'needs_fix' &&
1768
1856
  task.review.status === 'failed' &&
1769
1857
  plannedSteps.every((step) => step.status === 'completed')) {
@@ -1787,7 +1875,7 @@ export class WorkflowService {
1787
1875
  : []),
1788
1876
  ...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
1789
1877
  ...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
1790
- ...(preparedPlan.planHash === task.planHash
1878
+ ...(!changesPlanSemantics
1791
1879
  ? ['task.rolling-replan.semantic-plan-change']
1792
1880
  : []),
1793
1881
  ...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
@@ -1860,6 +1948,61 @@ export class WorkflowService {
1860
1948
  };
1861
1949
  if (planRiskAudit)
1862
1950
  validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
1951
+ if (preExecutionReplan && preExecutionReplanAudits && planRiskAudit) {
1952
+ const now = this.now();
1953
+ const resultTask = {
1954
+ ...candidate,
1955
+ revision: task.revision + 1,
1956
+ updatedAt: now.toISOString(),
1957
+ };
1958
+ const preparedAuditBase = buildCandidatePlanRiskAuditEvent(this.store, resultTask, planRiskAudit, now.toISOString(), now.getTime());
1959
+ const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
1960
+ const projectRoot = this.store.projectRoot(task.projectId);
1961
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
1962
+ const versionFile = path.join(root, '.versions', `plan.md.${preparedPlan.planHash}`);
1963
+ if (existsSync(versionFile) && sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash) {
1964
+ throw new WorkflowError('STATE_CORRUPT', 'Immutable replacement Plan version is corrupt.', {
1965
+ taskId: task.id,
1966
+ planHash: preparedPlan.planHash,
1967
+ });
1968
+ }
1969
+ applyProjectTransaction({
1970
+ projectRoot,
1971
+ projectId: task.projectId,
1972
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
1973
+ kind: 'preexecution-replacement-plan-set',
1974
+ now,
1975
+ targets: [
1976
+ { path: relative(path.join(root, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
1977
+ { path: relative(path.join(root, 'plan.md')), content: preparedPlan.planBytes },
1978
+ { path: relative(versionFile), content: preparedPlan.planBytes },
1979
+ { path: relative(path.join(root, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
1980
+ ],
1981
+ ...(this.transactionHooks.afterPreExecutionReplanTargetWrite
1982
+ ? { afterTargetWrite: this.transactionHooks.afterPreExecutionReplanTargetWrite }
1983
+ : {}),
1984
+ validate: () => {
1985
+ const saved = this.store.readTask(task.projectId, task.id);
1986
+ const currentAudit = readCurrentPlanRiskAudit(this.store, saved, { requireCurrentTaskRevision: true });
1987
+ if (saved.revision !== resultTask.revision
1988
+ || saved.status !== 'awaiting_execution_authorization'
1989
+ || saved.planHash !== preparedPlan.planHash
1990
+ || this.store.hashArtifact(root, 'plan.md') !== preparedPlan.planHash
1991
+ || sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash
1992
+ || currentAudit?.audit.eventHash !== preparedAudit.event.eventHash
1993
+ || hashCompletedSteps(saved) !== preExecutionReplan.completedStepsHash
1994
+ || readCurrentPreExecutionReplan(this.store, saved) !== null
1995
+ || saved.authorizations.some((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved')) {
1996
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replacement Plan transaction readback failed.', {
1997
+ taskId: task.id,
1998
+ });
1999
+ }
2000
+ if (saved.baseCommit)
2001
+ validateTaskHistory(identity.repositoryRoot, saved);
2002
+ },
2003
+ });
2004
+ return this.store.readTask(task.projectId, task.id);
2005
+ }
1863
2006
  const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
1864
2007
  if (stagedPlan.hash !== preparedPlan.planHash) {
1865
2008
  throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
@@ -2068,6 +2211,9 @@ export class WorkflowService {
2068
2211
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2069
2212
  const task = this.store.readTask(identity.projectId, taskId);
2070
2213
  assertExpectedRevision(task, expectedRevision);
2214
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2215
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic replacement Plan before execution authorization.', { taskId, requiredAction: 'task plan-set' });
2216
+ }
2071
2217
  if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
2072
2218
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting execution authorization.`);
2073
2219
  }
@@ -2125,9 +2271,143 @@ export class WorkflowService {
2125
2271
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2126
2272
  const task = this.store.readTask(identity.projectId, taskId);
2127
2273
  assertExpectedRevision(task, expectedRevision);
2274
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2275
+ 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' });
2276
+ }
2128
2277
  this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
2129
2278
  return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
2130
2279
  }
2280
+ recordPreExecutionReplan(repository, taskId, expectedRevision, actor, reason, binding, input) {
2281
+ const context = this.#mutationContext(repository);
2282
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
2283
+ const task = this.store.readTask(context.identity.projectId, taskId);
2284
+ assertExpectedRevision(task, expectedRevision);
2285
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2286
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task already has a pending pre-execution replan.', {
2287
+ taskId,
2288
+ requiredAction: 'task plan-set',
2289
+ });
2290
+ }
2291
+ if (task.status !== 'ready' || task.steps.some((step) => ['in_progress', 'failed'].includes(step.status))) {
2292
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a ready Task with no Step in progress or failed.', {
2293
+ taskId,
2294
+ status: task.status,
2295
+ });
2296
+ }
2297
+ if (!isClean(context.identity.repositoryRoot)) {
2298
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replan requires a clean checkout.', {
2299
+ changedFiles: changedFiles(context.identity.repositoryRoot),
2300
+ });
2301
+ }
2302
+ const c1Posture = readTaskC1Posture(this.store, task);
2303
+ const writerLease = context.locks.inspect(task.id);
2304
+ if (c1Posture.state !== 'none' || writerLease) {
2305
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a clean authority boundary without C1 or writer lease.', {
2306
+ taskId,
2307
+ c1Posture: c1Posture.state,
2308
+ writerLeasePresent: writerLease !== null,
2309
+ });
2310
+ }
2311
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2312
+ if (task.baseCommit) {
2313
+ validateTaskHistory(context.identity.repositoryRoot, task);
2314
+ }
2315
+ else if (task.workspaceOwner !== null || task.taskBranch !== null
2316
+ || task.steps.some((step) => step.status === 'completed')) {
2317
+ throw new WorkflowError('STATE_CORRUPT', 'Unstarted pre-execution replan Task has inconsistent execution history.', {
2318
+ taskId,
2319
+ });
2320
+ }
2321
+ const authorization = this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2322
+ const sourceAudit = readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: false });
2323
+ if (!sourceAudit || !['approved', 'approved-with-rationale'].includes(sourceAudit.audit.decision)) {
2324
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires the current positive Plan Risk Audit.');
2325
+ }
2326
+ const currentHead = headCommit(context.identity.repositoryRoot);
2327
+ const actualBinding = {
2328
+ expectedBriefHash: task.briefHash,
2329
+ expectedPlanHash: task.planHash,
2330
+ expectedPlanRiskAuditEventHash: sourceAudit.audit.eventHash,
2331
+ expectedHeadCommit: currentHead,
2332
+ };
2333
+ if (canonicalJsonStringify(binding) !== canonicalJsonStringify(actualBinding)) {
2334
+ throw new WorkflowError('STATE_CONFLICT', 'Pre-execution replan source binding is stale.', {
2335
+ expected: binding,
2336
+ actual: actualBinding,
2337
+ });
2338
+ }
2339
+ if (input.decision !== 'stop-escalate') {
2340
+ throw new WorkflowError('INVALID_ARGUMENT', 'Pre-execution replan requires a stop-escalate Plan Risk Audit.');
2341
+ }
2342
+ if (actor.trim() !== input.auditor.trim() || input.planner.trim() !== sourceAudit.audit.planner) {
2343
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must be recorded by the independent auditor against the original planner.', {
2344
+ actor: actor.trim(),
2345
+ auditor: input.auditor.trim(),
2346
+ expectedPlanner: sourceAudit.audit.planner,
2347
+ actualPlanner: input.planner.trim(),
2348
+ });
2349
+ }
2350
+ assertPlanRiskSafetyPreserved(sourceAudit.audit, input, 'Pre-execution replan audit');
2351
+ const now = this.now();
2352
+ const preparedAuditBase = buildPlanRiskAuditEvent(this.store, task, input, now.toISOString(), now.getTime());
2353
+ const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
2354
+ const preparedMarker = preparePreExecutionReplanEvent(this.store, task, {
2355
+ sourceAudit: sourceAudit.audit,
2356
+ obstructionAudit: preparedAudit.event,
2357
+ authorization,
2358
+ sourceHeadCommit: currentHead,
2359
+ actor,
2360
+ reason,
2361
+ }, now);
2362
+ const resultTask = {
2363
+ ...task,
2364
+ revision: task.revision + 1,
2365
+ updatedAt: now.toISOString(),
2366
+ status: 'awaiting_execution_authorization',
2367
+ authorizations: task.authorizations.map((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved'
2368
+ ? { ...candidate, decision: 'superseded' }
2369
+ : candidate),
2370
+ blockReason: reason.trim(),
2371
+ };
2372
+ const projectRoot = this.store.projectRoot(task.projectId);
2373
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2374
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2375
+ applyProjectTransaction({
2376
+ projectRoot,
2377
+ projectId: task.projectId,
2378
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2379
+ kind: 'preexecution-plan-authority-withdrawal',
2380
+ now,
2381
+ targets: [
2382
+ { path: relative(path.join(taskRoot, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
2383
+ { path: relative(path.join(taskRoot, preExecutionReplanSidecar())), content: preparedMarker.postimage },
2384
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
2385
+ ],
2386
+ validate: () => {
2387
+ const saved = this.store.readTask(task.projectId, task.id);
2388
+ const latestAudit = readPlanRiskAuditEvents(this.store, saved).at(-1) ?? null;
2389
+ const marker = readCurrentPreExecutionReplan(this.store, saved);
2390
+ if (saved.revision !== resultTask.revision
2391
+ || saved.status !== 'awaiting_execution_authorization'
2392
+ || saved.planHash !== task.planHash
2393
+ || latestAudit?.eventHash !== preparedAudit.event.eventHash
2394
+ || marker?.eventHash !== preparedMarker.event.eventHash
2395
+ || saved.authorizations.some((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved')
2396
+ || hashCompletedSteps(saved) !== preparedMarker.event.completedStepsHash
2397
+ || headCommit(context.identity.repositoryRoot) !== currentHead
2398
+ || !isClean(context.identity.repositoryRoot)) {
2399
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan transaction readback failed.', { taskId });
2400
+ }
2401
+ if (saved.baseCommit)
2402
+ validateTaskHistory(context.identity.repositoryRoot, saved);
2403
+ },
2404
+ });
2405
+ return {
2406
+ task: this.store.readTask(task.projectId, task.id),
2407
+ audit: preparedAudit.event,
2408
+ event: preparedMarker.event,
2409
+ };
2410
+ }
2131
2411
  showTaskHandoff(repository, taskId) {
2132
2412
  const { identity } = this.#mutationContext(repository);
2133
2413
  const task = this.store.readTask(identity.projectId, taskId);
@@ -2141,6 +2421,12 @@ export class WorkflowService {
2141
2421
  const context = this.#mutationContext(repository);
2142
2422
  const task = this.store.readTask(context.identity.projectId, taskId);
2143
2423
  assertExpectedRevision(task, expectedRevision);
2424
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2425
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
2426
+ taskId,
2427
+ requiredAction: 'task plan-set',
2428
+ });
2429
+ }
2144
2430
  const normalizedActor = actor.trim();
2145
2431
  if (!normalizedActor) {
2146
2432
  throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
@@ -2748,7 +3034,7 @@ export class WorkflowService {
2748
3034
  const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
2749
3035
  ? stepHistory
2750
3036
  : [...stepHistory, preparedRemediation.event];
2751
- const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3037
+ const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
2752
3038
  const c1Posture = readTaskC1Posture(this.store, task);
2753
3039
  const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
2754
3040
  ? prepareTaskCorrectiveYield(this.store, paused, {
@@ -3062,7 +3348,7 @@ export class WorkflowService {
3062
3348
  const resultingHistory = preparedRemediation.postimage === null
3063
3349
  ? stepHistory
3064
3350
  : [...stepHistory, preparedRemediation.event];
3065
- const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3351
+ const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
3066
3352
  const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
3067
3353
  const paused = {
3068
3354
  ...resolved,
@@ -3086,7 +3372,7 @@ export class WorkflowService {
3086
3372
  ? prepareTaskCorrectiveYield(this.store, paused, {
3087
3373
  actor: c1Posture.claimant,
3088
3374
  writerToken: reviewLease.token,
3089
- reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
3375
+ reason: `Yield execution authority after authoritative replan cause ${preparedRemediation.event.eventId}.`,
3090
3376
  eventId: `HOF-${sha256Hex([
3091
3377
  'strict-review-root-cause-replan-pause-v1',
3092
3378
  task.id,
@@ -3809,6 +4095,9 @@ export class WorkflowService {
3809
4095
  const now = this.now().getTime();
3810
4096
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
3811
4097
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4098
+ const pendingPreExecutionReplanTaskIds = tasks
4099
+ .filter((task) => readCurrentPreExecutionReplan(this.store, task) !== null)
4100
+ .map((task) => task.id);
3812
4101
  const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
3813
4102
  const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
3814
4103
  const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
@@ -3836,6 +4125,9 @@ export class WorkflowService {
3836
4125
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
3837
4126
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
3838
4127
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
4128
+ ...(pendingPreExecutionReplanTaskIds.length > 0
4129
+ ? [`Complete the pending pre-execution Plan replacement and reauthorization first: ${pendingPreExecutionReplanTaskIds.join(', ')}.`]
4130
+ : []),
3839
4131
  ...versionBlockers,
3840
4132
  ...observationPreflightBlockers(snapshot.observation),
3841
4133
  ];
@@ -3861,10 +4153,12 @@ export class WorkflowService {
3861
4153
  milestoneBases,
3862
4154
  },
3863
4155
  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.',
4156
+ safe: versionBlockers.length === 0 && pendingPreExecutionReplanTaskIds.length === 0,
4157
+ requiredAction: pendingPreExecutionReplanTaskIds.length > 0
4158
+ ? 'Complete each pending pre-execution task plan-set and reauthorization before changing Workflow dependency HEAD.'
4159
+ : versionBlockers.length === 0
4160
+ ? null
4161
+ : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
3868
4162
  },
3869
4163
  adoption: snapshot.adoption,
3870
4164
  observation: snapshot.observation,
@@ -5071,6 +5365,11 @@ export class WorkflowService {
5071
5365
  throw error;
5072
5366
  const blockingDecision = error.details.decision;
5073
5367
  const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
5368
+ const preExecutionReplan = readCurrentPreExecutionReplan(this.store, activeTask);
5369
+ const verifiedPreExecutionReplan = preExecutionReplan
5370
+ && blockingAudit?.eventHash === preExecutionReplan.obstructionPlanRiskAuditEventHash
5371
+ ? resolvePreExecutionReplanAudits(this.store, activeTask, preExecutionReplan)
5372
+ : null;
5074
5373
  const existingRemediationEvent = blockingAudit && activeTask.milestoneId
5075
5374
  ? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
5076
5375
  .reverse()
@@ -5104,50 +5403,34 @@ export class WorkflowService {
5104
5403
  const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
5105
5404
  .filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
5106
5405
  .sort();
5107
- planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
5406
+ planRiskAuditRecovery = verifiedPreExecutionReplan && preExecutionReplan
5108
5407
  ? {
5109
5408
  scope: 'task',
5110
5409
  id: activeTask.id,
5111
5410
  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',
5411
+ action: 'task plan-set',
5121
5412
  blockedAction: 'task authorize',
5122
5413
  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.',
5414
+ preExecutionReplan: {
5415
+ state: 'recorded',
5416
+ eventId: preExecutionReplan.eventId,
5417
+ eventHash: preExecutionReplan.eventHash,
5418
+ sourcePlanHash: preExecutionReplan.sourcePlanHash,
5419
+ sourceHeadCommit: preExecutionReplan.sourceHeadCommit,
5420
+ obstructionPlanRiskAuditEventId: preExecutionReplan.obstructionPlanRiskAuditEventId,
5421
+ obstructionPlanRiskAuditEventHash: preExecutionReplan.obstructionPlanRiskAuditEventHash,
5422
+ preservesTaskScope: true,
5423
+ preservesCompletedStepAuthority: true,
5424
+ requiredAction: 'Replace the obstructed implementation Plan through ordinary task plan-set.',
5128
5425
  },
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'
5426
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5427
+ }
5428
+ : blockingDecision === 'stop-escalate' && auditedRemediationEligible
5429
+ ? {
5430
+ scope: 'task',
5431
+ id: activeTask.id,
5432
+ status: activeTask.status,
5433
+ action: existingRemediationTask?.status === 'merged'
5151
5434
  ? 'task plan-set'
5152
5435
  : existingRemediationTask
5153
5436
  ? 'doctor'
@@ -5156,46 +5439,84 @@ export class WorkflowService {
5156
5439
  : remediationDiscovery
5157
5440
  ? 'discovery update'
5158
5441
  : '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
5442
  blockedAction: 'task authorize',
5171
5443
  revision: activeTask.revision,
5172
5444
  planRiskAuditDecision: {
5173
5445
  state: 'recorded-blocking',
5174
5446
  decision: blockingDecision,
5175
5447
  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.',
5448
+ requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
5179
5449
  },
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,
5450
+ auditedRemediation: {
5451
+ state: existingRemediationTask?.status === 'merged'
5452
+ ? 'merged-replan-required'
5453
+ : existingRemediationTask
5454
+ ? 'materialized-not-merged'
5455
+ : remediationDiscovery?.status === 'ready_to_materialize'
5456
+ ? 'ready-to-materialize'
5457
+ : 'discovery-required',
5458
+ humanApprovalRequired: false,
5459
+ blockedTaskId: activeTask.id,
5460
+ blockedTaskRevision: activeTask.revision,
5461
+ milestoneId: remediationMilestone.id,
5462
+ milestoneRevision: remediationMilestone.revision,
5463
+ planRiskAuditEventId: blockingAudit?.eventId ?? null,
5464
+ planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
5465
+ discoveryId: remediationDiscovery?.id ?? null,
5466
+ discoveryRevision: remediationDiscovery?.revision ?? null,
5467
+ remediationTaskId: existingRemediationTask?.id ?? null,
5468
+ remediationTaskStatus: existingRemediationTask?.status ?? null,
5469
+ requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
5470
+ eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
5471
+ command: existingRemediationTask?.status === 'merged'
5472
+ ? 'task plan-set'
5473
+ : existingRemediationTask
5474
+ ? 'doctor'
5475
+ : remediationDiscovery?.status === 'ready_to_materialize'
5476
+ ? 'milestone remediation-materialize'
5477
+ : remediationDiscovery
5478
+ ? 'discovery update'
5479
+ : 'discovery start',
5480
+ boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5481
+ preservesMilestoneSemantics: true,
5197
5482
  },
5198
- };
5483
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5484
+ }
5485
+ : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5486
+ ? {
5487
+ scope: 'task',
5488
+ id: activeTask.id,
5489
+ status: activeTask.status,
5490
+ action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5491
+ blockedAction: 'task authorize',
5492
+ revision: activeTask.revision,
5493
+ planRiskAuditDecision: {
5494
+ state: 'recorded-blocking',
5495
+ decision: blockingDecision,
5496
+ reason: error.message,
5497
+ requiredAction: blockingDecision === 'stop-escalate'
5498
+ ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5499
+ : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5500
+ },
5501
+ ...(blockingDecision === 'stop-escalate'
5502
+ ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5503
+ : {}),
5504
+ }
5505
+ : {
5506
+ scope: 'task',
5507
+ id: activeTask.id,
5508
+ status: activeTask.status,
5509
+ action: 'task plan-risk-audit',
5510
+ revision: activeTask.revision,
5511
+ planRiskAuditRecovery: {
5512
+ state: 'required',
5513
+ reason: error.message,
5514
+ currentBriefHash: activeTask.briefHash,
5515
+ currentPlanHash: activeTask.planHash,
5516
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
5517
+ knowledgeMapHash: activeTask.knowledgeMapHash,
5518
+ },
5519
+ };
5199
5520
  }
5200
5521
  }
5201
5522
  const taskNext = activeTask.status === 'planning'
@@ -6071,8 +6392,8 @@ export class WorkflowService {
6071
6392
  ...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
6072
6393
  ? [`${guardedCorrectivePosture.stepId} must remain failed.`]
6073
6394
  : []),
6074
- ...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
6075
- ...(rollingC1Posture.state !== 'none'
6395
+ ...(writerLease && rollingC1Posture.state !== 'claimed' ? ['No active or stale writer lease may remain after the failure pause.'] : []),
6396
+ ...(rollingC1Posture.state !== 'none' && rollingC1Posture.state !== 'claimed'
6076
6397
  ? ['No C1 claim may remain after the root-cause failure pause.']
6077
6398
  : []),
6078
6399
  ];
@@ -6089,7 +6410,7 @@ export class WorkflowService {
6089
6410
  },
6090
6411
  }
6091
6412
  : {
6092
- action: 'task plan-set',
6413
+ action: rollingC1Posture.state === 'claimed' ? 'task corrective-yield' : 'task plan-set',
6093
6414
  blockedAction: 'task run',
6094
6415
  stepId: guardedCorrectivePosture.stepId,
6095
6416
  rollingWave: {
@@ -6236,9 +6557,42 @@ export class WorkflowService {
6236
6557
  },
6237
6558
  }
6238
6559
  : {};
6560
+ const preExecutionReplanBlockers = task.status === 'ready'
6561
+ ? [
6562
+ ...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may be in progress.'] : []),
6563
+ ...(task.steps.some((step) => step.status === 'failed') ? ['No failed Step may require corrective recovery.'] : []),
6564
+ ...(handoffPosture.state !== 'none' ? ['No C1 claim or pending handoff may exist.'] : []),
6565
+ ...(inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
6566
+ ? ['No active or stale writer lease may exist.']
6567
+ : []),
6568
+ ...(!isClean(repositoryRoot) ? ['Repository checkout must be clean.'] : []),
6569
+ ...(!planRiskAudit || !['approved', 'approved-with-rationale'].includes(planRiskAudit.audit.decision)
6570
+ ? ['A current positive Plan Risk Audit is required.']
6571
+ : []),
6572
+ ]
6573
+ : [];
6574
+ const preExecutionReplanOption = task.status === 'ready' && task.planHash && planRiskAudit
6575
+ ? {
6576
+ command: 'task preexecution-replan',
6577
+ state: preExecutionReplanBlockers.length === 0 ? 'eligible' : 'blocked',
6578
+ expectedRevision: task.revision,
6579
+ expectedBriefHash: task.briefHash,
6580
+ expectedPlanHash: task.planHash,
6581
+ expectedPlanRiskAuditEventHash: planRiskAudit.audit.eventHash,
6582
+ expectedHeadCommit: headCommit(repositoryRoot),
6583
+ auditDecision: 'stop-escalate',
6584
+ actorMustEqualAuditAuditor: true,
6585
+ humanApprovalRequired: false,
6586
+ boundedEffect: 'record-current-plan-obstruction-and-withdraw-execution-authorization',
6587
+ preservesTaskScope: true,
6588
+ preservesCompletedStepAuthority: true,
6589
+ blockers: preExecutionReplanBlockers,
6590
+ }
6591
+ : null;
6239
6592
  const result = {
6240
6593
  ...next,
6241
6594
  ...handoffOverlay,
6595
+ ...(preExecutionReplanOption ? { preExecutionReplanOption } : {}),
6242
6596
  ...(strictStepReviewStep
6243
6597
  ? {
6244
6598
  action: 'task step-review',
@@ -6414,6 +6768,9 @@ export class WorkflowService {
6414
6768
  };
6415
6769
  }
6416
6770
  assertTaskContextChangeAllowed(task) {
6771
+ if (readCurrentPreExecutionReplan(this.store, task)) {
6772
+ 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' });
6773
+ }
6417
6774
  if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
6418
6775
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
6419
6776
  taskId: task.id,
@@ -7623,6 +7980,83 @@ function taskHasStarted(task) {
7623
7980
  'merged',
7624
7981
  ].includes(task.status);
7625
7982
  }
7983
+ function resolvePreExecutionReplanAudits(store, task, marker) {
7984
+ const audits = readPlanRiskAuditEvents(store, task);
7985
+ const sourceAudit = audits.find((audit) => audit.eventId === marker.sourcePlanRiskAuditEventId
7986
+ && audit.eventHash === marker.sourcePlanRiskAuditEventHash) ?? null;
7987
+ const obstructionAudit = audits.find((audit) => audit.eventId === marker.obstructionPlanRiskAuditEventId
7988
+ && audit.eventHash === marker.obstructionPlanRiskAuditEventHash) ?? null;
7989
+ const sourceAuthorization = task.authorizations.find((authorization) => authorization.kind === 'execution'
7990
+ && authorization.decision === 'superseded'
7991
+ && hashExecutionAuthorization({ ...authorization, decision: 'approved' }) === marker.sourceExecutionAuthorizationHash);
7992
+ if (!sourceAudit || !obstructionAudit || audits.at(-1)?.eventHash !== obstructionAudit.eventHash
7993
+ || !['approved', 'approved-with-rationale'].includes(sourceAudit.decision)
7994
+ || obstructionAudit.decision !== 'stop-escalate'
7995
+ || sourceAudit.planHash !== task.planHash || obstructionAudit.planHash !== task.planHash
7996
+ || !sourceAuthorization) {
7997
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan provenance chain is incomplete or inconsistent.', {
7998
+ taskId: task.id,
7999
+ eventId: marker.eventId,
8000
+ });
8001
+ }
8002
+ return { sourceAudit, obstructionAudit };
8003
+ }
8004
+ function assertPlanRiskSafetyPreserved(source, candidate, label) {
8005
+ const candidateByStep = new Map(candidate.stepClassifications.map((classification) => [classification.stepId, classification]));
8006
+ const missing = [];
8007
+ for (const sourceClassification of source.stepClassifications) {
8008
+ const candidateClassification = candidateByStep.get(sourceClassification.stepId);
8009
+ if (!candidateClassification) {
8010
+ missing.push({ stepId: sourceClassification.stepId, field: 'classification', values: [] });
8011
+ continue;
8012
+ }
8013
+ const preserveStrings = (field, sourceValues, candidateValues) => {
8014
+ const absent = sourceValues.filter((value) => !candidateValues.includes(value));
8015
+ if (absent.length > 0)
8016
+ missing.push({ stepId: sourceClassification.stepId, field, values: absent });
8017
+ };
8018
+ preserveStrings('categories', sourceClassification.categories, candidateClassification.categories);
8019
+ preserveStrings('failureModes', sourceClassification.failureModes, candidateClassification.failureModes);
8020
+ preserveStrings('requiredEvidence', sourceClassification.requiredEvidence, candidateClassification.requiredEvidence);
8021
+ if (sourceClassification.reviewRequired && !candidateClassification.reviewRequired) {
8022
+ missing.push({ stepId: sourceClassification.stepId, field: 'reviewRequired', values: [true] });
8023
+ }
8024
+ const proofBinding = (proof) => canonicalJsonStringify({ failureMode: proof.failureMode, evidence: proof.evidence });
8025
+ const candidateProofs = new Set((candidateClassification.proofObligations ?? []).map(proofBinding));
8026
+ const absentProofs = (sourceClassification.proofObligations ?? [])
8027
+ .filter((proof) => !candidateProofs.has(proofBinding(proof)));
8028
+ if (absentProofs.length > 0) {
8029
+ missing.push({ stepId: sourceClassification.stepId, field: 'proofObligations', values: absentProofs });
8030
+ }
8031
+ }
8032
+ if (missing.length > 0) {
8033
+ throw new WorkflowError('TRANSITION_BLOCKED', `${label} cannot suppress existing guarded risk or proof authority.`, {
8034
+ missing,
8035
+ });
8036
+ }
8037
+ const sourceProofText = new Map(source.stepClassifications.flatMap((classification) => (classification.proofObligations ?? []).map((proof) => [
8038
+ canonicalJsonStringify({ stepId: classification.stepId, failureMode: proof.failureMode, evidence: proof.evidence }),
8039
+ canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult }),
8040
+ ])));
8041
+ const revisedProofText = candidate.stepClassifications.some((classification) => (classification.proofObligations ?? []).some((proof) => {
8042
+ const binding = canonicalJsonStringify({
8043
+ stepId: classification.stepId,
8044
+ failureMode: proof.failureMode,
8045
+ evidence: proof.evidence,
8046
+ });
8047
+ const previous = sourceProofText.get(binding);
8048
+ return previous !== undefined
8049
+ && previous !== canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult });
8050
+ }));
8051
+ if (revisedProofText && (candidate.decision !== 'approved-with-rationale'
8052
+ || candidate.summary.trim() === source.summary.trim())) {
8053
+ throw new WorkflowError('TRANSITION_BLOCKED', `${label} may revise proof prose only with approved-with-rationale and a new audit summary.`, {
8054
+ sourceSummary: source.summary,
8055
+ candidateSummary: candidate.summary,
8056
+ candidateDecision: candidate.decision,
8057
+ });
8058
+ }
8059
+ }
7626
8060
  function taskCanRebindKnowledge(task) {
7627
8061
  return [
7628
8062
  'awaiting_execution_authorization',