codex-workflow-v2 2.0.0-beta.13 → 2.0.0-beta.13.10

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 (86) hide show
  1. package/README.md +6 -1
  2. package/dist/reviewer-runtime-build.json +44 -20
  3. package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
  4. package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
  5. package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
  6. package/dist/src/alpha6/plan-risk.d.ts +4 -0
  7. package/dist/src/alpha6/plan-risk.js +50 -13
  8. package/dist/src/alpha6/plan-risk.js.map +1 -1
  9. package/dist/src/alpha6/preexecution-replan.d.ts +38 -0
  10. package/dist/src/alpha6/preexecution-replan.js +130 -0
  11. package/dist/src/alpha6/preexecution-replan.js.map +1 -0
  12. package/dist/src/alpha6/remediation.d.ts +4 -2
  13. package/dist/src/alpha6/remediation.js +155 -20
  14. package/dist/src/alpha6/remediation.js.map +1 -1
  15. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +18 -0
  16. package/dist/src/alpha6/root-cause-replan-carryover.js +396 -0
  17. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -0
  18. package/dist/src/alpha7/autonomy.d.ts +3 -0
  19. package/dist/src/alpha7/autonomy.js +89 -42
  20. package/dist/src/alpha7/autonomy.js.map +1 -1
  21. package/dist/src/change-explanation.d.ts +64 -0
  22. package/dist/src/change-explanation.js +150 -0
  23. package/dist/src/change-explanation.js.map +1 -0
  24. package/dist/src/cli-actions.d.ts +3 -2
  25. package/dist/src/cli-actions.js +6 -0
  26. package/dist/src/cli-actions.js.map +1 -1
  27. package/dist/src/cli.js +73 -1
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/contracts.d.ts +38 -0
  30. package/dist/src/dependency-provenance.d.ts +3 -2
  31. package/dist/src/dependency-provenance.js +33 -2
  32. package/dist/src/dependency-provenance.js.map +1 -1
  33. package/dist/src/domain/plan-semantics.d.ts +11 -0
  34. package/dist/src/domain/plan-semantics.js +49 -0
  35. package/dist/src/domain/plan-semantics.js.map +1 -0
  36. package/dist/src/gateway-handshake.js +2 -0
  37. package/dist/src/gateway-handshake.js.map +1 -1
  38. package/dist/src/lifecycle/corrective-replan.js +3 -3
  39. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  40. package/dist/src/observed-routes.js +1 -0
  41. package/dist/src/observed-routes.js.map +1 -1
  42. package/dist/src/pending-review-update.d.ts +49 -0
  43. package/dist/src/pending-review-update.js +132 -0
  44. package/dist/src/pending-review-update.js.map +1 -0
  45. package/dist/src/repository.js +16 -4
  46. package/dist/src/repository.js.map +1 -1
  47. package/dist/src/reviewer.js +4 -1
  48. package/dist/src/reviewer.js.map +1 -1
  49. package/dist/src/state/corrective-replan-executor.js +3 -1
  50. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  51. package/dist/src/state/corrective-replan-public.js +5 -4
  52. package/dist/src/state/corrective-replan-public.js.map +1 -1
  53. package/dist/src/state/corrective-yield-executor.js +12 -3
  54. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  55. package/dist/src/state/store.d.ts +1 -1
  56. package/dist/src/state/store.js +17 -3
  57. package/dist/src/state/store.js.map +1 -1
  58. package/dist/src/version.d.ts +1 -1
  59. package/dist/src/version.js +1 -1
  60. package/dist/src/version.js.map +1 -1
  61. package/dist/src/workflow.d.ts +84 -1
  62. package/dist/src/workflow.js +1616 -355
  63. package/dist/src/workflow.js.map +1 -1
  64. package/docs/beta13.2-signal-review-recovery.md +38 -0
  65. package/docs/change-model.md +118 -0
  66. package/docs/delegated-approval.md +26 -0
  67. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  68. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  69. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  70. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +9 -1
  71. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +9 -5
  72. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +18 -3
  73. package/docs/pending-review-update.md +15 -0
  74. package/docs/release.md +267 -2
  75. package/docs/updating-existing-project.md +36 -6
  76. package/docs/validation-report.md +30 -9
  77. package/package.json +2 -1
  78. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +204 -0
  79. package/plugins/codex-workflow-gateway/references/protocol.md +11 -0
  80. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +298 -0
  81. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
  82. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +55 -3
  83. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +218 -21
  84. package/schemas/plan-risk-audit-event.schema.json +1 -0
  85. package/schemas/preexecution-replan-event.schema.json +45 -0
  86. package/schemas/task.schema.json +55 -0
@@ -1,23 +1,29 @@
1
+ import { readCorrectivePlanningAudit, readFailedStepPlanningRecovery } from './alpha6/failed-step-planning-recovery.js';
2
+ import { explainChange } from './change-explanation.js';
3
+ import { pendingReviewRunnerFingerprint, PENDING_REVIEW_UPDATE_SIDECAR, preparePendingReviewUpdateEvent, PENDING_REVIEW_SOURCE_VERSION, pendingReviewSourceBinding, pendingReviewBindingHash, assertPendingReviewTransport, verifiedPendingReviewTransport } from './pending-review-update.js';
1
4
  import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
5
  import { createHash } from 'node:crypto';
3
6
  import path from 'node:path';
4
7
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
5
- import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
8
+ import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, workflowDependencyVersionAt, } from './dependency-provenance.js';
6
9
  import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
7
10
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
8
11
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
9
12
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
13
+ import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from './domain/plan-semantics.js';
10
14
  import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
11
15
  import { pathAllowed, validatePlan } from './domain/validation.js';
12
16
  import { WorkflowError } from './errors.js';
13
17
  import { withObservedRouteMetadata } from './observed-routes.js';
14
18
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
15
19
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
16
- import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
20
+ import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
17
21
  import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
18
22
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
19
- import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
20
- import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
23
+ import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
24
+ import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
25
+ import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
26
+ import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
21
27
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
22
28
  import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
23
29
  import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
@@ -27,7 +33,7 @@ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, en
27
33
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
28
34
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
29
35
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
30
- import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
36
+ import { canonicalJsonStringify, prepareSidecarAppend, sha256Hex } from './alpha6/store-sidecars.js';
31
37
  import { ensureDirectory } from './fs-utils.js';
32
38
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
33
39
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
@@ -43,12 +49,16 @@ import { createEntityId } from './ulid.js';
43
49
  import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
44
50
  import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
45
51
  const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
52
+ // Release-reviewed exact compatibility target; never infer support from PACKAGE_VERSION.
53
+ const CONSUMED_CARRYOVER_TARGET_VERSION = '2.0.0-beta.13.10';
46
54
  export class WorkflowService {
47
55
  store;
48
56
  now;
49
- constructor(store = new FileStateStore(), now = () => new Date()) {
57
+ transactionHooks;
58
+ constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
50
59
  this.store = store;
51
60
  this.now = now;
61
+ this.transactionHooks = transactionHooks;
52
62
  }
53
63
  #mutationContext(repository) {
54
64
  const identity = resolveRepositoryIdentity(repository);
@@ -405,7 +415,7 @@ export class WorkflowService {
405
415
  discoveries: this.store.listDiscoveries(identity.projectId),
406
416
  tasks,
407
417
  milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
408
- delegations: this.store.listDelegations(identity.projectId),
418
+ delegations: this.listDelegations(identity.projectId),
409
419
  adoption,
410
420
  };
411
421
  }
@@ -422,7 +432,7 @@ export class WorkflowService {
422
432
  if (confirmationCode.trim() !== gate.confirmationCode) {
423
433
  throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation confirmation does not match the exact project and policy.', { projectId: identity.projectId, policyHash: gate.policyHash });
424
434
  }
425
- const duplicate = this.store.listDelegations(identity.projectId)
435
+ const duplicate = this.listDelegations(identity.projectId)
426
436
  .find((grant) => grant.policyHash === gate.policyHash);
427
437
  if (duplicate) {
428
438
  throw new WorkflowError('TRANSITION_BLOCKED', 'This exact delegation policy was already issued.', {
@@ -452,26 +462,35 @@ export class WorkflowService {
452
462
  }, null);
453
463
  }
454
464
  revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
455
- const { identity } = this.#mutationContext(repository);
456
- const grant = this.store.readDelegation(identity.projectId, grantId);
457
- assertExpectedRevision(grant, expectedRevision);
458
- if (grant.status !== 'active') {
459
- throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
460
- }
461
- if (actor.trim() !== grant.principal) {
462
- throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
463
- principal: grant.principal,
464
- });
465
+ const { identity, locks } = this.#mutationContext(repository);
466
+ const before = this.store.readDelegation(identity.projectId, grantId);
467
+ const lease = before.scope.kind === 'milestone'
468
+ ? locks.acquire(before.scope.id, 'workflow-core:delegation-revocation') : null;
469
+ try {
470
+ const grant = this.store.readDelegation(identity.projectId, grantId);
471
+ assertExpectedRevision(grant, expectedRevision);
472
+ if (grant.status !== 'active') {
473
+ throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
474
+ }
475
+ if (actor.trim() !== grant.principal) {
476
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
477
+ principal: grant.principal,
478
+ });
479
+ }
480
+ if (!reason.trim())
481
+ throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
482
+ return this.store.writeDelegation({
483
+ ...grant,
484
+ status: 'revoked',
485
+ revokedAt: this.now().toISOString(),
486
+ revokedBy: grant.principal,
487
+ revokeReason: reason.trim(),
488
+ }, expectedRevision);
489
+ }
490
+ finally {
491
+ if (lease)
492
+ locks.release(lease.entityId, lease.token);
465
493
  }
466
- if (!reason.trim())
467
- throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
468
- return this.store.writeDelegation({
469
- ...grant,
470
- status: 'revoked',
471
- revokedAt: this.now().toISOString(),
472
- revokedBy: grant.principal,
473
- revokeReason: reason.trim(),
474
- }, expectedRevision);
475
494
  }
476
495
  startDiscovery(repository, input) {
477
496
  if (!input.goal.trim())
@@ -1353,34 +1372,53 @@ export class WorkflowService {
1353
1372
  };
1354
1373
  }
1355
1374
  applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
1356
- const { identity } = this.#mutationContext(repository);
1357
- const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
1358
- assertExpectedRevision(milestone, expectedRevision);
1359
- assertMilestoneScopeChangeStatusAllowed(milestone);
1360
- const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
1361
- if (confirmationCode.trim() !== prepared.confirmationCode) {
1362
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
1363
- milestoneId,
1364
- revision: milestone.revision,
1365
- actor: prepared.actor,
1366
- confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
1375
+ const { identity, locks } = this.#mutationContext(repository);
1376
+ const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1377
+ try {
1378
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
1379
+ assertExpectedRevision(milestone, expectedRevision);
1380
+ assertMilestoneScopeChangeStatusAllowed(milestone);
1381
+ const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
1382
+ if (confirmationCode.trim() !== prepared.confirmationCode) {
1383
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
1384
+ milestoneId,
1385
+ revision: milestone.revision,
1386
+ actor: prepared.actor,
1387
+ confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
1388
+ });
1389
+ }
1390
+ return applyMilestoneScopeChangeTransaction({
1391
+ store: this.store,
1392
+ milestone,
1393
+ expectedRevision,
1394
+ prepared,
1395
+ now: this.now(),
1396
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1367
1397
  });
1368
1398
  }
1369
- return applyMilestoneScopeChangeTransaction({
1370
- store: this.store,
1371
- milestone,
1372
- expectedRevision,
1373
- prepared,
1374
- now: this.now(),
1375
- readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1376
- });
1399
+ finally {
1400
+ locks.release(milestoneId, authorityLease.token);
1401
+ }
1402
+ }
1403
+ listDelegations(projectId) {
1404
+ // Valid append-only issuance events can precede publication of their grant.
1405
+ // Such missing state conveys no authority, even if mkdir already succeeded.
1406
+ const issuedIds = new Set(this.store.listMilestones(projectId).flatMap(milestone => readMilestoneAutonomyEvents(this.store, projectId, milestone.id).map(event => event.delegationGrantId)));
1407
+ return this.store.listDelegations(projectId, issuedIds);
1377
1408
  }
1378
1409
  prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
1379
- const { identity } = this.#mutationContext(repository);
1380
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1410
+ const snapshot = this.observationSnapshot(repository);
1411
+ if (snapshot.observation.kind !== 'assessment') {
1412
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Autonomy preparation requires a coherent observation boundary.', {
1413
+ observation: snapshot.observation,
1414
+ });
1415
+ }
1416
+ const identity = resolveRepositoryIdentity(repository);
1417
+ const milestone = inspectMilestoneWithIntegrity(this.store, identity.projectId, milestoneId, (taskId) => this.store.readTask(identity.projectId, taskId), () => this.store.listTasks(identity.projectId));
1381
1418
  assertExpectedRevision(milestone, expectedRevision);
1382
1419
  this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
1383
1420
  return prepareMilestoneAutonomyContract({
1421
+ store: this.store,
1384
1422
  projectId: identity.projectId,
1385
1423
  milestone,
1386
1424
  principal,
@@ -1390,208 +1428,244 @@ export class WorkflowService {
1390
1428
  });
1391
1429
  }
1392
1430
  grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
1393
- const { identity } = this.#mutationContext(repository);
1394
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1395
- assertExpectedRevision(milestone, expectedRevision);
1396
- this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
1397
- return applyMilestoneAutonomyContract({
1398
- store: this.store,
1399
- projectId: identity.projectId,
1400
- milestone,
1401
- principal,
1402
- delegate,
1403
- expiresAt,
1404
- confirmationCode,
1405
- now: this.now(),
1406
- });
1431
+ const { identity, locks } = this.#mutationContext(repository);
1432
+ const lease = locks.acquire(milestoneId, 'workflow-core:autonomy-issuance');
1433
+ try {
1434
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1435
+ assertExpectedRevision(milestone, expectedRevision);
1436
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
1437
+ return applyMilestoneAutonomyContract({
1438
+ store: this.store,
1439
+ projectId: identity.projectId,
1440
+ milestone,
1441
+ principal,
1442
+ delegate,
1443
+ expiresAt,
1444
+ confirmationCode,
1445
+ now: this.now(),
1446
+ });
1447
+ }
1448
+ finally {
1449
+ locks.release(milestoneId, lease.token);
1450
+ }
1407
1451
  }
1408
1452
  evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
1409
- const { identity } = this.#mutationContext(repository);
1410
- const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
1411
- assertExpectedRevision(milestone, expectedRevision);
1412
- assertMilestoneScopeChangeStatusAllowed(milestone);
1413
- const autonomy = requireActiveMilestoneAutonomy({
1414
- store: this.store,
1415
- projectId: identity.projectId,
1416
- milestone,
1417
- actor,
1418
- now: this.now(),
1419
- });
1420
- validateMilestonePlan(plan);
1421
- assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
1422
- const prepared = prepareMilestoneScopeChangeCandidate({
1423
- milestone,
1424
- plan,
1425
- readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1426
- actor,
1427
- autonomyContractEventHash: autonomy.contract.eventHash,
1428
- });
1429
- return applyMilestoneScopeChangeTransaction({
1430
- store: this.store,
1431
- milestone,
1432
- expectedRevision,
1433
- prepared,
1434
- now: this.now(),
1435
- readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1436
- });
1453
+ const { identity, locks } = this.#mutationContext(repository);
1454
+ const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1455
+ try {
1456
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
1457
+ assertExpectedRevision(milestone, expectedRevision);
1458
+ assertMilestoneScopeChangeStatusAllowed(milestone);
1459
+ const autonomy = requireActiveMilestoneAutonomy({
1460
+ store: this.store,
1461
+ projectId: identity.projectId,
1462
+ milestone,
1463
+ actor,
1464
+ now: this.now(),
1465
+ });
1466
+ validateMilestonePlan(plan);
1467
+ assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
1468
+ const prepared = prepareMilestoneScopeChangeCandidate({
1469
+ milestone,
1470
+ plan,
1471
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1472
+ actor,
1473
+ autonomyContractEventHash: autonomy.contract.eventHash,
1474
+ });
1475
+ return applyMilestoneScopeChangeTransaction({
1476
+ store: this.store,
1477
+ milestone,
1478
+ expectedRevision,
1479
+ prepared,
1480
+ now: this.now(),
1481
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1482
+ });
1483
+ }
1484
+ finally {
1485
+ locks.release(milestoneId, authorityLease.token);
1486
+ }
1437
1487
  }
1438
1488
  authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
1439
- const { identity } = this.#mutationContext(repository);
1440
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1441
- assertExpectedRevision(milestone, expectedRevision);
1442
- if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
1443
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
1489
+ const { identity, locks } = this.#mutationContext(repository);
1490
+ const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1491
+ try {
1492
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1493
+ assertExpectedRevision(milestone, expectedRevision);
1494
+ if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
1495
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
1496
+ }
1497
+ assertMilestoneDependenciesDeclared(milestone);
1498
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
1499
+ const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1500
+ const planHash = this.store.hashArtifact(root, 'plan.json');
1501
+ if (planHash !== milestone.planHash) {
1502
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
1503
+ }
1504
+ const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
1505
+ const event = {
1506
+ kind: 'execution',
1507
+ decision: 'approved',
1508
+ actor: actor.trim() || 'user',
1509
+ recordedAt: this.now().toISOString(),
1510
+ briefHash: null,
1511
+ planHash,
1512
+ resultHash: null,
1513
+ evidenceHash: null,
1514
+ headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
1515
+ reason,
1516
+ authorizationMode: delegation ? 'delegated' : 'human',
1517
+ ...(delegation ? { delegation } : {}),
1518
+ };
1519
+ return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
1444
1520
  }
1445
- assertMilestoneDependenciesDeclared(milestone);
1446
- this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
1447
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1448
- const planHash = this.store.hashArtifact(root, 'plan.json');
1449
- if (planHash !== milestone.planHash) {
1450
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
1521
+ finally {
1522
+ locks.release(milestoneId, authorityLease.token);
1451
1523
  }
1452
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
1453
- const event = {
1454
- kind: 'execution',
1455
- decision: 'approved',
1456
- actor: actor.trim() || 'user',
1457
- recordedAt: this.now().toISOString(),
1458
- briefHash: null,
1459
- planHash,
1460
- resultHash: null,
1461
- evidenceHash: null,
1462
- headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
1463
- reason,
1464
- authorizationMode: delegation ? 'delegated' : 'human',
1465
- ...(delegation ? { delegation } : {}),
1466
- };
1467
- return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
1468
1524
  }
1469
1525
  validateMilestone(repository, milestoneId, expectedRevision) {
1470
- const { identity } = this.#mutationContext(repository);
1471
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1472
- assertExpectedRevision(milestone, expectedRevision);
1473
- if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
1474
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
1475
- }
1476
- this.assertMilestoneAuthorizationFresh(milestone);
1477
- if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1478
- throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
1479
- expectedBranch: milestone.baseBranch,
1480
- actualBranch: currentBranch(identity.repositoryRoot),
1481
- changedFiles: changedFiles(identity.repositoryRoot),
1482
- });
1483
- }
1484
- const incomplete = milestone.memberships.filter((membership) => {
1485
- if (membership.disposition !== 'required')
1486
- return false;
1487
- return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
1488
- });
1489
- if (incomplete.length > 0) {
1490
- throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
1491
- taskIds: incomplete.map((membership) => membership.taskId),
1526
+ const { identity, locks } = this.#mutationContext(repository);
1527
+ const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1528
+ try {
1529
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1530
+ assertExpectedRevision(milestone, expectedRevision);
1531
+ if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
1532
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
1533
+ }
1534
+ this.assertMilestoneAuthorizationFresh(milestone);
1535
+ if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1536
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
1537
+ expectedBranch: milestone.baseBranch,
1538
+ actualBranch: currentBranch(identity.repositoryRoot),
1539
+ changedFiles: changedFiles(identity.repositoryRoot),
1540
+ });
1541
+ }
1542
+ const incomplete = milestone.memberships.filter((membership) => {
1543
+ if (membership.disposition !== 'required')
1544
+ return false;
1545
+ return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
1492
1546
  });
1493
- }
1494
- const checks = runChecks(identity.repositoryRoot, milestone.checks);
1495
- const baseHead = headCommit(identity.repositoryRoot);
1496
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1497
- const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
1498
- milestoneId,
1499
- baseBranch: milestone.baseBranch,
1500
- headCommit: baseHead,
1501
- generatedAt: this.now().toISOString(),
1502
- membershipRevision: milestone.membershipRevision,
1503
- memberships: milestone.memberships,
1504
- checks,
1505
- }, null, 2));
1506
- if (checks.some((check) => !check.passed)) {
1507
- const saved = this.store.writeMilestone({ ...milestone, status: 'blocked', evidenceHash: stagedEvidence.hash }, expectedRevision);
1547
+ if (incomplete.length > 0) {
1548
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
1549
+ taskIds: incomplete.map((membership) => membership.taskId),
1550
+ });
1551
+ }
1552
+ const checks = runChecks(identity.repositoryRoot, milestone.checks);
1553
+ const baseHead = headCommit(identity.repositoryRoot);
1554
+ const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1555
+ const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
1556
+ milestoneId,
1557
+ baseBranch: milestone.baseBranch,
1558
+ headCommit: baseHead,
1559
+ generatedAt: this.now().toISOString(),
1560
+ membershipRevision: milestone.membershipRevision,
1561
+ memberships: milestone.memberships,
1562
+ checks,
1563
+ }, null, 2));
1564
+ if (checks.some((check) => !check.passed)) {
1565
+ const saved = this.store.writeMilestone({ ...milestone, status: 'blocked', evidenceHash: stagedEvidence.hash }, expectedRevision);
1566
+ this.store.publishArtifact(stagedEvidence);
1567
+ return saved;
1568
+ }
1569
+ const result = [
1570
+ `# Milestone result: ${milestone.title}`,
1571
+ '',
1572
+ `Outcome: ${milestone.outcome}`,
1573
+ '',
1574
+ `Validated base: ${milestone.baseBranch}@${baseHead}`,
1575
+ '',
1576
+ 'All required memberships are merged and all Milestone checks passed.',
1577
+ '',
1578
+ ].join('\n');
1579
+ const stagedResult = this.store.stageArtifact(root, 'result.md', result);
1580
+ const saved = this.store.writeMilestone({
1581
+ ...milestone,
1582
+ status: 'awaiting_final_acceptance',
1583
+ evidenceHash: stagedEvidence.hash,
1584
+ resultHash: stagedResult.hash,
1585
+ }, expectedRevision);
1508
1586
  this.store.publishArtifact(stagedEvidence);
1587
+ this.store.publishArtifact(stagedResult);
1509
1588
  return saved;
1510
1589
  }
1511
- const result = [
1512
- `# Milestone result: ${milestone.title}`,
1513
- '',
1514
- `Outcome: ${milestone.outcome}`,
1515
- '',
1516
- `Validated base: ${milestone.baseBranch}@${baseHead}`,
1517
- '',
1518
- 'All required memberships are merged and all Milestone checks passed.',
1519
- '',
1520
- ].join('\n');
1521
- const stagedResult = this.store.stageArtifact(root, 'result.md', result);
1522
- const saved = this.store.writeMilestone({
1523
- ...milestone,
1524
- status: 'awaiting_final_acceptance',
1525
- evidenceHash: stagedEvidence.hash,
1526
- resultHash: stagedResult.hash,
1527
- }, expectedRevision);
1528
- this.store.publishArtifact(stagedEvidence);
1529
- this.store.publishArtifact(stagedResult);
1530
- return saved;
1590
+ finally {
1591
+ locks.release(milestoneId, authorityLease.token);
1592
+ }
1531
1593
  }
1532
1594
  acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
1533
- const { identity } = this.#mutationContext(repository);
1534
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1535
- assertExpectedRevision(milestone, expectedRevision);
1536
- if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
1537
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
1538
- }
1539
- if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1540
- throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone acceptance requires a clean base branch checkout.');
1541
- }
1542
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1543
- const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
1544
- const currentHead = headCommit(identity.repositoryRoot);
1545
- const resultHash = this.store.hashArtifact(root, 'result.md');
1546
- const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
1547
- if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
1548
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
1549
- }
1550
- const normalizedActor = actor.trim();
1551
- if (!normalizedActor) {
1552
- throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
1595
+ const { identity, locks } = this.#mutationContext(repository);
1596
+ const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1597
+ try {
1598
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1599
+ assertExpectedRevision(milestone, expectedRevision);
1600
+ if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
1601
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
1602
+ }
1603
+ if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1604
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone acceptance requires a clean base branch checkout.');
1605
+ }
1606
+ const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1607
+ const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
1608
+ const currentHead = headCommit(identity.repositoryRoot);
1609
+ const resultHash = this.store.hashArtifact(root, 'result.md');
1610
+ const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
1611
+ if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
1612
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
1613
+ }
1614
+ const normalizedActor = actor.trim();
1615
+ if (!normalizedActor) {
1616
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
1617
+ }
1618
+ const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
1619
+ if (confirmationCode.trim() !== gate.confirmationCode) {
1620
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
1621
+ milestoneId,
1622
+ revision: milestone.revision,
1623
+ });
1624
+ }
1625
+ const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
1626
+ const event = {
1627
+ kind: 'final_acceptance',
1628
+ decision: 'approved',
1629
+ actor: normalizedActor,
1630
+ recordedAt: this.now().toISOString(),
1631
+ briefHash: null,
1632
+ planHash: milestone.planHash,
1633
+ resultHash,
1634
+ evidenceHash,
1635
+ headCommit: currentHead,
1636
+ reason: 'Milestone final acceptance confirmed against the current validated state.',
1637
+ authorizationMode: delegation ? 'delegated' : 'human',
1638
+ ...(delegation ? { delegation } : {}),
1639
+ };
1640
+ return this.store.writeMilestone({
1641
+ ...milestone,
1642
+ status: 'accepted',
1643
+ resultHash,
1644
+ evidenceHash,
1645
+ acceptedHead: currentHead,
1646
+ authorizations: [...milestone.authorizations, event],
1647
+ }, expectedRevision);
1553
1648
  }
1554
- const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
1555
- if (confirmationCode.trim() !== gate.confirmationCode) {
1556
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
1557
- milestoneId,
1558
- revision: milestone.revision,
1559
- });
1649
+ finally {
1650
+ locks.release(milestoneId, authorityLease.token);
1560
1651
  }
1561
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
1562
- const event = {
1563
- kind: 'final_acceptance',
1564
- decision: 'approved',
1565
- actor: normalizedActor,
1566
- recordedAt: this.now().toISOString(),
1567
- briefHash: null,
1568
- planHash: milestone.planHash,
1569
- resultHash,
1570
- evidenceHash,
1571
- headCommit: currentHead,
1572
- reason: 'Milestone final acceptance confirmed against the current validated state.',
1573
- authorizationMode: delegation ? 'delegated' : 'human',
1574
- ...(delegation ? { delegation } : {}),
1575
- };
1576
- return this.store.writeMilestone({
1577
- ...milestone,
1578
- status: 'accepted',
1579
- resultHash,
1580
- evidenceHash,
1581
- acceptedHead: currentHead,
1582
- authorizations: [...milestone.authorizations, event],
1583
- }, expectedRevision);
1584
1652
  }
1585
1653
  cancelMilestone(repository, milestoneId, expectedRevision, reason) {
1586
1654
  if (!reason.trim())
1587
1655
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
1588
- const { identity } = this.#mutationContext(repository);
1589
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1590
- assertExpectedRevision(milestone, expectedRevision);
1591
- if (['accepted', 'cancelled'].includes(milestone.status)) {
1592
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
1656
+ const { identity, locks } = this.#mutationContext(repository);
1657
+ const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
1658
+ try {
1659
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1660
+ assertExpectedRevision(milestone, expectedRevision);
1661
+ if (['accepted', 'cancelled'].includes(milestone.status)) {
1662
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
1663
+ }
1664
+ return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
1665
+ }
1666
+ finally {
1667
+ locks.release(milestoneId, authorityLease.token);
1593
1668
  }
1594
- return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
1595
1669
  }
1596
1670
  setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
1597
1671
  validatePlan(plan);
@@ -1600,6 +1674,10 @@ export class WorkflowService {
1600
1674
  this.validateKnowledgeTargets(knowledgeMap, plan);
1601
1675
  const task = this.store.readTask(identity.projectId, taskId);
1602
1676
  assertExpectedRevision(task, expectedRevision);
1677
+ const preExecutionReplan = readCurrentPreExecutionReplan(this.store, task);
1678
+ const preExecutionReplanAudits = preExecutionReplan
1679
+ ? resolvePreExecutionReplanAudits(this.store, task, preExecutionReplan)
1680
+ : null;
1603
1681
  const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
1604
1682
  const currentPlanCorrectiveDecisions = task.planHash
1605
1683
  ? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
@@ -1746,6 +1824,9 @@ export class WorkflowService {
1746
1824
  }
1747
1825
  const root = this.store.taskRoot(identity.projectId, taskId);
1748
1826
  const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
1827
+ const changesPlanSemantics = !task.planHash
1828
+ || normalizePlanKnowledgeBinding(preparedPlan.planBytes, 'candidate').semanticHash
1829
+ !== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(root, task.planHash, task.planHash), 'current').semanticHash;
1749
1830
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
1750
1831
  ? { ...authorization, decision: 'superseded' }
1751
1832
  : authorization);
@@ -1763,6 +1844,83 @@ export class WorkflowService {
1763
1844
  ...(task.invalidatedStepCommits ?? []),
1764
1845
  ...omittedStepEvidenceCommits(task, plannedSteps),
1765
1846
  ])];
1847
+ if (preExecutionReplan && preExecutionReplanAudits) {
1848
+ const replanC1Posture = readTaskC1Posture(this.store, task);
1849
+ const replanWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
1850
+ if (replanC1Posture.state !== 'none' || replanWriterLease) {
1851
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replacement Plan requires no C1 posture or writer lease.', {
1852
+ taskId: task.id,
1853
+ c1Posture: replanC1Posture.state,
1854
+ writerLeasePresent: replanWriterLease !== null,
1855
+ });
1856
+ }
1857
+ if (!planRiskAudit) {
1858
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires an atomic replacement Plan Risk Audit.');
1859
+ }
1860
+ if (task.planObjective !== plan.objective
1861
+ || canonicalJsonStringify(task.requirements) !== canonicalJsonStringify(plan.requirements)
1862
+ || canonicalJsonStringify(task.acceptance) !== canonicalJsonStringify(plan.acceptance)) {
1863
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot change Task objective, requirements, or acceptance.', {
1864
+ taskId: task.id,
1865
+ });
1866
+ }
1867
+ if (!changesPlanSemantics) {
1868
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic Plan change.', { taskId: task.id });
1869
+ }
1870
+ const candidateSteps = new Map(plan.steps.map((step) => [step.id, step]));
1871
+ const missingStepIds = task.steps.filter((step) => !candidateSteps.has(step.id)).map((step) => step.id);
1872
+ if (missingStepIds.length > 0) {
1873
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must retain every existing Step identifier.', {
1874
+ taskId: task.id,
1875
+ missingStepIds,
1876
+ });
1877
+ }
1878
+ const completedStepIds = task.steps.filter((step) => step.status === 'completed').map((step) => step.id);
1879
+ const lostCompletedStepIds = completedStepIds.filter((stepId) => !retainedCompletedSteps.has(stepId));
1880
+ if (lostCompletedStepIds.length > 0) {
1881
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must preserve the exact completed Step subgraph and evidence.', {
1882
+ taskId: task.id,
1883
+ lostCompletedStepIds,
1884
+ });
1885
+ }
1886
+ const weakenedForbiddenScope = task.steps.flatMap((step) => {
1887
+ const candidate = candidateSteps.get(step.id);
1888
+ const missing = step.forbiddenScope.filter((scope) => !candidate.forbiddenScope.includes(scope));
1889
+ return missing.length > 0 ? [{ stepId: step.id, missing }] : [];
1890
+ });
1891
+ if (weakenedForbiddenScope.length > 0) {
1892
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken existing forbiddenScope boundaries.', {
1893
+ taskId: task.id,
1894
+ weakenedForbiddenScope,
1895
+ });
1896
+ }
1897
+ const missingRisks = task.risks.filter((risk) => !plan.risks.includes(risk));
1898
+ if (missingRisks.length > 0) {
1899
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot suppress existing Plan risks.', {
1900
+ taskId: task.id,
1901
+ missingRisks,
1902
+ });
1903
+ }
1904
+ assertPlanRiskSafetyPreserved(preExecutionReplanAudits.obstructionAudit, planRiskAudit, 'Replacement Plan Risk Audit');
1905
+ const candidateEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => canonicalJsonStringify(edge)));
1906
+ const missingProductionEdges = (task.productionEdges ?? [])
1907
+ .filter((edge) => !candidateEdgeKeys.has(canonicalJsonStringify(edge)));
1908
+ if ((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
1909
+ || missingProductionEdges.length > 0) {
1910
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken production-edge authority.', {
1911
+ taskId: task.id,
1912
+ missingProductionEdges,
1913
+ });
1914
+ }
1915
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== preExecutionReplan.sourceHeadCommit) {
1916
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replacement Plan requires the exact clean recorded HEAD.', {
1917
+ taskId: task.id,
1918
+ expectedHead: preExecutionReplan.sourceHeadCommit,
1919
+ actualHead: headCommit(identity.repositoryRoot),
1920
+ changedFiles: changedFiles(identity.repositoryRoot),
1921
+ });
1922
+ }
1923
+ }
1766
1924
  if (task.status === 'needs_fix' &&
1767
1925
  task.review.status === 'failed' &&
1768
1926
  plannedSteps.every((step) => step.status === 'completed')) {
@@ -1786,7 +1944,7 @@ export class WorkflowService {
1786
1944
  : []),
1787
1945
  ...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
1788
1946
  ...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
1789
- ...(preparedPlan.planHash === task.planHash
1947
+ ...(!changesPlanSemantics
1790
1948
  ? ['task.rolling-replan.semantic-plan-change']
1791
1949
  : []),
1792
1950
  ...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
@@ -1859,6 +2017,61 @@ export class WorkflowService {
1859
2017
  };
1860
2018
  if (planRiskAudit)
1861
2019
  validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
2020
+ if (preExecutionReplan && preExecutionReplanAudits && planRiskAudit) {
2021
+ const now = this.now();
2022
+ const resultTask = {
2023
+ ...candidate,
2024
+ revision: task.revision + 1,
2025
+ updatedAt: now.toISOString(),
2026
+ };
2027
+ const preparedAuditBase = buildCandidatePlanRiskAuditEvent(this.store, resultTask, planRiskAudit, now.toISOString(), now.getTime());
2028
+ const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
2029
+ const projectRoot = this.store.projectRoot(task.projectId);
2030
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2031
+ const versionFile = path.join(root, '.versions', `plan.md.${preparedPlan.planHash}`);
2032
+ if (existsSync(versionFile) && sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash) {
2033
+ throw new WorkflowError('STATE_CORRUPT', 'Immutable replacement Plan version is corrupt.', {
2034
+ taskId: task.id,
2035
+ planHash: preparedPlan.planHash,
2036
+ });
2037
+ }
2038
+ applyProjectTransaction({
2039
+ projectRoot,
2040
+ projectId: task.projectId,
2041
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2042
+ kind: 'preexecution-replacement-plan-set',
2043
+ now,
2044
+ targets: [
2045
+ { path: relative(path.join(root, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
2046
+ { path: relative(path.join(root, 'plan.md')), content: preparedPlan.planBytes },
2047
+ { path: relative(versionFile), content: preparedPlan.planBytes },
2048
+ { path: relative(path.join(root, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
2049
+ ],
2050
+ ...(this.transactionHooks.afterPreExecutionReplanTargetWrite
2051
+ ? { afterTargetWrite: this.transactionHooks.afterPreExecutionReplanTargetWrite }
2052
+ : {}),
2053
+ validate: () => {
2054
+ const saved = this.store.readTask(task.projectId, task.id);
2055
+ const currentAudit = readCurrentPlanRiskAudit(this.store, saved, { requireCurrentTaskRevision: true });
2056
+ if (saved.revision !== resultTask.revision
2057
+ || saved.status !== 'awaiting_execution_authorization'
2058
+ || saved.planHash !== preparedPlan.planHash
2059
+ || this.store.hashArtifact(root, 'plan.md') !== preparedPlan.planHash
2060
+ || sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash
2061
+ || currentAudit?.audit.eventHash !== preparedAudit.event.eventHash
2062
+ || hashCompletedSteps(saved) !== preExecutionReplan.completedStepsHash
2063
+ || readCurrentPreExecutionReplan(this.store, saved) !== null
2064
+ || saved.authorizations.some((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved')) {
2065
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replacement Plan transaction readback failed.', {
2066
+ taskId: task.id,
2067
+ });
2068
+ }
2069
+ if (saved.baseCommit)
2070
+ validateTaskHistory(identity.repositoryRoot, saved);
2071
+ },
2072
+ });
2073
+ return this.store.readTask(task.projectId, task.id);
2074
+ }
1862
2075
  const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
1863
2076
  if (stagedPlan.hash !== preparedPlan.planHash) {
1864
2077
  throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
@@ -1930,6 +2143,12 @@ export class WorkflowService {
1930
2143
  knowledgeMapHash,
1931
2144
  graphUse: task.graphUse,
1932
2145
  steps: task.steps.map(stripStepRuntime),
2146
+ ...(Object.hasOwn(task, 'productionEdges')
2147
+ ? { productionEdges: structuredClone(task.productionEdges ?? []) }
2148
+ : {}),
2149
+ ...(Object.hasOwn(task, 'requiresProductionEdges')
2150
+ ? { requiresProductionEdges: task.requiresProductionEdges === true }
2151
+ : {}),
1933
2152
  };
1934
2153
  validatePlan(reboundPlan);
1935
2154
  this.validateKnowledgeTargets(knowledgeMap, reboundPlan);
@@ -1981,11 +2200,15 @@ export class WorkflowService {
1981
2200
  }
1982
2201
  const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
1983
2202
  const transitions = [];
1984
- const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2203
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
2204
+ const currentPlanRiskAudit = planningRecovery?.current ?? readCurrentPlanRiskAudit(this.store, task);
1985
2205
  const currentPlanArtifactHash = task.planHash
1986
2206
  ? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
1987
2207
  : null;
1988
2208
  const refreshAssessment = assessDelegatedKnowledgeRefresh(currentMap, inspectedMap, task, currentPlanArtifactHash);
2209
+ if (planningRecovery && refreshAssessment.mode !== 'delegated-content-only') {
2210
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stopped planning context refresh permits content-only Knowledge drift.');
2211
+ }
1989
2212
  // Validate every delegated transition before the first write so a narrow grant cannot
1990
2213
  // leave the composite operation half-applied.
1991
2214
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
@@ -2032,11 +2255,15 @@ export class WorkflowService {
2032
2255
  appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
2033
2256
  }
2034
2257
  transitions.push('task knowledge-rebind');
2035
- refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
2036
- ? 'Delegated Plan-bounded supporting-source context refresh.'
2037
- : 'Delegated content-only context refresh.', delegationGrantId);
2038
- transitions.push('task execution authorize');
2258
+ // A stopped Plan may regain its Knowledge binding, never execution authority.
2259
+ if (!planningRecovery)
2260
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
2261
+ ? 'Delegated Plan-bounded supporting-source context refresh.'
2262
+ : 'Delegated content-only context refresh.', delegationGrantId);
2263
+ if (!planningRecovery)
2264
+ transitions.push('task execution authorize');
2039
2265
  return {
2266
+ authority: planningRecovery ? 'planning-only' : 'execution',
2040
2267
  mode: refreshAssessment.mode,
2041
2268
  transitions,
2042
2269
  addedSources: refreshAssessment.addedSources,
@@ -2046,8 +2273,16 @@ export class WorkflowService {
2046
2273
  },
2047
2274
  validate: (result) => {
2048
2275
  const currentTask = this.store.readTask(identity.projectId, task.id);
2049
- if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
2050
- throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit an authorized Task.');
2276
+ if (currentTask.revision !== result.task.revision || currentTask.status !== (planningRecovery ? 'awaiting_execution_authorization' : 'ready')) {
2277
+ throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit the expected Task posture.');
2278
+ }
2279
+ if (planningRecovery) {
2280
+ const rebound = readFailedStepPlanningRecovery(this.store, currentTask);
2281
+ if (!rebound || rebound.baseline.eventHash !== planningRecovery.baseline.eventHash
2282
+ || JSON.stringify(currentTask.steps) !== JSON.stringify(task.steps)
2283
+ || JSON.stringify(currentTask.authorizations) !== JSON.stringify(task.authorizations)) {
2284
+ throw new WorkflowError('STATE_CORRUPT', 'Planning context refresh changed retained recovery evidence.');
2285
+ }
2051
2286
  }
2052
2287
  const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
2053
2288
  if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
@@ -2061,6 +2296,9 @@ export class WorkflowService {
2061
2296
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2062
2297
  const task = this.store.readTask(identity.projectId, taskId);
2063
2298
  assertExpectedRevision(task, expectedRevision);
2299
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2300
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic replacement Plan before execution authorization.', { taskId, requiredAction: 'task plan-set' });
2301
+ }
2064
2302
  if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
2065
2303
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting execution authorization.`);
2066
2304
  }
@@ -2118,9 +2356,143 @@ export class WorkflowService {
2118
2356
  this.readCurrentAdoptionPostureStrict(identity.projectId);
2119
2357
  const task = this.store.readTask(identity.projectId, taskId);
2120
2358
  assertExpectedRevision(task, expectedRevision);
2359
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2360
+ 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' });
2361
+ }
2121
2362
  this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
2122
2363
  return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
2123
2364
  }
2365
+ recordPreExecutionReplan(repository, taskId, expectedRevision, actor, reason, binding, input) {
2366
+ const context = this.#mutationContext(repository);
2367
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
2368
+ const task = this.store.readTask(context.identity.projectId, taskId);
2369
+ assertExpectedRevision(task, expectedRevision);
2370
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2371
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task already has a pending pre-execution replan.', {
2372
+ taskId,
2373
+ requiredAction: 'task plan-set',
2374
+ });
2375
+ }
2376
+ if (task.status !== 'ready' || task.steps.some((step) => ['in_progress', 'failed'].includes(step.status))) {
2377
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a ready Task with no Step in progress or failed.', {
2378
+ taskId,
2379
+ status: task.status,
2380
+ });
2381
+ }
2382
+ if (!isClean(context.identity.repositoryRoot)) {
2383
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replan requires a clean checkout.', {
2384
+ changedFiles: changedFiles(context.identity.repositoryRoot),
2385
+ });
2386
+ }
2387
+ const c1Posture = readTaskC1Posture(this.store, task);
2388
+ const writerLease = context.locks.inspect(task.id);
2389
+ if (c1Posture.state !== 'none' || writerLease) {
2390
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a clean authority boundary without C1 or writer lease.', {
2391
+ taskId,
2392
+ c1Posture: c1Posture.state,
2393
+ writerLeasePresent: writerLease !== null,
2394
+ });
2395
+ }
2396
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2397
+ if (task.baseCommit) {
2398
+ validateTaskHistory(context.identity.repositoryRoot, task);
2399
+ }
2400
+ else if (task.workspaceOwner !== null || task.taskBranch !== null
2401
+ || task.steps.some((step) => step.status === 'completed')) {
2402
+ throw new WorkflowError('STATE_CORRUPT', 'Unstarted pre-execution replan Task has inconsistent execution history.', {
2403
+ taskId,
2404
+ });
2405
+ }
2406
+ const authorization = this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2407
+ const sourceAudit = readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: false });
2408
+ if (!sourceAudit || !['approved', 'approved-with-rationale'].includes(sourceAudit.audit.decision)) {
2409
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires the current positive Plan Risk Audit.');
2410
+ }
2411
+ const currentHead = headCommit(context.identity.repositoryRoot);
2412
+ const actualBinding = {
2413
+ expectedBriefHash: task.briefHash,
2414
+ expectedPlanHash: task.planHash,
2415
+ expectedPlanRiskAuditEventHash: sourceAudit.audit.eventHash,
2416
+ expectedHeadCommit: currentHead,
2417
+ };
2418
+ if (canonicalJsonStringify(binding) !== canonicalJsonStringify(actualBinding)) {
2419
+ throw new WorkflowError('STATE_CONFLICT', 'Pre-execution replan source binding is stale.', {
2420
+ expected: binding,
2421
+ actual: actualBinding,
2422
+ });
2423
+ }
2424
+ if (input.decision !== 'stop-escalate') {
2425
+ throw new WorkflowError('INVALID_ARGUMENT', 'Pre-execution replan requires a stop-escalate Plan Risk Audit.');
2426
+ }
2427
+ if (actor.trim() !== input.auditor.trim() || input.planner.trim() !== sourceAudit.audit.planner) {
2428
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must be recorded by the independent auditor against the original planner.', {
2429
+ actor: actor.trim(),
2430
+ auditor: input.auditor.trim(),
2431
+ expectedPlanner: sourceAudit.audit.planner,
2432
+ actualPlanner: input.planner.trim(),
2433
+ });
2434
+ }
2435
+ assertPlanRiskSafetyPreserved(sourceAudit.audit, input, 'Pre-execution replan audit');
2436
+ const now = this.now();
2437
+ const preparedAuditBase = buildPlanRiskAuditEvent(this.store, task, input, now.toISOString(), now.getTime());
2438
+ const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
2439
+ const preparedMarker = preparePreExecutionReplanEvent(this.store, task, {
2440
+ sourceAudit: sourceAudit.audit,
2441
+ obstructionAudit: preparedAudit.event,
2442
+ authorization,
2443
+ sourceHeadCommit: currentHead,
2444
+ actor,
2445
+ reason,
2446
+ }, now);
2447
+ const resultTask = {
2448
+ ...task,
2449
+ revision: task.revision + 1,
2450
+ updatedAt: now.toISOString(),
2451
+ status: 'awaiting_execution_authorization',
2452
+ authorizations: task.authorizations.map((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved'
2453
+ ? { ...candidate, decision: 'superseded' }
2454
+ : candidate),
2455
+ blockReason: reason.trim(),
2456
+ };
2457
+ const projectRoot = this.store.projectRoot(task.projectId);
2458
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2459
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2460
+ applyProjectTransaction({
2461
+ projectRoot,
2462
+ projectId: task.projectId,
2463
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2464
+ kind: 'preexecution-plan-authority-withdrawal',
2465
+ now,
2466
+ targets: [
2467
+ { path: relative(path.join(taskRoot, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
2468
+ { path: relative(path.join(taskRoot, preExecutionReplanSidecar())), content: preparedMarker.postimage },
2469
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
2470
+ ],
2471
+ validate: () => {
2472
+ const saved = this.store.readTask(task.projectId, task.id);
2473
+ const latestAudit = readPlanRiskAuditEvents(this.store, saved).at(-1) ?? null;
2474
+ const marker = readCurrentPreExecutionReplan(this.store, saved);
2475
+ if (saved.revision !== resultTask.revision
2476
+ || saved.status !== 'awaiting_execution_authorization'
2477
+ || saved.planHash !== task.planHash
2478
+ || latestAudit?.eventHash !== preparedAudit.event.eventHash
2479
+ || marker?.eventHash !== preparedMarker.event.eventHash
2480
+ || saved.authorizations.some((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved')
2481
+ || hashCompletedSteps(saved) !== preparedMarker.event.completedStepsHash
2482
+ || headCommit(context.identity.repositoryRoot) !== currentHead
2483
+ || !isClean(context.identity.repositoryRoot)) {
2484
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan transaction readback failed.', { taskId });
2485
+ }
2486
+ if (saved.baseCommit)
2487
+ validateTaskHistory(context.identity.repositoryRoot, saved);
2488
+ },
2489
+ });
2490
+ return {
2491
+ task: this.store.readTask(task.projectId, task.id),
2492
+ audit: preparedAudit.event,
2493
+ event: preparedMarker.event,
2494
+ };
2495
+ }
2124
2496
  showTaskHandoff(repository, taskId) {
2125
2497
  const { identity } = this.#mutationContext(repository);
2126
2498
  const task = this.store.readTask(identity.projectId, taskId);
@@ -2134,6 +2506,12 @@ export class WorkflowService {
2134
2506
  const context = this.#mutationContext(repository);
2135
2507
  const task = this.store.readTask(context.identity.projectId, taskId);
2136
2508
  assertExpectedRevision(task, expectedRevision);
2509
+ if (readCurrentPreExecutionReplan(this.store, task)) {
2510
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
2511
+ taskId,
2512
+ requiredAction: 'task plan-set',
2513
+ });
2514
+ }
2137
2515
  const normalizedActor = actor.trim();
2138
2516
  if (!normalizedActor) {
2139
2517
  throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
@@ -2289,8 +2667,12 @@ export class WorkflowService {
2289
2667
  });
2290
2668
  }
2291
2669
  }
2292
- const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
2293
- const action = String(navigation.action);
2670
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
2671
+ const navigation = planningRecovery
2672
+ ? this.nextForTaskWithDelegation(task.projectId, context.identity.repositoryRoot, task, planningRecovery.current)
2673
+ : this.taskGitNavigationOverride(task) ?? nextForTask(task);
2674
+ const action = String(navigation.action === 'task writer-credential-replace'
2675
+ ? navigation.blockedAction : navigation.action);
2294
2676
  const currentLease = context.locks.inspect(taskId);
2295
2677
  const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
2296
2678
  const requiredActor = writerTokenContract
@@ -2463,11 +2845,15 @@ export class WorkflowService {
2463
2845
  const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2464
2846
  ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2465
2847
  : null;
2848
+ const rootCauseReplanDirtyCarryover = step.status === 'planned' && dirty.length > 0
2849
+ ? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2850
+ : null;
2466
2851
  if (step.status === 'planned'
2467
2852
  && dirty.length > 0
2468
2853
  && !resumesCorrectiveReplan
2469
2854
  && !downstreamProofCarryover?.eligible
2470
- && !planIntegrityDirtyCarryover?.eligible) {
2855
+ && !planIntegrityDirtyCarryover?.eligible
2856
+ && !rootCauseReplanDirtyCarryover?.eligible) {
2471
2857
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2472
2858
  changedFiles: dirty,
2473
2859
  ...(downstreamProofCarryover?.applicable
@@ -2476,6 +2862,9 @@ export class WorkflowService {
2476
2862
  ...(planIntegrityDirtyCarryover?.applicable
2477
2863
  ? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
2478
2864
  : {}),
2865
+ ...(rootCauseReplanDirtyCarryover?.applicable
2866
+ ? { rootCauseReplanDirtyCarryoverBlockers: rootCauseReplanDirtyCarryover.blockers }
2867
+ : {}),
2479
2868
  });
2480
2869
  }
2481
2870
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
@@ -2741,7 +3130,7 @@ export class WorkflowService {
2741
3130
  const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
2742
3131
  ? stepHistory
2743
3132
  : [...stepHistory, preparedRemediation.event];
2744
- const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3133
+ const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
2745
3134
  const c1Posture = readTaskC1Posture(this.store, task);
2746
3135
  const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
2747
3136
  ? prepareTaskCorrectiveYield(this.store, paused, {
@@ -2964,7 +3353,15 @@ export class WorkflowService {
2964
3353
  throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
2965
3354
  }
2966
3355
  validateTaskHistory(identity.repositoryRoot, task);
2967
- if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
3356
+ const transport = verifiedPendingReviewTransport(identity.repositoryRoot, task, stepId, this.store.taskRoot(task.projectId, task.id));
3357
+ if (transport) {
3358
+ const cycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
3359
+ if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== transport.source.pendingEventHash
3360
+ || cycle.binding.evidenceHash !== transport.source.evidenceHash) {
3361
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The transported strict review must retain its exact pending chain.');
3362
+ }
3363
+ }
3364
+ if (!isClean(identity.repositoryRoot) || (headCommit(identity.repositoryRoot) !== step.evidence.commitSha && !transport)) {
2968
3365
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2969
3366
  }
2970
3367
  const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
@@ -3047,7 +3444,7 @@ export class WorkflowService {
3047
3444
  const resultingHistory = preparedRemediation.postimage === null
3048
3445
  ? stepHistory
3049
3446
  : [...stepHistory, preparedRemediation.event];
3050
- const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3447
+ const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
3051
3448
  const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
3052
3449
  const paused = {
3053
3450
  ...resolved,
@@ -3071,7 +3468,7 @@ export class WorkflowService {
3071
3468
  ? prepareTaskCorrectiveYield(this.store, paused, {
3072
3469
  actor: c1Posture.claimant,
3073
3470
  writerToken: reviewLease.token,
3074
- reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
3471
+ reason: `Yield execution authority after authoritative replan cause ${preparedRemediation.event.eventId}.`,
3075
3472
  eventId: `HOF-${sha256Hex([
3076
3473
  'strict-review-root-cause-replan-pause-v1',
3077
3474
  task.id,
@@ -3340,10 +3737,14 @@ export class WorkflowService {
3340
3737
  assertExpectedRevision(task, expectedRevision);
3341
3738
  this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
3342
3739
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
3343
- if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
3740
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
3741
+ if (!['ready', 'needs_fix', 'blocked'].includes(task.status) && !planningRecovery) {
3344
3742
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
3345
3743
  }
3346
- const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
3744
+ if (planningRecovery && correctiveAudit.decision === 'continue-fix') {
3745
+ throw new WorkflowError('TRANSITION_BLOCKED', 'A stopped rebound Plan requires replan, split, or escalation; continue-fix cannot restore execution authority.', { taskId, stepId });
3746
+ }
3747
+ const planRiskAudit = readCorrectivePlanningAudit(this.store, task);
3347
3748
  if (!planRiskAudit) {
3348
3749
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
3349
3750
  taskId,
@@ -3634,22 +4035,153 @@ export class WorkflowService {
3634
4035
  const { identity } = this.#mutationContext(repository);
3635
4036
  return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
3636
4037
  }
3637
- applyAdoption(repository, actor, confirmationCode) {
4038
+ applyAdoption(repository, actor, confirmationCode) {
4039
+ const context = this.#mutationContext(repository);
4040
+ const tasks = context.store.listTasks(context.identity.projectId);
4041
+ const milestones = context.store.listMilestones(context.identity.projectId);
4042
+ return applyAdoptionPosture(this.store, {
4043
+ projectId: context.identity.projectId,
4044
+ repositoryIdentity: context.identity,
4045
+ packageVersion: PACKAGE_VERSION,
4046
+ protocolVersion: PROTOCOL_VERSION,
4047
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
4048
+ tasks,
4049
+ milestones,
4050
+ sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
4051
+ blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
4052
+ now: this.now(),
4053
+ }, actor, confirmationCode);
4054
+ }
4055
+ pendingReviewUpdatePreflight(repository, taskId) {
4056
+ const snapshot = this.observationSnapshot(repository);
4057
+ const blockers = observationPreflightBlockers(snapshot.observation);
4058
+ try {
4059
+ pendingReviewRunnerFingerprint();
4060
+ }
4061
+ catch (error) {
4062
+ blockers.push(error instanceof Error ? error.message : String(error));
4063
+ }
4064
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4065
+ let binding = null;
4066
+ if (PACKAGE_VERSION === PENDING_REVIEW_SOURCE_VERSION)
4067
+ blockers.push('The exact target runner must be newer than the supported source.');
4068
+ if (!task)
4069
+ blockers.push('Task not found.');
4070
+ if (task) {
4071
+ try {
4072
+ binding = pendingReviewSourceBinding(snapshot.identity.repositoryRoot, this.store, task);
4073
+ }
4074
+ catch (error) {
4075
+ blockers.push(error instanceof Error ? error.message : String(error));
4076
+ }
4077
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4078
+ if ([versions.declared, versions.locked, versions.installed, versions.currentBranch,
4079
+ ...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PENDING_REVIEW_SOURCE_VERSION)) {
4080
+ blockers.push('All source version surfaces must equal the exact supported beta.13.1.');
4081
+ }
4082
+ }
4083
+ const otherRunningSteps = snapshot.tasks.flatMap((candidate) => candidate.steps.filter((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== binding?.stepId)));
4084
+ if (otherRunningSteps.length)
4085
+ blockers.push('No other Step may be running.');
4086
+ const stale = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= this.now().getTime());
4087
+ if (snapshot.leases.some((lease) => !stale.includes(lease)))
4088
+ blockers.push('No live writer lease may exist.');
4089
+ if (stale.some((lease) => lease.entityId !== taskId))
4090
+ blockers.push('No unrelated stale writer lease may exist.');
4091
+ const staleLeaseRepair = blockers.length === 0 && binding
4092
+ ? stale.map((lease) => ({ action: 'locks repair', entityId: lease.entityId })) : [];
4093
+ if (stale.length)
4094
+ blockers.push('Repair the exact stale Task lease with the installed source locks repair, then rerun source preflight.');
4095
+ return { readOnly: true, eligible: blockers.length === 0 && binding !== null, binding,
4096
+ bindingHash: binding ? pendingReviewBindingHash(binding) : null, blockers, staleLeaseRepair };
4097
+ }
4098
+ pendingReviewDependencyPreflight(repository, taskId, source) {
4099
+ const snapshot = this.observationSnapshot(repository);
4100
+ const blockers = observationPreflightBlockers(snapshot.observation);
4101
+ try {
4102
+ pendingReviewRunnerFingerprint();
4103
+ }
4104
+ catch (error) {
4105
+ blockers.push(error instanceof Error ? error.message : String(error));
4106
+ }
4107
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4108
+ const taskHead = headCommit(snapshot.identity.repositoryRoot);
4109
+ let baseTransportCommit = null;
4110
+ if (snapshot.leases.length)
4111
+ blockers.push('No live or stale writer lease may exist.');
4112
+ if (!task)
4113
+ blockers.push('Task not found.');
4114
+ if (task && existsSync(path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)))
4115
+ blockers.push('A pending review update is already recorded.');
4116
+ if (task) {
4117
+ try {
4118
+ // Receipt fields are claims, not authority. Reconstruct every source binding from
4119
+ // canonical Task/sidecars and the exact parents of the candidate Git commits.
4120
+ const parent = runGit(repository, ['rev-parse', 'HEAD^']);
4121
+ baseTransportCommit = runGit(repository, ['rev-parse', task.baseBranch]);
4122
+ const baseParent = runGit(repository, ['rev-parse', `${baseTransportCommit}^`]);
4123
+ const reconstructed = pendingReviewSourceBinding(repository, this.store, task, parent, baseParent);
4124
+ if (canonicalJsonStringify(reconstructed) !== canonicalJsonStringify(source)) {
4125
+ throw new Error('Source receipt does not match reconstructed Task/revision/Plan/HEAD/pending bindings.');
4126
+ }
4127
+ assertPendingReviewTransport(repository, reconstructed, taskHead, baseTransportCommit);
4128
+ if (snapshot.tasks.some((candidate) => candidate.steps.some((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== source.stepId)))) {
4129
+ throw new Error('No other Step may be running.');
4130
+ }
4131
+ const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
4132
+ if ([versions.runtime, versions.declared, versions.locked, versions.installed, versions.currentBranch,
4133
+ ...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PACKAGE_VERSION)) {
4134
+ throw new Error('All target version surfaces must equal the exact target runner.');
4135
+ }
4136
+ }
4137
+ catch (error) {
4138
+ blockers.push(error instanceof Error ? error.message : String(error));
4139
+ }
4140
+ }
4141
+ return { readOnly: true, eligible: blockers.length === 0, blockers, taskHead, baseTransportCommit };
4142
+ }
4143
+ recoverPendingReviewDependency(repository, taskId, expectedRevision, source, actor, reason) {
4144
+ if (!actor.trim() || !reason.trim())
4145
+ throw new WorkflowError('INVALID_ARGUMENT', 'Actor and reason are required.');
4146
+ const initial = this.pendingReviewDependencyPreflight(repository, taskId, source);
4147
+ if (!initial.eligible)
4148
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight failed.', initial);
3638
4149
  const context = this.#mutationContext(repository);
3639
- const tasks = context.store.listTasks(context.identity.projectId);
3640
- const milestones = context.store.listMilestones(context.identity.projectId);
3641
- return applyAdoptionPosture(this.store, {
3642
- projectId: context.identity.projectId,
3643
- repositoryIdentity: context.identity,
3644
- packageVersion: PACKAGE_VERSION,
3645
- protocolVersion: PROTOCOL_VERSION,
3646
- stateSchemaVersion: STATE_SCHEMA_VERSION,
3647
- tasks,
3648
- milestones,
3649
- sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
3650
- blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
3651
- now: this.now(),
3652
- }, actor, confirmationCode);
4150
+ const task = this.store.readTask(context.identity.projectId, taskId);
4151
+ assertExpectedRevision(task, expectedRevision);
4152
+ const preflight = this.pendingReviewDependencyPreflight(repository, taskId, source);
4153
+ if (!preflight.eligible || !preflight.baseTransportCommit) {
4154
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight changed.', preflight);
4155
+ }
4156
+ const now = this.now();
4157
+ const nextTask = { ...task, revision: task.revision + 1, updatedAt: now.toISOString(),
4158
+ systemCommits: [...task.systemCommits, preflight.taskHead],
4159
+ dependencyProvenanceRecoveries: [...(task.dependencyProvenanceRecoveries ?? []), {
4160
+ commitSha: preflight.taskHead, parentCommitSha: source.completionCommit,
4161
+ packageName: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, files: ['package-lock.json', 'package.json'],
4162
+ actor: actor.trim(), reason: reason.trim(), recordedAt: now.toISOString(),
4163
+ }] };
4164
+ const projectRoot = this.store.projectRoot(task.projectId);
4165
+ const event = preparePendingReviewUpdateEvent({ source, baseTransportCommit: preflight.baseTransportCommit }, preflight.taskHead, now.toISOString());
4166
+ applyProjectTransaction({ projectRoot, projectId: task.projectId,
4167
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
4168
+ kind: 'pending-review-dependency-recovery', now,
4169
+ targets: [{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')).split(path.sep).join('/'),
4170
+ content: `${JSON.stringify(nextTask, null, 2)}\n` },
4171
+ { path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)).split(path.sep).join('/'),
4172
+ content: `${canonicalJsonStringify(event)}\n` }],
4173
+ validate: () => {
4174
+ const saved = this.store.readTask(task.projectId, task.id);
4175
+ if (saved.revision !== nextTask.revision || !verifiedPendingReviewTransport(repository, saved, source.stepId, this.store.taskRoot(task.projectId, task.id))) {
4176
+ throw new WorkflowError('STATE_CORRUPT', 'Pending review dependency recovery readback failed.');
4177
+ }
4178
+ const cycle = buildCurrentStrictStepReviewCycle(this.store, saved, source.stepId);
4179
+ if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== source.pendingEventHash) {
4180
+ throw new WorkflowError('STATE_CORRUPT', 'Pending review changed during dependency recovery.');
4181
+ }
4182
+ },
4183
+ });
4184
+ return this.store.readTask(task.projectId, task.id);
3653
4185
  }
3654
4186
  updatePreflight(repository) {
3655
4187
  const snapshot = this.observationSnapshot(repository);
@@ -3663,6 +4195,9 @@ export class WorkflowService {
3663
4195
  const now = this.now().getTime();
3664
4196
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
3665
4197
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4198
+ const pendingPreExecutionReplanTaskIds = tasks
4199
+ .filter((task) => readCurrentPreExecutionReplan(this.store, task) !== null)
4200
+ .map((task) => task.id);
3666
4201
  const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
3667
4202
  const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
3668
4203
  const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
@@ -3690,6 +4225,9 @@ export class WorkflowService {
3690
4225
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
3691
4226
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
3692
4227
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
4228
+ ...(pendingPreExecutionReplanTaskIds.length > 0
4229
+ ? [`Complete the pending pre-execution Plan replacement and reauthorization first: ${pendingPreExecutionReplanTaskIds.join(', ')}.`]
4230
+ : []),
3693
4231
  ...versionBlockers,
3694
4232
  ...observationPreflightBlockers(snapshot.observation),
3695
4233
  ];
@@ -3715,10 +4253,12 @@ export class WorkflowService {
3715
4253
  milestoneBases,
3716
4254
  },
3717
4255
  dependencyTransport: {
3718
- safe: versionBlockers.length === 0,
3719
- requiredAction: versionBlockers.length === 0
3720
- ? null
3721
- : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
4256
+ safe: versionBlockers.length === 0 && pendingPreExecutionReplanTaskIds.length === 0,
4257
+ requiredAction: pendingPreExecutionReplanTaskIds.length > 0
4258
+ ? 'Complete each pending pre-execution task plan-set and reauthorization before changing Workflow dependency HEAD.'
4259
+ : versionBlockers.length === 0
4260
+ ? null
4261
+ : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
3722
4262
  },
3723
4263
  adoption: snapshot.adoption,
3724
4264
  observation: snapshot.observation,
@@ -3734,7 +4274,299 @@ export class WorkflowService {
3734
4274
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3735
4275
  if (!task)
3736
4276
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3737
- return this.inspectDependencyProvenanceCandidate(snapshot, task);
4277
+ const candidate = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, headCommit(snapshot.identity.repositoryRoot));
4278
+ const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, candidate?.parentCommitSha);
4279
+ const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
4280
+ if (!preflight.rootCauseReplan)
4281
+ return preflight;
4282
+ let baseTip = null;
4283
+ let baseDependency = null;
4284
+ try {
4285
+ baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
4286
+ baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
4287
+ }
4288
+ catch {
4289
+ // The blocker below keeps a missing or unreadable base reference fail closed.
4290
+ }
4291
+ const bridgeBlockers = [
4292
+ ...(preflight.dependencyCommits.length !== 1
4293
+ ? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
4294
+ ...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
4295
+ ? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
4296
+ ...(preflight.parentCommitSha
4297
+ && workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
4298
+ ? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
4299
+ ...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
4300
+ ? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
4301
+ ...(!baseDependency
4302
+ || baseDependency.commitSha !== baseTip
4303
+ || baseDependency.packageVersion !== PACKAGE_VERSION
4304
+ || workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
4305
+ !== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
4306
+ ? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
4307
+ ];
4308
+ return bridgeBlockers.length === 0
4309
+ ? preflight
4310
+ : {
4311
+ ...preflight,
4312
+ eligible: false,
4313
+ action: null,
4314
+ commitSha: null,
4315
+ parentCommitSha: null,
4316
+ blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
4317
+ };
4318
+ }
4319
+ correctiveCarryoverUpdatePreflight(repository, taskId, candidate = null) {
4320
+ const snapshot = this.observationSnapshot(repository);
4321
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4322
+ if (!task)
4323
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4324
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4325
+ const sourceVersion = '2.0.0-beta.13.6';
4326
+ const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
4327
+ const sourceSurfaces = {
4328
+ declared: versions.declared,
4329
+ locked: versions.locked,
4330
+ installed: versions.installed,
4331
+ currentBranch: versions.currentBranch,
4332
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4333
+ };
4334
+ const c1 = readTaskC1Posture(this.store, task);
4335
+ if (c1.state !== 'claimed') {
4336
+ return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
4337
+ }
4338
+ const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4339
+ const now = this.now().getTime();
4340
+ const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
4341
+ const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4342
+ const transactionCount = snapshot.observation.pendingProjectTransactions.length
4343
+ + snapshot.observation.pendingMilestoneTransactions.length
4344
+ + snapshot.observation.pendingTaskTransactions.length;
4345
+ const authorizationBlockers = [];
4346
+ if (assessment.eligible) {
4347
+ try {
4348
+ this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
4349
+ }
4350
+ catch (error) {
4351
+ authorizationBlockers.push(error instanceof Error ? error.message : String(error));
4352
+ }
4353
+ }
4354
+ const baseBlockers = [
4355
+ ...assessment.blockers,
4356
+ ...authorizationBlockers,
4357
+ ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4358
+ ...Object.entries(sourceSurfaces)
4359
+ .filter(([, version]) => version !== sourceVersion)
4360
+ .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4361
+ ...observationPreflightBlockers(snapshot.observation),
4362
+ ...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
4363
+ ...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
4364
+ ...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
4365
+ ...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
4366
+ ? ['The exact original Worker C1 claim must remain current.'] : []),
4367
+ ...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
4368
+ ...(staleLeases.some((lease) => lease.entityId !== task.id
4369
+ || c1.state !== 'claimed' || lease.owner !== c1.claimant
4370
+ || createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
4371
+ ? ['No unrelated or differently owned stale writer lease may exist.'] : []),
4372
+ ...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
4373
+ ];
4374
+ const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
4375
+ ? staleLeases[0]
4376
+ : null;
4377
+ const blockers = [
4378
+ ...baseBlockers,
4379
+ ...(repairableStale
4380
+ ? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
4381
+ : []),
4382
+ ];
4383
+ const compatibility = assessment.compatibility;
4384
+ return {
4385
+ readOnly: true,
4386
+ eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
4387
+ action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
4388
+ ? 'update corrective-carryover-transport'
4389
+ : null,
4390
+ projectId: task.projectId,
4391
+ taskId: task.id,
4392
+ taskRevision: task.revision,
4393
+ stepId: compatibility?.stepId ?? null,
4394
+ sourceVersion,
4395
+ targetVersion,
4396
+ sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
4397
+ taskBranch: task.taskBranch,
4398
+ baseBranch: task.baseBranch,
4399
+ dirtyFiles: compatibility?.dirtyFiles ?? [],
4400
+ dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
4401
+ compatibilityHash: compatibility?.compatibilityHash ?? null,
4402
+ c1Posture: c1.state === 'claimed' ? {
4403
+ state: 'claimed',
4404
+ claimant: c1.claimant,
4405
+ claimEventId: compatibility?.c1ClaimEventId ?? null,
4406
+ claimEventHash: compatibility?.c1ClaimEventHash ?? null,
4407
+ } : null,
4408
+ staleLeaseRepair: repairableStale ? [{
4409
+ action: 'locks repair',
4410
+ entityId: task.id,
4411
+ owner: repairableStale.owner,
4412
+ expiresAt: repairableStale.expiresAt,
4413
+ }] : [],
4414
+ requiredTransport: [
4415
+ 'Commit only package.json and package-lock.json on the active Milestone base.',
4416
+ 'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
4417
+ 'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
4418
+ ],
4419
+ blockers,
4420
+ };
4421
+ }
4422
+ consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
4423
+ const repositoryRoot = snapshot.identity.repositoryRoot;
4424
+ const sourceVersion = '2.0.0-beta.13.6';
4425
+ const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
4426
+ const c1 = readTaskC1Posture(this.store, task);
4427
+ const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
4428
+ const currentHead = headCommit(repositoryRoot);
4429
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
4430
+ const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
4431
+ const navigation = this.next(repositoryRoot, task.id);
4432
+ const action = String(navigation.action);
4433
+ const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
4434
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
4435
+ const terminal = readRemediationEvents(this.store, task)
4436
+ .filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
4437
+ const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
4438
+ const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
4439
+ const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
4440
+ const missingClaimOnly = assessment.blockers.length === 1
4441
+ && assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
4442
+ const rollingWave = navigation.rollingWave;
4443
+ const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
4444
+ && rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
4445
+ || (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
4446
+ || (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
4447
+ && c1.state === 'pending' && c1.targetActor === yielded?.actor);
4448
+ const blockers = [
4449
+ ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4450
+ ...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
4451
+ .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4452
+ ...observationPreflightBlockers(snapshot.observation),
4453
+ ...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
4454
+ ...(snapshot.observation.pendingProjectTransactions.length
4455
+ + snapshot.observation.pendingMilestoneTransactions.length
4456
+ + snapshot.observation.pendingTaskTransactions.length
4457
+ + snapshot.observation.corruptTaskTransactions.length
4458
+ + snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
4459
+ ...(!recorded || recorded.stepId === stepId
4460
+ || task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
4461
+ ? ['A different, verified completed Step must own the historical carryover binding.'] : []),
4462
+ ...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
4463
+ ...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
4464
+ || task.workspaceOwner !== 'local'
4465
+ || currentBranch(repositoryRoot) !== task.taskBranch
4466
+ || task.steps.some((candidate) => candidate.status === 'in_progress')
4467
+ ? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
4468
+ ...(!step || dirtyFiles.length === 0
4469
+ || dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
4470
+ ? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
4471
+ ...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
4472
+ || terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
4473
+ ? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
4474
+ ...(!yielded || !terminal || yielded.planHash === null
4475
+ || !Number.isFinite(Date.parse(yielded.recordedAt))
4476
+ || Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
4477
+ ? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
4478
+ ];
4479
+ let candidateBinding = null;
4480
+ if (action === 'task plan-set') {
4481
+ if (!candidate) {
4482
+ blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
4483
+ }
4484
+ else {
4485
+ try {
4486
+ validatePlan(candidate.plan);
4487
+ const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
4488
+ if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
4489
+ throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
4490
+ }
4491
+ const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
4492
+ if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
4493
+ throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
4494
+ }
4495
+ if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
4496
+ throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
4497
+ }
4498
+ if (candidate.plan.objective !== task.planObjective
4499
+ || canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
4500
+ || canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
4501
+ throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
4502
+ }
4503
+ const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
4504
+ this.validateKnowledgeTargets(map, candidate.plan);
4505
+ const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
4506
+ if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
4507
+ === normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
4508
+ throw new Error('Candidate must change the failed Plan semantics before source recovery.');
4509
+ }
4510
+ if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
4511
+ throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
4512
+ }
4513
+ const auditTask = {
4514
+ ...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
4515
+ knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
4516
+ knowledgeMapHash: candidate.plan.knowledgeMapHash,
4517
+ steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
4518
+ ...candidateStep, status: 'planned', evidence: null, failurePause: null,
4519
+ }),
4520
+ };
4521
+ validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
4522
+ candidateBinding = {
4523
+ candidatePlanHash: prepared.planHash,
4524
+ candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
4525
+ riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
4526
+ };
4527
+ }
4528
+ catch (error) {
4529
+ blockers.push(error instanceof Error ? error.message : String(error));
4530
+ }
4531
+ }
4532
+ }
4533
+ try {
4534
+ validateTaskHistory(repositoryRoot, task);
4535
+ this.assertTaskKnowledgeBinding(repositoryRoot, task);
4536
+ const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
4537
+ if (!sourceAudit || !yielded?.planHash || !task.planHash
4538
+ || normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
4539
+ !== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
4540
+ throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
4541
+ }
4542
+ }
4543
+ catch (error) {
4544
+ blockers.push(error instanceof Error ? error.message : String(error));
4545
+ }
4546
+ const binding = {
4547
+ taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
4548
+ planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
4549
+ sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
4550
+ completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
4551
+ terminalRemediationEventHash: terminal?.eventHash ?? null,
4552
+ correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
4553
+ c1Posture: c1.state,
4554
+ candidateBinding,
4555
+ };
4556
+ return {
4557
+ readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
4558
+ action: null, transportAllowed: false,
4559
+ ...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
4560
+ compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
4561
+ sourceRecovery: blockers.length === 0 ? {
4562
+ eligible: true, action,
4563
+ ...(action === 'task plan-set' ? { ...candidateBinding } : {}),
4564
+ ...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
4565
+ ...(action === 'task claim' ? { actor: yielded.actor } : {}),
4566
+ instruction: 'Execute only this exact action with the installed source package; run source status then next and repeat this read-only preflight. Ordinary Plan Risk Audit and execution authorization retain their normal gates.',
4567
+ } : null,
4568
+ blockers,
4569
+ };
3738
4570
  }
3739
4571
  historicalStepProvenanceRecoveryPreflight(repository, taskId) {
3740
4572
  const snapshot = this.observationSnapshot(repository);
@@ -4205,7 +5037,9 @@ export class WorkflowService {
4205
5037
  ? Boolean(record.historicalStepProvenance)
4206
5038
  : recoveryMode === 'plan-integrity'
4207
5039
  ? Boolean(record.planIntegrity)
4208
- : Boolean(record.activeDownstreamProof) === recoveryMode));
5040
+ : recoveryMode === false && record.rootCauseReplan
5041
+ ? true
5042
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
4209
5043
  if (prior && current.systemCommits.includes(currentHead))
4210
5044
  return current;
4211
5045
  assertExpectedRevision(current, expectedRevision);
@@ -4221,13 +5055,26 @@ export class WorkflowService {
4221
5055
  ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
4222
5056
  : recoveryMode
4223
5057
  ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4224
- : this.inspectDependencyProvenanceCandidate(snapshot, task);
5058
+ : this.dependencyProvenanceRecoveryPreflight(repository, taskId);
4225
5059
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
4226
5060
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
4227
5061
  taskId,
4228
5062
  blockers: preflight.blockers,
4229
5063
  });
4230
5064
  }
5065
+ if (preflight.rootCauseReplan) {
5066
+ const posture = readTaskC1Posture(this.store, task);
5067
+ if (posture.state !== 'claimed'
5068
+ || posture.claimant !== preflight.rootCauseReplan.claimant
5069
+ || normalizedActor !== posture.claimant) {
5070
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
5071
+ taskId,
5072
+ actor: normalizedActor,
5073
+ requiredActor: preflight.rootCauseReplan.claimant,
5074
+ posture: posture.state,
5075
+ });
5076
+ }
5077
+ }
4231
5078
  const now = this.now();
4232
5079
  const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
4233
5080
  const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
@@ -4253,6 +5100,9 @@ export class WorkflowService {
4253
5100
  ...(isTerminalCommit && preflight.planIntegrity
4254
5101
  ? { planIntegrity: preflight.planIntegrity }
4255
5102
  : {}),
5103
+ ...(isTerminalCommit && preflight.rootCauseReplan
5104
+ ? { rootCauseReplan: preflight.rootCauseReplan }
5105
+ : {}),
4256
5106
  };
4257
5107
  });
4258
5108
  const nextTask = {
@@ -4579,6 +5429,18 @@ export class WorkflowService {
4579
5429
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
4580
5430
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
4581
5431
  }
5432
+ explainChange(repository, taskId, input) {
5433
+ const status = this.statusFromSnapshot(this.observationSnapshot(repository));
5434
+ const task = status.tasks.find(candidate => candidate.id === taskId);
5435
+ if (!task && status.observation.kind === 'assessment') {
5436
+ throw new WorkflowError('INVALID_ARGUMENT', 'The requested Task is not present in the current observation.');
5437
+ }
5438
+ const navigation = this.nextFromStatus(repository, taskId, status);
5439
+ const step = task?.steps.find(candidate => candidate.id === navigation.stepId)
5440
+ ?? task?.steps.find(candidate => candidate.status === 'in_progress' || candidate.status === 'failed');
5441
+ const effectiveTask = task && step ? taskWithEffectiveStepAllowedWrites(this.store, task, step.id) : task;
5442
+ return explainChange(input, repository, effectiveTask, navigation);
5443
+ }
4582
5444
  next(repository, taskId = null) {
4583
5445
  return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
4584
5446
  }
@@ -4690,6 +5552,37 @@ export class WorkflowService {
4690
5552
  }
4691
5553
  const withAdoption = (value) => ({
4692
5554
  ...value,
5555
+ ...(status.milestones.some((milestone) => milestone.status === 'active' && milestone.planHash)
5556
+ ? { activeMilestoneAutonomyOptions: status.milestones
5557
+ .filter((milestone) => milestone.status === 'active' && milestone.planHash)
5558
+ .filter((milestone) => {
5559
+ const event = readMilestoneAutonomyEvents(this.store, status.projectId, milestone.id).at(-1);
5560
+ if (!event)
5561
+ return true;
5562
+ try {
5563
+ requireActiveMilestoneAutonomy({
5564
+ store: this.store, projectId: status.projectId, milestone,
5565
+ actor: event.delegate, now: this.now(),
5566
+ });
5567
+ return false;
5568
+ }
5569
+ catch (error) {
5570
+ if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
5571
+ return true;
5572
+ if (error instanceof WorkflowError && error.code === 'NOT_FOUND')
5573
+ return true;
5574
+ throw error;
5575
+ }
5576
+ })
5577
+ .map((milestone) => ({
5578
+ action: 'milestone autonomy-prepare',
5579
+ milestoneId: milestone.id,
5580
+ expectedRevision: milestone.revision,
5581
+ requiresExactHumanConfirmation: true,
5582
+ maximumDurationHours: 72,
5583
+ preparationRequired: true,
5584
+ meaning: 'Optional human-requested preparation; existing valid contracts cannot be replaced. Knowledge must be active.',
5585
+ })) } : {}),
4693
5586
  adoption: adoptionInfo,
4694
5587
  observation: status.observation,
4695
5588
  });
@@ -4742,6 +5635,19 @@ export class WorkflowService {
4742
5635
  ?? prioritizedRemediationTask
4743
5636
  ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
4744
5637
  if (activeTask) {
5638
+ let pendingReviewDependencyRecovery = null;
5639
+ try {
5640
+ const reconstructed = pendingReviewSourceBinding(repositoryRoot, this.store, activeTask, runGit(repositoryRoot, ['rev-parse', 'HEAD^']), runGit(repositoryRoot, ['rev-parse', `${activeTask.baseBranch}^`]));
5641
+ pendingReviewDependencyRecovery = this.pendingReviewDependencyPreflight(repositoryRoot, activeTask.id, reconstructed);
5642
+ }
5643
+ catch { /* A nonmatching shape follows ordinary navigation without a compatibility bypass. */ }
5644
+ if (pendingReviewDependencyRecovery?.eligible) {
5645
+ return withAdoption({ scope: 'task', id: activeTask.id, status: activeTask.status,
5646
+ action: 'update pending-review-dependency-recover', revision: activeTask.revision,
5647
+ pendingReviewDependencyRecovery,
5648
+ sourceReceiptRequired: true,
5649
+ });
5650
+ }
4745
5651
  const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
4746
5652
  if (historicalStepProvenanceRecovery.eligible) {
4747
5653
  return withAdoption({
@@ -4905,13 +5811,18 @@ export class WorkflowService {
4905
5811
  && !activeTask.steps.some((step) => step.status === 'failed');
4906
5812
  if (activeTask.planHash && !requiresReplacementPlan) {
4907
5813
  try {
4908
- activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
5814
+ activePlanRiskAudit = readCorrectivePlanningAudit(this.store, activeTask);
4909
5815
  }
4910
5816
  catch (error) {
4911
5817
  if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
4912
5818
  throw error;
4913
5819
  const blockingDecision = error.details.decision;
4914
5820
  const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
5821
+ const preExecutionReplan = readCurrentPreExecutionReplan(this.store, activeTask);
5822
+ const verifiedPreExecutionReplan = preExecutionReplan
5823
+ && blockingAudit?.eventHash === preExecutionReplan.obstructionPlanRiskAuditEventHash
5824
+ ? resolvePreExecutionReplanAudits(this.store, activeTask, preExecutionReplan)
5825
+ : null;
4915
5826
  const existingRemediationEvent = blockingAudit && activeTask.milestoneId
4916
5827
  ? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
4917
5828
  .reverse()
@@ -4930,6 +5841,9 @@ export class WorkflowService {
4930
5841
  : new Map();
4931
5842
  const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
4932
5843
  const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
5844
+ && activeTask.planHash !== null
5845
+ && activeTask.workspaceOwner === null
5846
+ && activeTask.steps.every(step => step.status === 'planned')
4933
5847
  && remediationMilestone?.status === 'active'
4934
5848
  && remediationMembership?.disposition === 'required';
4935
5849
  const remediationAncestorIds = new Set();
@@ -4945,50 +5859,34 @@ export class WorkflowService {
4945
5859
  const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
4946
5860
  .filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
4947
5861
  .sort();
4948
- planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
5862
+ planRiskAuditRecovery = verifiedPreExecutionReplan && preExecutionReplan
4949
5863
  ? {
4950
5864
  scope: 'task',
4951
5865
  id: activeTask.id,
4952
5866
  status: activeTask.status,
4953
- action: existingRemediationTask?.status === 'merged'
4954
- ? 'task plan-set'
4955
- : existingRemediationTask
4956
- ? 'doctor'
4957
- : remediationDiscovery?.status === 'ready_to_materialize'
4958
- ? 'milestone remediation-materialize'
4959
- : remediationDiscovery
4960
- ? 'discovery update'
4961
- : 'discovery start',
5867
+ action: 'task plan-set',
4962
5868
  blockedAction: 'task authorize',
4963
5869
  revision: activeTask.revision,
4964
- planRiskAuditDecision: {
4965
- state: 'recorded-blocking',
4966
- decision: blockingDecision,
4967
- reason: error.message,
4968
- requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
5870
+ preExecutionReplan: {
5871
+ state: 'recorded',
5872
+ eventId: preExecutionReplan.eventId,
5873
+ eventHash: preExecutionReplan.eventHash,
5874
+ sourcePlanHash: preExecutionReplan.sourcePlanHash,
5875
+ sourceHeadCommit: preExecutionReplan.sourceHeadCommit,
5876
+ obstructionPlanRiskAuditEventId: preExecutionReplan.obstructionPlanRiskAuditEventId,
5877
+ obstructionPlanRiskAuditEventHash: preExecutionReplan.obstructionPlanRiskAuditEventHash,
5878
+ preservesTaskScope: true,
5879
+ preservesCompletedStepAuthority: true,
5880
+ requiredAction: 'Replace the obstructed implementation Plan through ordinary task plan-set.',
4969
5881
  },
4970
- auditedRemediation: {
4971
- state: existingRemediationTask?.status === 'merged'
4972
- ? 'merged-replan-required'
4973
- : existingRemediationTask
4974
- ? 'materialized-not-merged'
4975
- : remediationDiscovery?.status === 'ready_to_materialize'
4976
- ? 'ready-to-materialize'
4977
- : 'discovery-required',
4978
- humanApprovalRequired: false,
4979
- blockedTaskId: activeTask.id,
4980
- blockedTaskRevision: activeTask.revision,
4981
- milestoneId: remediationMilestone.id,
4982
- milestoneRevision: remediationMilestone.revision,
4983
- planRiskAuditEventId: blockingAudit?.eventId ?? null,
4984
- planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
4985
- discoveryId: remediationDiscovery?.id ?? null,
4986
- discoveryRevision: remediationDiscovery?.revision ?? null,
4987
- remediationTaskId: existingRemediationTask?.id ?? null,
4988
- remediationTaskStatus: existingRemediationTask?.status ?? null,
4989
- requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
4990
- eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
4991
- command: existingRemediationTask?.status === 'merged'
5882
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5883
+ }
5884
+ : blockingDecision === 'stop-escalate' && auditedRemediationEligible
5885
+ ? {
5886
+ scope: 'task',
5887
+ id: activeTask.id,
5888
+ status: activeTask.status,
5889
+ action: existingRemediationTask?.status === 'merged'
4992
5890
  ? 'task plan-set'
4993
5891
  : existingRemediationTask
4994
5892
  ? 'doctor'
@@ -4997,46 +5895,84 @@ export class WorkflowService {
4997
5895
  : remediationDiscovery
4998
5896
  ? 'discovery update'
4999
5897
  : 'discovery start',
5000
- boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5001
- preservesMilestoneSemantics: true,
5002
- },
5003
- taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5004
- }
5005
- : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5006
- ? {
5007
- scope: 'task',
5008
- id: activeTask.id,
5009
- status: activeTask.status,
5010
- action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5011
5898
  blockedAction: 'task authorize',
5012
5899
  revision: activeTask.revision,
5013
5900
  planRiskAuditDecision: {
5014
5901
  state: 'recorded-blocking',
5015
5902
  decision: blockingDecision,
5016
5903
  reason: error.message,
5017
- requiredAction: blockingDecision === 'stop-escalate'
5018
- ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5019
- : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5904
+ requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
5020
5905
  },
5021
- ...(blockingDecision === 'stop-escalate'
5022
- ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5023
- : {}),
5024
- }
5025
- : {
5026
- scope: 'task',
5027
- id: activeTask.id,
5028
- status: activeTask.status,
5029
- action: 'task plan-risk-audit',
5030
- revision: activeTask.revision,
5031
- planRiskAuditRecovery: {
5032
- state: 'required',
5033
- reason: error.message,
5034
- currentBriefHash: activeTask.briefHash,
5035
- currentPlanHash: activeTask.planHash,
5036
- knowledgeMapRevision: activeTask.knowledgeMapRevision,
5037
- knowledgeMapHash: activeTask.knowledgeMapHash,
5906
+ auditedRemediation: {
5907
+ state: existingRemediationTask?.status === 'merged'
5908
+ ? 'merged-replan-required'
5909
+ : existingRemediationTask
5910
+ ? 'materialized-not-merged'
5911
+ : remediationDiscovery?.status === 'ready_to_materialize'
5912
+ ? 'ready-to-materialize'
5913
+ : 'discovery-required',
5914
+ humanApprovalRequired: false,
5915
+ blockedTaskId: activeTask.id,
5916
+ blockedTaskRevision: activeTask.revision,
5917
+ milestoneId: remediationMilestone.id,
5918
+ milestoneRevision: remediationMilestone.revision,
5919
+ planRiskAuditEventId: blockingAudit?.eventId ?? null,
5920
+ planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
5921
+ discoveryId: remediationDiscovery?.id ?? null,
5922
+ discoveryRevision: remediationDiscovery?.revision ?? null,
5923
+ remediationTaskId: existingRemediationTask?.id ?? null,
5924
+ remediationTaskStatus: existingRemediationTask?.status ?? null,
5925
+ requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
5926
+ eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
5927
+ command: existingRemediationTask?.status === 'merged'
5928
+ ? 'task plan-set'
5929
+ : existingRemediationTask
5930
+ ? 'doctor'
5931
+ : remediationDiscovery?.status === 'ready_to_materialize'
5932
+ ? 'milestone remediation-materialize'
5933
+ : remediationDiscovery
5934
+ ? 'discovery update'
5935
+ : 'discovery start',
5936
+ boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5937
+ preservesMilestoneSemantics: true,
5038
5938
  },
5039
- };
5939
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5940
+ }
5941
+ : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5942
+ ? {
5943
+ scope: 'task',
5944
+ id: activeTask.id,
5945
+ status: activeTask.status,
5946
+ action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5947
+ blockedAction: 'task authorize',
5948
+ revision: activeTask.revision,
5949
+ planRiskAuditDecision: {
5950
+ state: 'recorded-blocking',
5951
+ decision: blockingDecision,
5952
+ reason: error.message,
5953
+ requiredAction: blockingDecision === 'stop-escalate'
5954
+ ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5955
+ : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5956
+ },
5957
+ ...(blockingDecision === 'stop-escalate'
5958
+ ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5959
+ : {}),
5960
+ }
5961
+ : {
5962
+ scope: 'task',
5963
+ id: activeTask.id,
5964
+ status: activeTask.status,
5965
+ action: 'task plan-risk-audit',
5966
+ revision: activeTask.revision,
5967
+ planRiskAuditRecovery: {
5968
+ state: 'required',
5969
+ reason: error.message,
5970
+ currentBriefHash: activeTask.briefHash,
5971
+ currentPlanHash: activeTask.planHash,
5972
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
5973
+ knowledgeMapHash: activeTask.knowledgeMapHash,
5974
+ },
5975
+ };
5040
5976
  }
5041
5977
  }
5042
5978
  const taskNext = activeTask.status === 'planning'
@@ -5086,7 +6022,7 @@ export class WorkflowService {
5086
6022
  || taskNext?.action === 'task downstream-proof-recover'
5087
6023
  || taskNext?.action === 'task remediation-mode-recover'
5088
6024
  || (taskNext?.action === 'task writer-credential-replace'
5089
- && taskNext?.blockedAction === 'task remediation-mode-recover')
6025
+ && ['task step-complete', 'task step-review', 'task remediation-mode-recover'].includes(String(taskNext?.blockedAction)))
5090
6026
  || taskNext?.action === 'task plan-integrity-recover'
5091
6027
  || taskNext?.blockedAction === 'task plan-integrity-recover'
5092
6028
  || taskNext?.action === 'task corrective-yield'
@@ -5162,6 +6098,7 @@ export class WorkflowService {
5162
6098
  contextRefresh: {
5163
6099
  action: 'task context-refresh',
5164
6100
  mode: refreshAssessment.mode,
6101
+ authority: readFailedStepPlanningRecovery(this.store, activeTask) ? 'planning-only' : 'execution',
5165
6102
  addedSources: refreshAssessment.addedSources,
5166
6103
  options: contextRefreshOptions,
5167
6104
  },
@@ -5518,7 +6455,9 @@ export class WorkflowService {
5518
6455
  return { milestone, authorization, scopeChange };
5519
6456
  }
5520
6457
  nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
5521
- const transition = task.status === 'awaiting_execution_authorization'
6458
+ const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
6459
+ // A stopped rebound Plan can expose planning recovery, never an approval route.
6460
+ const transition = planningRecovery ? null : task.status === 'awaiting_execution_authorization'
5522
6461
  ? 'task.execution_authorize'
5523
6462
  : task.status === 'awaiting_final_acceptance'
5524
6463
  ? 'task.final_accept'
@@ -5559,6 +6498,9 @@ export class WorkflowService {
5559
6498
  }
5560
6499
  const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
5561
6500
  const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
6501
+ const rootCauseStepId = nextStepId
6502
+ ?? task.steps.find((candidate) => candidate.status === 'planned')?.id
6503
+ ?? null;
5562
6504
  const downstreamProofCarryover = next.action === 'task run'
5563
6505
  && nextStepId
5564
6506
  && changedFiles(repositoryRoot).length > 0
@@ -5569,6 +6511,24 @@ export class WorkflowService {
5569
6511
  && changedFiles(repositoryRoot).length > 0
5570
6512
  ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
5571
6513
  : null;
6514
+ let rootCauseReplanDirtyCarryover = ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
6515
+ && rootCauseStepId
6516
+ && changedFiles(repositoryRoot).length > 0
6517
+ ? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
6518
+ : null;
6519
+ if (rootCauseReplanDirtyCarryover?.eligible) {
6520
+ try {
6521
+ this.assertExecutionAuthorizationFresh(task, repositoryRoot);
6522
+ }
6523
+ catch (error) {
6524
+ rootCauseReplanDirtyCarryover = {
6525
+ applicable: true,
6526
+ eligible: false,
6527
+ compatibility: null,
6528
+ blockers: [error instanceof Error ? error.message : String(error)],
6529
+ };
6530
+ }
6531
+ }
5572
6532
  let mechanicalFeasibilityOverlay = {};
5573
6533
  if (task.status === 'awaiting_execution_authorization' && task.planHash) {
5574
6534
  try {
@@ -5912,8 +6872,8 @@ export class WorkflowService {
5912
6872
  ...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
5913
6873
  ? [`${guardedCorrectivePosture.stepId} must remain failed.`]
5914
6874
  : []),
5915
- ...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
5916
- ...(rollingC1Posture.state !== 'none'
6875
+ ...(writerLease && rollingC1Posture.state !== 'claimed' ? ['No active or stale writer lease may remain after the failure pause.'] : []),
6876
+ ...(rollingC1Posture.state !== 'none' && rollingC1Posture.state !== 'claimed'
5917
6877
  ? ['No C1 claim may remain after the root-cause failure pause.']
5918
6878
  : []),
5919
6879
  ];
@@ -5930,7 +6890,7 @@ export class WorkflowService {
5930
6890
  },
5931
6891
  }
5932
6892
  : {
5933
- action: 'task plan-set',
6893
+ action: rollingC1Posture.state === 'claimed' ? 'task corrective-yield' : 'task plan-set',
5934
6894
  blockedAction: 'task run',
5935
6895
  stepId: guardedCorrectivePosture.stepId,
5936
6896
  rollingWave: {
@@ -6077,9 +7037,42 @@ export class WorkflowService {
6077
7037
  },
6078
7038
  }
6079
7039
  : {};
7040
+ const preExecutionReplanBlockers = task.status === 'ready'
7041
+ ? [
7042
+ ...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may be in progress.'] : []),
7043
+ ...(task.steps.some((step) => step.status === 'failed') ? ['No failed Step may require corrective recovery.'] : []),
7044
+ ...(handoffPosture.state !== 'none' ? ['No C1 claim or pending handoff may exist.'] : []),
7045
+ ...(inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
7046
+ ? ['No active or stale writer lease may exist.']
7047
+ : []),
7048
+ ...(!isClean(repositoryRoot) ? ['Repository checkout must be clean.'] : []),
7049
+ ...(!planRiskAudit || !['approved', 'approved-with-rationale'].includes(planRiskAudit.audit.decision)
7050
+ ? ['A current positive Plan Risk Audit is required.']
7051
+ : []),
7052
+ ]
7053
+ : [];
7054
+ const preExecutionReplanOption = task.status === 'ready' && task.planHash && planRiskAudit
7055
+ ? {
7056
+ command: 'task preexecution-replan',
7057
+ state: preExecutionReplanBlockers.length === 0 ? 'eligible' : 'blocked',
7058
+ expectedRevision: task.revision,
7059
+ expectedBriefHash: task.briefHash,
7060
+ expectedPlanHash: task.planHash,
7061
+ expectedPlanRiskAuditEventHash: planRiskAudit.audit.eventHash,
7062
+ expectedHeadCommit: headCommit(repositoryRoot),
7063
+ auditDecision: 'stop-escalate',
7064
+ actorMustEqualAuditAuditor: true,
7065
+ humanApprovalRequired: false,
7066
+ boundedEffect: 'record-current-plan-obstruction-and-withdraw-execution-authorization',
7067
+ preservesTaskScope: true,
7068
+ preservesCompletedStepAuthority: true,
7069
+ blockers: preExecutionReplanBlockers,
7070
+ }
7071
+ : null;
6080
7072
  const result = {
6081
7073
  ...next,
6082
7074
  ...handoffOverlay,
7075
+ ...(preExecutionReplanOption ? { preExecutionReplanOption } : {}),
6083
7076
  ...(strictStepReviewStep
6084
7077
  ? {
6085
7078
  action: 'task step-review',
@@ -6165,7 +7158,7 @@ export class WorkflowService {
6165
7158
  downstreamProofCarryover: {
6166
7159
  state: 'validated',
6167
7160
  invalidationEventId: downstreamProofCarryover.invalidationEventId,
6168
- stepId: nextStepId,
7161
+ stepId: rootCauseStepId,
6169
7162
  dirtyFiles: downstreamProofCarryover.dirtyFiles,
6170
7163
  dirtyWorktreeHash: downstreamProofCarryover.dirtyWorktreeHash,
6171
7164
  meaning: 'The replacement Step may adopt exactly these preserved uncommitted bytes on task run.',
@@ -6205,6 +7198,52 @@ export class WorkflowService {
6205
7198
  },
6206
7199
  }
6207
7200
  : {}),
7201
+ ...(rootCauseReplanDirtyCarryover?.applicable
7202
+ ? rootCauseReplanDirtyCarryover.eligible && rootCauseReplanDirtyCarryover.compatibility
7203
+ ? {
7204
+ action: 'task run',
7205
+ rootCauseReplanDirtyCarryover: {
7206
+ state: 'validated',
7207
+ stepId: nextStepId,
7208
+ compatibilityHash: rootCauseReplanDirtyCarryover.compatibility.compatibilityHash,
7209
+ dirtyFiles: rootCauseReplanDirtyCarryover.compatibility.dirtyFiles,
7210
+ dirtyWorktreeHash: rootCauseReplanDirtyCarryover.compatibility.dirtyWorktreeHash,
7211
+ meaning: 'The replacement Step may adopt the exact retained root-cause failure bytes.',
7212
+ },
7213
+ }
7214
+ : rootCauseReplanDirtyCarryover.blockers.length === 1
7215
+ && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1')
7216
+ ? handoffPosture.state === 'none'
7217
+ ? {
7218
+ action: 'task handoff-prepare',
7219
+ blockedAction: 'task run',
7220
+ rootCauseReplanDirtyCarryover: {
7221
+ state: 'awaiting-c1',
7222
+ stepId: rootCauseStepId,
7223
+ requiredActor: defaultTaskWorkerActor(task.id),
7224
+ },
7225
+ }
7226
+ : handoffPosture.state === 'pending'
7227
+ ? {}
7228
+ : {
7229
+ action: 'doctor',
7230
+ blockedAction: 'task run',
7231
+ rootCauseReplanDirtyCarryover: {
7232
+ state: 'blocked',
7233
+ stepId: rootCauseStepId,
7234
+ blockers: rootCauseReplanDirtyCarryover.blockers,
7235
+ },
7236
+ }
7237
+ : {
7238
+ action: 'doctor',
7239
+ blockedAction: 'task run',
7240
+ rootCauseReplanDirtyCarryover: {
7241
+ state: 'blocked',
7242
+ stepId: rootCauseStepId,
7243
+ blockers: rootCauseReplanDirtyCarryover.blockers,
7244
+ },
7245
+ }
7246
+ : {}),
6208
7247
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6209
7248
  ...(correctiveDerivedAuthority && correctiveDerivedActor
6210
7249
  ? {
@@ -6221,9 +7260,56 @@ export class WorkflowService {
6221
7260
  }
6222
7261
  : {}),
6223
7262
  };
6224
- if (handoffPosture.state === 'pending') {
7263
+ // A later independent audit may discover an obstruction absent from the sealed
7264
+ // fix review. Advertise the existing append-only decision route without turning
7265
+ // every ordinary repair into a mandatory replan or rewriting that review.
7266
+ const correctiveDecisionOrdinal = failedStepRemediations.length + 1;
7267
+ const optionalCorrectiveDecision = (result.action === 'task run' || planningRecovery !== null)
7268
+ && (['needs_fix', 'ready'].includes(task.status) || planningRecovery !== null)
7269
+ && failedStep && task.planHash && planRiskAudit
7270
+ && planRiskAudit.reviewRequiredStepIds.includes(failedStep.id)
7271
+ && !task.steps.some(step => step.status === 'in_progress')
7272
+ && failedStepRemediations.length >= 2
7273
+ && handoffPosture.state !== 'pending'
7274
+ && !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
7275
+ && currentBranch(repositoryRoot) === task.taskBranch && isClean(repositoryRoot)
7276
+ ? {
7277
+ command: 'task corrective-decision',
7278
+ state: 'eligible',
7279
+ optional: true,
7280
+ taskId: task.id,
7281
+ stepId: failedStep.id,
7282
+ expectedRevision: task.revision,
7283
+ expectedBriefHash: task.briefHash,
7284
+ expectedPlanHash: task.planHash,
7285
+ expectedHeadCommit: headCommit(repositoryRoot),
7286
+ triggeringAttemptCount: correctiveDecisionOrdinal,
7287
+ coveredRemediationEvents: failedStepRemediations.map(event => ({ eventId: event.eventId, eventHash: event.eventHash })),
7288
+ recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
7289
+ auditorMustDifferFromLatestStrictReviewer: true,
7290
+ requiredActor: handoffPosture.state === 'claimed' ? handoffPosture.claimant : null,
7291
+ decisions: planningRecovery ? ['replan-required', 'split-required', 'stop-escalate']
7292
+ : ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
7293
+ authority: 'none',
7294
+ preservesOriginalReview: true,
7295
+ humanApprovalRequiredForDecision: false,
7296
+ replanRequiresExistingHumanGate: true,
7297
+ requiredAction: 'Only for newly assessed evidence: obtain an independent corrective audit, refresh next and existing writer authority, then record the decision. This option does not authorize Plan replacement or extra writes.',
7298
+ }
7299
+ : null;
7300
+ const resultWithCorrectiveOption = optionalCorrectiveDecision
7301
+ ? { ...result, correctiveDecisionOption: optionalCorrectiveDecision,
7302
+ ...(planningRecovery ? { action: 'task corrective-decision', blockedAction: 'task authorize',
7303
+ planningRecovery: { authority: 'planning-only', obstructionAuditHash: planningRecovery.current.audit.eventHash,
7304
+ baselineAuditHash: planningRecovery.baseline.eventHash } } : {}) }
7305
+ : planningRecovery && result.action === 'task authorize'
7306
+ ? { ...result, action: 'doctor', blockedAction: 'task authorize' } : result;
7307
+ const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
7308
+ || (rootCauseReplanDirtyCarryover.blockers.length === 1
7309
+ && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
7310
+ if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
6225
7311
  return this.withTaskNavigationContracts(task, {
6226
- ...result,
7312
+ ...resultWithCorrectiveOption,
6227
7313
  action: 'task claim',
6228
7314
  blockedAction: next.action,
6229
7315
  });
@@ -6231,10 +7317,10 @@ export class WorkflowService {
6231
7317
  const navigableResult = ['task plan-set']
6232
7318
  .includes(String(result.action))
6233
7319
  ? {
6234
- ...result,
7320
+ ...resultWithCorrectiveOption,
6235
7321
  taskPlanContract: this.taskPlanContract(projectId, task),
6236
7322
  }
6237
- : result;
7323
+ : resultWithCorrectiveOption;
6238
7324
  return this.withTaskNavigationContracts(task, navigableResult);
6239
7325
  }
6240
7326
  taskPlanContract(projectId, task) {
@@ -6255,6 +7341,9 @@ export class WorkflowService {
6255
7341
  };
6256
7342
  }
6257
7343
  assertTaskContextChangeAllowed(task) {
7344
+ if (readCurrentPreExecutionReplan(this.store, task)) {
7345
+ 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' });
7346
+ }
6258
7347
  if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
6259
7348
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
6260
7349
  taskId: task.id,
@@ -6296,7 +7385,7 @@ export class WorkflowService {
6296
7385
  taskTerminalCorrectivePosture(task) {
6297
7386
  if (!task.planHash)
6298
7387
  return null;
6299
- const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
7388
+ const planRiskAudit = readCurrentPlanningObstructionAudit(this.store, task);
6300
7389
  if (!planRiskAudit)
6301
7390
  return null;
6302
7391
  const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
@@ -6341,7 +7430,7 @@ export class WorkflowService {
6341
7430
  }
6342
7431
  }
6343
7432
  delegatedApprovalOptions(projectId, transition, target) {
6344
- return this.store.listDelegations(projectId)
7433
+ return this.listDelegations(projectId)
6345
7434
  .filter((grant) => {
6346
7435
  try {
6347
7436
  return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
@@ -6363,7 +7452,20 @@ export class WorkflowService {
6363
7452
  }));
6364
7453
  }
6365
7454
  delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
6366
- return this.store.listDelegations(projectId)
7455
+ let planningRecovery;
7456
+ try {
7457
+ planningRecovery = readFailedStepPlanningRecovery(this.store, task);
7458
+ if (!planningRecovery)
7459
+ readCurrentPlanRiskAudit(this.store, task);
7460
+ }
7461
+ catch (error) {
7462
+ if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
7463
+ return [];
7464
+ throw error;
7465
+ }
7466
+ if (planningRecovery && mode !== 'delegated-content-only')
7467
+ return [];
7468
+ return this.listDelegations(projectId)
6367
7469
  .filter((grant) => {
6368
7470
  try {
6369
7471
  if (mode === 'delegated-plan-bounded-supporting-source') {
@@ -6382,8 +7484,8 @@ export class WorkflowService {
6382
7484
  grantId: grant.id,
6383
7485
  principal: grant.principal,
6384
7486
  delegate: grant.delegate,
6385
- transition: 'task.execution_authorize',
6386
- transitions: ['project_memory.approve', 'task.execution_authorize'],
7487
+ transition: planningRecovery ? 'project_memory.approve' : 'task.execution_authorize',
7488
+ transitions: planningRecovery ? ['project_memory.approve'] : ['project_memory.approve', 'task.execution_authorize'],
6387
7489
  scope: grant.scope,
6388
7490
  expiresAt: grant.expiresAt,
6389
7491
  policyHash: grant.policyHash,
@@ -6740,7 +7842,7 @@ export class WorkflowService {
6740
7842
  blockers,
6741
7843
  };
6742
7844
  }
6743
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
7845
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null, rootCauseReplan = null) {
6744
7846
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
6745
7847
  const now = this.now().getTime();
6746
7848
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
@@ -6760,7 +7862,89 @@ export class WorkflowService {
6760
7862
  currentBranch: versions.currentBranch,
6761
7863
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
6762
7864
  },
6763
- }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
7865
+ }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
7866
+ }
7867
+ assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
7868
+ const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
7869
+ && task.systemCommits.includes(record.commitSha)) ?? null;
7870
+ const recorded = recordedRecovery?.rootCauseReplan ?? null;
7871
+ if (recorded) {
7872
+ const recordedStep = task.steps.find((candidate) => candidate.id === recorded.stepId) ?? null;
7873
+ if (!recordedStep) {
7874
+ return {
7875
+ applicable: true,
7876
+ eligible: false,
7877
+ compatibility: null,
7878
+ blockers: [`Recorded corrective carryover Step ${recorded.stepId} is missing from the current Plan.`],
7879
+ };
7880
+ }
7881
+ if (recordedStep.status === 'completed') {
7882
+ const blockers = [];
7883
+ if (!recordedStep.evidence) {
7884
+ blockers.push(`Completed corrective carryover Step ${recorded.stepId} has no execution evidence.`);
7885
+ }
7886
+ try {
7887
+ validateTaskHistory(repositoryRoot, task, historyHead);
7888
+ const sourceAudit = readPlanRiskAuditEvents(this.store, task).find((event) => event.eventId === recorded.replacementPlanRiskAuditEventId
7889
+ && event.eventHash === recorded.replacementPlanRiskAuditEventHash) ?? null;
7890
+ const classification = sourceAudit?.stepClassifications.find((candidate) => candidate.stepId === recorded.stepId) ?? null;
7891
+ if (!sourceAudit || !classification) {
7892
+ blockers.push('Completed corrective carryover Step lost its exact replacement Plan Risk Audit binding.');
7893
+ }
7894
+ else if (classification.reviewRequired
7895
+ && recordedStep.evidence
7896
+ && !verifiedHistoricalStepReviewBindings(this.store, task).some((binding) => binding.stepId === recorded.stepId
7897
+ && binding.commitSha === recordedStep.evidence.commitSha)) {
7898
+ blockers.push(`Completed corrective carryover Step ${recorded.stepId} lacks verified strict-review proof.`);
7899
+ }
7900
+ }
7901
+ catch (error) {
7902
+ blockers.push(error instanceof Error ? error.message : String(error));
7903
+ }
7904
+ if (blockers.length > 0) {
7905
+ return {
7906
+ applicable: true,
7907
+ eligible: false,
7908
+ compatibility: null,
7909
+ blockers: [...new Set(blockers)],
7910
+ };
7911
+ }
7912
+ // A reviewed, history-valid completed Step consumed this carryover. Keep its
7913
+ // immutable recovery record, but let later Steps use their own lifecycle route.
7914
+ }
7915
+ else {
7916
+ if (stepId && stepId !== recorded.stepId) {
7917
+ return {
7918
+ applicable: true,
7919
+ eligible: false,
7920
+ compatibility: null,
7921
+ blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
7922
+ };
7923
+ }
7924
+ return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
7925
+ binding: recorded,
7926
+ });
7927
+ }
7928
+ }
7929
+ const candidates = task.steps
7930
+ .filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
7931
+ .flatMap((step) => [...new Set(readRemediationEvents(this.store, task)
7932
+ .filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed')
7933
+ .map((event) => event.failureEvidence.completionCommit))]
7934
+ .map((historyHead) => assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead })))
7935
+ .filter((assessment) => assessment.applicable);
7936
+ const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
7937
+ if (eligible.length === 1)
7938
+ return eligible[0];
7939
+ return {
7940
+ applicable: candidates.length > 0,
7941
+ eligible: false,
7942
+ compatibility: null,
7943
+ blockers: [...new Set([
7944
+ ...candidates.flatMap((assessment) => assessment.blockers),
7945
+ ...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
7946
+ ])],
7947
+ };
6764
7948
  }
6765
7949
  inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
6766
7950
  const now = this.now().getTime();
@@ -7464,6 +8648,83 @@ function taskHasStarted(task) {
7464
8648
  'merged',
7465
8649
  ].includes(task.status);
7466
8650
  }
8651
+ function resolvePreExecutionReplanAudits(store, task, marker) {
8652
+ const audits = readPlanRiskAuditEvents(store, task);
8653
+ const sourceAudit = audits.find((audit) => audit.eventId === marker.sourcePlanRiskAuditEventId
8654
+ && audit.eventHash === marker.sourcePlanRiskAuditEventHash) ?? null;
8655
+ const obstructionAudit = audits.find((audit) => audit.eventId === marker.obstructionPlanRiskAuditEventId
8656
+ && audit.eventHash === marker.obstructionPlanRiskAuditEventHash) ?? null;
8657
+ const sourceAuthorization = task.authorizations.find((authorization) => authorization.kind === 'execution'
8658
+ && authorization.decision === 'superseded'
8659
+ && hashExecutionAuthorization({ ...authorization, decision: 'approved' }) === marker.sourceExecutionAuthorizationHash);
8660
+ if (!sourceAudit || !obstructionAudit || audits.at(-1)?.eventHash !== obstructionAudit.eventHash
8661
+ || !['approved', 'approved-with-rationale'].includes(sourceAudit.decision)
8662
+ || obstructionAudit.decision !== 'stop-escalate'
8663
+ || sourceAudit.planHash !== task.planHash || obstructionAudit.planHash !== task.planHash
8664
+ || !sourceAuthorization) {
8665
+ throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan provenance chain is incomplete or inconsistent.', {
8666
+ taskId: task.id,
8667
+ eventId: marker.eventId,
8668
+ });
8669
+ }
8670
+ return { sourceAudit, obstructionAudit };
8671
+ }
8672
+ function assertPlanRiskSafetyPreserved(source, candidate, label) {
8673
+ const candidateByStep = new Map(candidate.stepClassifications.map((classification) => [classification.stepId, classification]));
8674
+ const missing = [];
8675
+ for (const sourceClassification of source.stepClassifications) {
8676
+ const candidateClassification = candidateByStep.get(sourceClassification.stepId);
8677
+ if (!candidateClassification) {
8678
+ missing.push({ stepId: sourceClassification.stepId, field: 'classification', values: [] });
8679
+ continue;
8680
+ }
8681
+ const preserveStrings = (field, sourceValues, candidateValues) => {
8682
+ const absent = sourceValues.filter((value) => !candidateValues.includes(value));
8683
+ if (absent.length > 0)
8684
+ missing.push({ stepId: sourceClassification.stepId, field, values: absent });
8685
+ };
8686
+ preserveStrings('categories', sourceClassification.categories, candidateClassification.categories);
8687
+ preserveStrings('failureModes', sourceClassification.failureModes, candidateClassification.failureModes);
8688
+ preserveStrings('requiredEvidence', sourceClassification.requiredEvidence, candidateClassification.requiredEvidence);
8689
+ if (sourceClassification.reviewRequired && !candidateClassification.reviewRequired) {
8690
+ missing.push({ stepId: sourceClassification.stepId, field: 'reviewRequired', values: [true] });
8691
+ }
8692
+ const proofBinding = (proof) => canonicalJsonStringify({ failureMode: proof.failureMode, evidence: proof.evidence });
8693
+ const candidateProofs = new Set((candidateClassification.proofObligations ?? []).map(proofBinding));
8694
+ const absentProofs = (sourceClassification.proofObligations ?? [])
8695
+ .filter((proof) => !candidateProofs.has(proofBinding(proof)));
8696
+ if (absentProofs.length > 0) {
8697
+ missing.push({ stepId: sourceClassification.stepId, field: 'proofObligations', values: absentProofs });
8698
+ }
8699
+ }
8700
+ if (missing.length > 0) {
8701
+ throw new WorkflowError('TRANSITION_BLOCKED', `${label} cannot suppress existing guarded risk or proof authority.`, {
8702
+ missing,
8703
+ });
8704
+ }
8705
+ const sourceProofText = new Map(source.stepClassifications.flatMap((classification) => (classification.proofObligations ?? []).map((proof) => [
8706
+ canonicalJsonStringify({ stepId: classification.stepId, failureMode: proof.failureMode, evidence: proof.evidence }),
8707
+ canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult }),
8708
+ ])));
8709
+ const revisedProofText = candidate.stepClassifications.some((classification) => (classification.proofObligations ?? []).some((proof) => {
8710
+ const binding = canonicalJsonStringify({
8711
+ stepId: classification.stepId,
8712
+ failureMode: proof.failureMode,
8713
+ evidence: proof.evidence,
8714
+ });
8715
+ const previous = sourceProofText.get(binding);
8716
+ return previous !== undefined
8717
+ && previous !== canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult });
8718
+ }));
8719
+ if (revisedProofText && (candidate.decision !== 'approved-with-rationale'
8720
+ || candidate.summary.trim() === source.summary.trim())) {
8721
+ throw new WorkflowError('TRANSITION_BLOCKED', `${label} may revise proof prose only with approved-with-rationale and a new audit summary.`, {
8722
+ sourceSummary: source.summary,
8723
+ candidateSummary: candidate.summary,
8724
+ candidateDecision: candidate.decision,
8725
+ });
8726
+ }
8727
+ }
7467
8728
  function taskCanRebindKnowledge(task) {
7468
8729
  return [
7469
8730
  'awaiting_execution_authorization',