codex-workflow-v2 2.0.0-beta.10 → 2.0.0-beta.12

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 (78) hide show
  1. package/README.md +47 -13
  2. package/dist/src/alpha6/handoff.d.ts +4 -3
  3. package/dist/src/alpha6/handoff.js +58 -3
  4. package/dist/src/alpha6/handoff.js.map +1 -1
  5. package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
  6. package/dist/src/alpha6/mechanical-feasibility.js +303 -0
  7. package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
  8. package/dist/src/alpha6/milestone.d.ts +25 -2
  9. package/dist/src/alpha6/milestone.js +359 -32
  10. package/dist/src/alpha6/milestone.js.map +1 -1
  11. package/dist/src/alpha6/plan-integrity.d.ts +14 -0
  12. package/dist/src/alpha6/plan-integrity.js +127 -0
  13. package/dist/src/alpha6/plan-integrity.js.map +1 -0
  14. package/dist/src/alpha6/remediation.d.ts +3 -2
  15. package/dist/src/alpha6/remediation.js +168 -55
  16. package/dist/src/alpha6/remediation.js.map +1 -1
  17. package/dist/src/alpha6/review.d.ts +5 -0
  18. package/dist/src/alpha6/review.js +110 -2
  19. package/dist/src/alpha6/review.js.map +1 -1
  20. package/dist/src/cli.js +14 -2
  21. package/dist/src/cli.js.map +1 -1
  22. package/dist/src/contracts.d.ts +82 -1
  23. package/dist/src/git.d.ts +1 -1
  24. package/dist/src/git.js +8 -1
  25. package/dist/src/git.js.map +1 -1
  26. package/dist/src/index.d.ts +1 -0
  27. package/dist/src/index.js +1 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/lifecycle/corrective-replan.js +8 -3
  30. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  31. package/dist/src/reviewer.d.ts +26 -0
  32. package/dist/src/reviewer.js +54 -1
  33. package/dist/src/reviewer.js.map +1 -1
  34. package/dist/src/version.d.ts +1 -1
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow.d.ts +5 -0
  37. package/dist/src/workflow.js +449 -148
  38. package/dist/src/workflow.js.map +1 -1
  39. package/docs/autonomy-guardrails.md +42 -33
  40. package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
  41. package/docs/decisions.md +5 -4
  42. package/docs/delegated-approval.md +5 -4
  43. package/docs/development-flow.md +38 -12
  44. package/docs/lifecycle/state-machine-stabilization.md +2 -2
  45. package/docs/pdf/README.md +24 -0
  46. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  47. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  48. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  49. package/docs/pdf/requirements.txt +1 -0
  50. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +235 -0
  51. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
  52. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +414 -0
  53. package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
  54. package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
  55. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
  56. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
  57. package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
  58. package/docs/problem-briefs/README.md +120 -0
  59. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
  60. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
  61. package/docs/release.md +17 -6
  62. package/docs/split-required-recovery.md +19 -26
  63. package/docs/validation-report.md +83 -70
  64. package/package.json +5 -1
  65. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
  66. package/plugins/codex-workflow-gateway/references/protocol.md +31 -9
  67. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +23 -6
  68. package/references/state-machine.md +18 -13
  69. package/references/validation-and-review.md +11 -4
  70. package/schemas/authorization-event.schema.json +64 -1
  71. package/schemas/corrective-decision-event.schema.json +48 -3
  72. package/schemas/milestone-scope-change-event.schema.json +6 -1
  73. package/schemas/milestone.schema.json +6 -1
  74. package/schemas/review-result.schema.json +17 -1
  75. package/schemas/step-review-event.schema.json +15 -0
  76. package/schemas/task-handoff-event.schema.json +18 -2
  77. package/scripts/generate-pdf-docs.py +513 -0
  78. package/scripts/run-pdf-docs.mjs +62 -0
@@ -13,10 +13,12 @@ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.j
13
13
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
14
14
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
15
15
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
16
- import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
17
- import { assessMilestoneInitialAssembly, applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
16
+ import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
17
+ import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
18
+ import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
19
+ import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
18
20
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
19
- import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
21
+ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
20
22
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
21
23
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
22
24
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
@@ -394,6 +396,32 @@ export class WorkflowService {
394
396
  disposition: membership?.disposition ?? null,
395
397
  });
396
398
  }
399
+ const incidentDependencies = milestone.memberships.flatMap((candidate) => {
400
+ if (!Array.isArray(candidate.dependsOnTaskIds))
401
+ return [];
402
+ return [
403
+ ...(candidate.taskId === oldTask.id
404
+ ? candidate.dependsOnTaskIds.map((predecessorTaskId) => ({
405
+ predecessorTaskId,
406
+ consumerTaskId: candidate.taskId,
407
+ }))
408
+ : []),
409
+ ...(candidate.dependsOnTaskIds.includes(oldTask.id)
410
+ ? [{ predecessorTaskId: oldTask.id, consumerTaskId: candidate.taskId }]
411
+ : []),
412
+ ];
413
+ });
414
+ if (structuralReplacementIsDisabled()) {
415
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Structural replacement is disabled until a topology-preserving replacement transaction is implemented.', {
416
+ diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
417
+ milestoneId: milestone.id,
418
+ oldTaskId: oldTask.id,
419
+ incidentDependencies,
420
+ requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
421
+ });
422
+ }
423
+ /* P04-B retains the existing one-for-one transaction below as implementation evidence,
424
+ but this route must remain unreachable until dependency rewiring is specified. */
397
425
  const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
398
426
  if (!riskAudit)
399
427
  throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
@@ -503,9 +531,19 @@ export class WorkflowService {
503
531
  checks: milestone.checks,
504
532
  memberships: [
505
533
  ...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
506
- ? { ...candidate, disposition: 'cancelled', reason: replacementReason }
507
- : candidate),
508
- { taskId: replacementTaskId, disposition: 'required', reason: replacementReason },
534
+ ? {
535
+ ...candidate,
536
+ disposition: 'cancelled',
537
+ reason: replacementReason,
538
+ dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])],
539
+ }
540
+ : { ...candidate, dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])] }),
541
+ {
542
+ taskId: replacementTaskId,
543
+ disposition: 'required',
544
+ reason: replacementReason,
545
+ dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])],
546
+ },
509
547
  ],
510
548
  };
511
549
  if (autonomyContractEventHash) {
@@ -714,38 +752,15 @@ export class WorkflowService {
714
752
  throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
715
753
  }
716
754
  }
717
- const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
718
- ? { ...authorization, decision: 'superseded' }
719
- : authorization);
720
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
721
- const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify({
722
- outcome: normalized.outcome,
723
- successSignal: normalized.successSignal,
724
- acceptance: normalized.acceptance,
725
- memberships: normalized.memberships,
726
- checks: normalized.checks,
727
- }, null, 2));
728
- const candidate = {
729
- ...milestone,
730
- status: 'awaiting_execution_authorization',
731
- outcome: normalized.outcome,
732
- successSignal: normalized.successSignal,
733
- acceptance: normalized.acceptance,
734
- taskIds,
735
- memberships: normalized.memberships,
736
- checks: normalized.checks,
737
- membershipRevision: milestone.membershipRevision + 1,
738
- authorizations,
739
- planHash: stagedPlan.hash,
740
- resultHash: null,
741
- evidenceHash: null,
742
- acceptedHead: null,
743
- cancellationReason: null,
744
- };
745
- assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
746
- const saved = this.store.writeMilestone(candidate, expectedRevision);
747
- this.store.publishArtifact(stagedPlan);
748
- return saved;
755
+ return applyInitialMilestonePlanTransaction({
756
+ store: this.store,
757
+ milestone,
758
+ expectedRevision,
759
+ normalizedPlan: normalized,
760
+ now: this.now(),
761
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
762
+ listTasks: () => this.store.listTasks(identity.projectId),
763
+ });
749
764
  }
750
765
  prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
751
766
  const { identity } = this.#mutationContext(repository);
@@ -854,6 +869,7 @@ export class WorkflowService {
854
869
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
855
870
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
856
871
  }
872
+ assertMilestoneDependenciesDeclared(milestone);
857
873
  this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
858
874
  const root = this.store.milestoneRoot(identity.projectId, milestoneId);
859
875
  const planHash = this.store.hashArtifact(root, 'plan.json');
@@ -1089,9 +1105,6 @@ export class WorkflowService {
1089
1105
  });
1090
1106
  }
1091
1107
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
1092
- if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
1093
- validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
1094
- }
1095
1108
  const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
1096
1109
  ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
1097
1110
  : null;
@@ -1129,6 +1142,23 @@ export class WorkflowService {
1129
1142
  throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
1130
1143
  }
1131
1144
  }
1145
+ if (task.planHash && task.status === 'needs_fix') {
1146
+ if (task.planObjective !== null && plan.objective !== task.planObjective) {
1147
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation may change the implementation Plan but not the Task objective.', {
1148
+ taskId: task.id,
1149
+ currentObjective: task.planObjective,
1150
+ requestedObjective: plan.objective,
1151
+ requiredAction: 'Obtain an explicit Task scope decision instead of changing the objective through remediation.',
1152
+ });
1153
+ }
1154
+ if (JSON.stringify(plan.requirements) !== JSON.stringify(task.requirements)
1155
+ || JSON.stringify(plan.acceptance) !== JSON.stringify(task.acceptance)) {
1156
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation cannot change Task requirements or acceptance.', {
1157
+ taskId: task.id,
1158
+ requiredAction: 'Obtain an explicit Task scope decision; replan may change only implementation Steps.',
1159
+ });
1160
+ }
1161
+ }
1132
1162
  const root = this.store.taskRoot(identity.projectId, taskId);
1133
1163
  const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
1134
1164
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
@@ -1397,7 +1427,24 @@ export class WorkflowService {
1397
1427
  const root = this.store.taskRoot(identity.projectId, taskId);
1398
1428
  const briefHash = this.store.hashArtifact(root, 'brief.md');
1399
1429
  const planHash = this.store.hashArtifact(root, 'plan.md');
1430
+ if (task.briefHash !== briefHash || task.planHash !== planHash) {
1431
+ throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
1432
+ taskId: task.id,
1433
+ stateBriefHash: task.briefHash,
1434
+ artifactBriefHash: briefHash,
1435
+ statePlanHash: task.planHash,
1436
+ artifactPlanHash: planHash,
1437
+ });
1438
+ }
1400
1439
  readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
1440
+ const mechanicalFeasibility = evaluateMechanicalFeasibility(identity.repositoryRoot, task, briefHash, planHash);
1441
+ if (mechanicalFeasibility.decision === 'blocked') {
1442
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task Plan contains a proven mechanical contradiction.', {
1443
+ diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
1444
+ mechanicalFeasibility,
1445
+ requiredAction: 'task plan-set',
1446
+ });
1447
+ }
1401
1448
  const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
1402
1449
  const event = {
1403
1450
  kind: 'execution',
@@ -1407,11 +1454,12 @@ export class WorkflowService {
1407
1454
  briefHash,
1408
1455
  planHash,
1409
1456
  resultHash: null,
1410
- evidenceHash: null,
1411
- headCommit: null,
1457
+ evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
1458
+ headCommit: mechanicalFeasibility.evaluatedHead,
1412
1459
  reason,
1413
1460
  authorizationMode: delegation ? 'delegated' : 'human',
1414
1461
  ...(delegation ? { delegation } : {}),
1462
+ mechanicalFeasibility,
1415
1463
  };
1416
1464
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
1417
1465
  }
@@ -1443,6 +1491,9 @@ export class WorkflowService {
1443
1491
  if (grantId)
1444
1492
  this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
1445
1493
  const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
1494
+ const dependencyBinding = milestone
1495
+ ? this.currentMilestoneDependencyBinding(milestone, task.id)
1496
+ : null;
1446
1497
  const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
1447
1498
  const milestoneDisplayNumber = milestone
1448
1499
  ? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
@@ -1488,6 +1539,7 @@ export class WorkflowService {
1488
1539
  writerToken: lease.token,
1489
1540
  milestoneDisplayNumber,
1490
1541
  taskDisplayNumber,
1542
+ dependencyBinding,
1491
1543
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1492
1544
  grantId,
1493
1545
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
@@ -1510,7 +1562,11 @@ export class WorkflowService {
1510
1562
  if (!normalizedActor) {
1511
1563
  throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
1512
1564
  }
1513
- const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now());
1565
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1566
+ const dependencyBinding = task.milestoneId
1567
+ ? this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id)
1568
+ : null;
1569
+ const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now(), dependencyBinding);
1514
1570
  if (pendingHandoff.latestEvent.grantId) {
1515
1571
  this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
1516
1572
  }
@@ -1543,7 +1599,7 @@ export class WorkflowService {
1543
1599
  lease = context.locks.acquire(taskId, normalizedActor);
1544
1600
  }
1545
1601
  try {
1546
- const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1602
+ const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now(), dependencyBinding);
1547
1603
  return {
1548
1604
  task,
1549
1605
  event,
@@ -1567,6 +1623,22 @@ export class WorkflowService {
1567
1623
  if (!normalizedActor)
1568
1624
  throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
1569
1625
  const posture = readTaskC1Posture(this.store, task);
1626
+ if (posture.state === 'claimed' && task.milestoneId) {
1627
+ const dependencyBinding = this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id);
1628
+ const claimedBinding = posture.latestEvent.dependencyBinding ?? null;
1629
+ if (claimedBinding === null
1630
+ || claimedBinding.milestoneRevision !== dependencyBinding.milestoneRevision
1631
+ || claimedBinding.membershipRevision !== dependencyBinding.membershipRevision
1632
+ || claimedBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
1633
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Writer credential replacement requires a current Milestone dependency binding.', {
1634
+ diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
1635
+ taskId: task.id,
1636
+ milestoneId: task.milestoneId,
1637
+ claimDependencyBinding: claimedBinding,
1638
+ currentDependencyBinding: dependencyBinding,
1639
+ });
1640
+ }
1641
+ }
1570
1642
  const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1571
1643
  const action = String(navigation.action);
1572
1644
  const currentLease = context.locks.inspect(taskId);
@@ -1615,17 +1687,10 @@ export class WorkflowService {
1615
1687
  this.requireExecutionAdoptionPosture(identity, taskId);
1616
1688
  if (task.status !== 'ready')
1617
1689
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
1618
- this.assertExecutionAuthorizationFresh(task);
1690
+ const executionAuthorization = this.assertExecutionAuthorizationFresh(task, identity.repositoryRoot);
1619
1691
  if (task.milestoneId) {
1620
1692
  const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
1621
- const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
1622
- if (milestone.status !== 'active' || membership?.disposition !== 'required') {
1623
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
1624
- milestoneId: milestone.id,
1625
- milestoneStatus: milestone.status,
1626
- disposition: membership?.disposition ?? null,
1627
- });
1628
- }
1693
+ const dependencyBinding = this.currentMilestoneDependencyBinding(milestone, task.id);
1629
1694
  const handoffPosture = readTaskC1Posture(this.store, task);
1630
1695
  if (handoffPosture.state !== 'claimed') {
1631
1696
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
@@ -1635,6 +1700,20 @@ export class WorkflowService {
1635
1700
  requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
1636
1701
  });
1637
1702
  }
1703
+ const claimBinding = handoffPosture.latestEvent.dependencyBinding ?? null;
1704
+ if (claimBinding === null
1705
+ || claimBinding.milestoneRevision !== dependencyBinding.milestoneRevision
1706
+ || claimBinding.membershipRevision !== dependencyBinding.membershipRevision
1707
+ || claimBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
1708
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a current dependency-bound C1 claim.', {
1709
+ diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
1710
+ taskId: task.id,
1711
+ milestoneId: milestone.id,
1712
+ claimDependencyBinding: handoffPosture.latestEvent.dependencyBinding ?? null,
1713
+ currentDependencyBinding: dependencyBinding,
1714
+ requiredAction: 'Prepare and claim a new C1 handoff from the current Milestone Plan.',
1715
+ });
1716
+ }
1638
1717
  }
1639
1718
  if (!isClean(identity.repositoryRoot)) {
1640
1719
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
@@ -1644,12 +1723,22 @@ export class WorkflowService {
1644
1723
  const branch = `codex/task-${task.id.slice(5, 15).toLowerCase()}-${task.slug}`;
1645
1724
  let baseCommit;
1646
1725
  let taskBranch;
1726
+ const authorizedBaseCommit = executionAuthorization.mechanicalFeasibility?.sourceKind === 'pre-start-base'
1727
+ ? executionAuthorization.mechanicalFeasibility.evaluatedHead
1728
+ : null;
1647
1729
  if (workspaceOwner === 'local') {
1648
- baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch);
1730
+ baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch, authorizedBaseCommit);
1649
1731
  taskBranch = branch;
1650
1732
  }
1651
1733
  else {
1652
1734
  baseCommit = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
1735
+ if (authorizedBaseCommit !== null && baseCommit !== authorizedBaseCommit) {
1736
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
1737
+ baseBranch: task.baseBranch,
1738
+ authorizedBaseCommit,
1739
+ currentBaseCommit: baseCommit,
1740
+ });
1741
+ }
1653
1742
  taskBranch = currentBranch(identity.repositoryRoot);
1654
1743
  if (!taskBranch || taskBranch === task.baseBranch) {
1655
1744
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `Externally owned execution requires a dedicated branch different from ${task.baseBranch}.`);
@@ -1668,7 +1757,7 @@ export class WorkflowService {
1668
1757
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
1669
1758
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
1670
1759
  }
1671
- this.assertExecutionAuthorizationFresh(task);
1760
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1672
1761
  const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
1673
1762
  .filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
1674
1763
  if (currentPlanBlockingDecisions.length > 0) {
@@ -2304,7 +2393,7 @@ export class WorkflowService {
2304
2393
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
2305
2394
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
2306
2395
  }
2307
- this.assertExecutionAuthorizationFresh(task);
2396
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2308
2397
  this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2309
2398
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
2310
2399
  }
@@ -2315,6 +2404,47 @@ export class WorkflowService {
2315
2404
  this.requireExecutionAdoptionPosture(context.identity, taskId);
2316
2405
  return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
2317
2406
  }
2407
+ recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
2408
+ const context = this.#mutationContext(repository);
2409
+ const task = this.store.readTask(context.identity.projectId, taskId);
2410
+ assertExpectedRevision(task, expectedRevision);
2411
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
2412
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2413
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2414
+ assertTaskBranch(context.identity.repositoryRoot, task);
2415
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2416
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2417
+ const handoff = readTaskC1Posture(this.store, task);
2418
+ const requiredActor = handoff.state === 'claimed'
2419
+ ? handoff.claimant
2420
+ : defaultPlanIntegrityRecoveryActor(task.id);
2421
+ if (actor.trim() !== requiredActor) {
2422
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery actor does not match the fresh Workflow route.', {
2423
+ taskId,
2424
+ stepId,
2425
+ requiredActor,
2426
+ actualActor: actor.trim(),
2427
+ });
2428
+ }
2429
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2430
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2431
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the failed Step to be guarded by the current Plan Risk Audit.', {
2432
+ taskId,
2433
+ stepId,
2434
+ });
2435
+ }
2436
+ const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
2437
+ if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
2438
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
2439
+ taskId,
2440
+ stepId,
2441
+ applicable: assessment.applicable,
2442
+ blockers: assessment.blockers,
2443
+ });
2444
+ }
2445
+ const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
2446
+ return { assessment, event };
2447
+ }
2318
2448
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
2319
2449
  const { identity } = this.#mutationContext(repository);
2320
2450
  this.readCurrentAdoptionPostureStrict(identity.projectId);
@@ -2693,6 +2823,13 @@ export class WorkflowService {
2693
2823
  if (!task)
2694
2824
  throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
2695
2825
  const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
2826
+ const dependency = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
2827
+ const candidate = tasksById.get(candidateTaskId);
2828
+ if (!candidate)
2829
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
2830
+ return candidate;
2831
+ });
2832
+ const executionEligible = milestone.status === 'active' && !['merged', 'cancelled'].includes(task.status);
2696
2833
  return {
2697
2834
  ordinal: `T${String(index + 1).padStart(2, '0')}`,
2698
2835
  taskId: task.id,
@@ -2701,6 +2838,15 @@ export class WorkflowService {
2701
2838
  revision: task.revision,
2702
2839
  disposition: membership.disposition,
2703
2840
  membershipReason: membership.reason,
2841
+ dependencyState: dependency.dependencyState,
2842
+ dependsOnTaskIds: dependency.dependsOnTaskIds,
2843
+ unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
2844
+ runnable: executionEligible && dependency.runnable,
2845
+ dependencyBlockReason: !executionEligible
2846
+ ? milestone.status !== 'active'
2847
+ ? `Milestone is ${milestone.status}, not active.`
2848
+ : `Task is terminal (${task.status}).`
2849
+ : dependency.blockReason,
2704
2850
  replacementForTaskId: task.replacementForTaskId ?? null,
2705
2851
  replacedByTaskId: task.replacedByTaskId ?? null,
2706
2852
  stepsCompleted,
@@ -3030,21 +3176,34 @@ export class WorkflowService {
3030
3176
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
3031
3177
  const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
3032
3178
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
3179
+ const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
3033
3180
  const requestedTask = taskId === null
3034
3181
  ? null
3035
3182
  : status.tasks.find((task) => task.id === taskId) ?? null;
3036
3183
  if (taskId !== null && !requestedTask) {
3037
3184
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3038
3185
  }
3039
- if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
3186
+ if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById, tasksById)) {
3187
+ const requestedMilestone = requestedTask.milestoneId
3188
+ ? milestonesById.get(requestedTask.milestoneId) ?? null
3189
+ : null;
3190
+ const dependency = requestedMilestone
3191
+ ? assessMilestoneTaskDependencies(requestedMilestone, requestedTask.id, (candidateTaskId) => {
3192
+ const candidate = tasksById.get(candidateTaskId);
3193
+ if (!candidate)
3194
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
3195
+ return candidate;
3196
+ })
3197
+ : null;
3040
3198
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
3041
3199
  taskId: requestedTask.id,
3042
3200
  taskStatus: requestedTask.status,
3043
3201
  milestoneId: requestedTask.milestoneId,
3202
+ ...(dependency ? { dependency } : {}),
3044
3203
  });
3045
3204
  }
3046
3205
  const activeTask = requestedTask
3047
- ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
3206
+ ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
3048
3207
  if (activeTask) {
3049
3208
  const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3050
3209
  if (dependencyRecovery.eligible) {
@@ -3109,7 +3268,7 @@ export class WorkflowService {
3109
3268
  const taskNext = activeTask.status === 'planning'
3110
3269
  ? null
3111
3270
  : planRiskAuditRecovery
3112
- ?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
3271
+ ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
3113
3272
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
3114
3273
  // strict review are executable while context refresh correctly remains forbidden.
3115
3274
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -3118,6 +3277,8 @@ export class WorkflowService {
3118
3277
  if (taskNext?.action === 'task step-complete'
3119
3278
  || taskNext?.action === 'task step-review'
3120
3279
  || taskNext?.action === 'task remediation-mode-recover'
3280
+ || taskNext?.action === 'task plan-integrity-recover'
3281
+ || taskNext?.blockedAction === 'task plan-integrity-recover'
3121
3282
  || hardStop
3122
3283
  || terminalCorrectiveDecision === 'split-required'
3123
3284
  || terminalCorrectiveDecision === 'stop-escalate') {
@@ -3216,7 +3377,7 @@ export class WorkflowService {
3216
3377
  knowledgeMapHash,
3217
3378
  }));
3218
3379
  }
3219
- return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
3380
+ return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)));
3220
3381
  }
3221
3382
  let repositoryKnowledgeMap;
3222
3383
  try {
@@ -3351,7 +3512,21 @@ export class WorkflowService {
3351
3512
  .filter((membership) => membership.disposition === 'required')
3352
3513
  .map((membership) => {
3353
3514
  const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
3354
- return { taskId: membership.taskId, status: task?.status ?? 'missing' };
3515
+ const dependency = assessMilestoneTaskDependencies(activeMilestone, membership.taskId, (candidateTaskId) => {
3516
+ const candidate = status.tasks.find((taskCandidate) => taskCandidate.id === candidateTaskId);
3517
+ if (!candidate)
3518
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
3519
+ return candidate;
3520
+ });
3521
+ return {
3522
+ taskId: membership.taskId,
3523
+ status: task?.status ?? 'missing',
3524
+ dependencyState: dependency.dependencyState,
3525
+ dependsOnTaskIds: dependency.dependsOnTaskIds,
3526
+ unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
3527
+ runnable: Boolean(task && !['merged', 'cancelled'].includes(task.status) && dependency.runnable),
3528
+ dependencyBlockReason: dependency.blockReason,
3529
+ };
3355
3530
  }),
3356
3531
  },
3357
3532
  }
@@ -3474,7 +3649,21 @@ export class WorkflowService {
3474
3649
  }
3475
3650
  }
3476
3651
  }
3477
- nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
3652
+ currentMilestoneDependencyBinding(milestone, taskId) {
3653
+ assertMilestoneTaskRunnable(milestone, taskId, (candidateTaskId) => this.store.readTask(milestone.projectId, candidateTaskId));
3654
+ if (!milestone.planHash) {
3655
+ throw new WorkflowError('STATE_CORRUPT', `Active Milestone ${milestone.id} has no Plan hash.`, {
3656
+ milestoneId: milestone.id,
3657
+ taskId,
3658
+ });
3659
+ }
3660
+ return {
3661
+ milestoneRevision: milestone.revision,
3662
+ membershipRevision: milestone.membershipRevision,
3663
+ milestonePlanHash: milestone.planHash,
3664
+ };
3665
+ }
3666
+ nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
3478
3667
  const transition = task.status === 'awaiting_execution_authorization'
3479
3668
  ? 'task.execution_authorize'
3480
3669
  : task.status === 'awaiting_final_acceptance'
@@ -3483,7 +3672,6 @@ export class WorkflowService {
3483
3672
  const delegatedApprovalOptions = transition
3484
3673
  ? this.delegatedApprovalOptions(projectId, transition, task)
3485
3674
  : [];
3486
- const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
3487
3675
  let remediationModeRecovery = null;
3488
3676
  let guardedCorrectivePosture = null;
3489
3677
  if (planRiskAudit) {
@@ -3510,7 +3698,62 @@ export class WorkflowService {
3510
3698
  }
3511
3699
  }
3512
3700
  const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
3701
+ let mechanicalFeasibilityOverlay = {};
3702
+ if (task.status === 'awaiting_execution_authorization' && task.planHash) {
3703
+ try {
3704
+ const taskRoot = this.store.taskRoot(projectId, task.id);
3705
+ const briefHash = this.store.hashArtifact(taskRoot, 'brief.md');
3706
+ const planHash = this.store.hashArtifact(taskRoot, 'plan.md');
3707
+ if (task.briefHash !== briefHash || task.planHash !== planHash) {
3708
+ throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
3709
+ taskId: task.id,
3710
+ stateBriefHash: task.briefHash,
3711
+ artifactBriefHash: briefHash,
3712
+ statePlanHash: task.planHash,
3713
+ artifactPlanHash: planHash,
3714
+ });
3715
+ }
3716
+ const mechanicalFeasibility = evaluateMechanicalFeasibility(repositoryRoot, task, briefHash, planHash);
3717
+ mechanicalFeasibilityOverlay = mechanicalFeasibility.decision === 'blocked'
3718
+ ? {
3719
+ action: 'task plan-set',
3720
+ blockedAction: 'task authorize',
3721
+ mechanicalFeasibility,
3722
+ mechanicalFeasibilityGate: {
3723
+ state: 'blocked',
3724
+ diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
3725
+ requiredAction: 'Replace the current Plan through task plan-set, then request fresh execution authorization.',
3726
+ },
3727
+ }
3728
+ : {
3729
+ mechanicalFeasibility,
3730
+ mechanicalFeasibilityGate: {
3731
+ state: 'clear',
3732
+ meaning: 'No supported analyzer proved infeasibility; semantic sufficiency is not asserted.',
3733
+ },
3734
+ };
3735
+ }
3736
+ catch (error) {
3737
+ mechanicalFeasibilityOverlay = {
3738
+ action: 'doctor',
3739
+ blockedAction: 'task authorize',
3740
+ mechanicalFeasibilityGate: {
3741
+ state: 'unavailable',
3742
+ diagnosticCode: error instanceof WorkflowError ? error.code : 'UNKNOWN_ERROR',
3743
+ reason: error instanceof Error ? error.message : String(error),
3744
+ requiredAction: 'Restore the exact Plan/source authority before execution authorization.',
3745
+ },
3746
+ };
3747
+ }
3748
+ }
3513
3749
  const handoffPosture = readTaskC1Posture(this.store, task);
3750
+ const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
3751
+ const failedStepReviewRouting = failedStep
3752
+ ? latestFailedStepReviewRouting(this.store, task, failedStep.id)
3753
+ : null;
3754
+ const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
3755
+ ? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
3756
+ : null;
3514
3757
  const strictStepReviewStep = planRiskAudit
3515
3758
  ? task.steps.find((step) => {
3516
3759
  if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
@@ -3623,6 +3866,39 @@ export class WorkflowService {
3623
3866
  };
3624
3867
  };
3625
3868
  let guardedCorrectiveOverlay = {};
3869
+ if (planIntegrityAssessment?.applicable) {
3870
+ const requiredActor = handoffPosture.state === 'claimed'
3871
+ ? handoffPosture.claimant
3872
+ : defaultPlanIntegrityRecoveryActor(task.id);
3873
+ guardedCorrectiveOverlay = planIntegrityAssessment.eligible
3874
+ ? {
3875
+ action: 'task plan-integrity-recover',
3876
+ blockedAction: 'task run',
3877
+ stepId: planIntegrityAssessment.stepId,
3878
+ planIntegrityRecovery: {
3879
+ state: 'eligible',
3880
+ reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
3881
+ expectedRevision: task.revision,
3882
+ requiredActor,
3883
+ evidence: planIntegrityAssessment.evidence,
3884
+ boundedEffect: 'record-replan-required-decision',
3885
+ preservesWorktree: true,
3886
+ },
3887
+ }
3888
+ : {
3889
+ action: 'doctor',
3890
+ blockedAction: 'task plan-integrity-recover',
3891
+ stepId: planIntegrityAssessment.stepId,
3892
+ planIntegrityRecovery: {
3893
+ state: 'blocked',
3894
+ reasonCode: 'PLAN_INTEGRITY_RECOVERY_BLOCKED',
3895
+ expectedRevision: task.revision,
3896
+ requiredActor,
3897
+ blockers: planIntegrityAssessment.blockers,
3898
+ evidence: planIntegrityAssessment.evidence,
3899
+ },
3900
+ };
3901
+ }
3626
3902
  if (remediationModeRecovery) {
3627
3903
  guardedCorrectiveOverlay = {
3628
3904
  action: 'task remediation-mode-recover',
@@ -3704,21 +3980,6 @@ export class WorkflowService {
3704
3980
  },
3705
3981
  };
3706
3982
  break;
3707
- case 'hard-stop':
3708
- guardedCorrectiveOverlay = {
3709
- action: 'task corrective-decision',
3710
- stepId: guardedCorrectivePosture.stepId,
3711
- correctiveDecisionGate: {
3712
- attemptCount: guardedCorrectivePosture.attemptCount,
3713
- recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
3714
- auditorDerived: true,
3715
- auditorMustDifferFromLatestStrictReviewer: true,
3716
- allowedDecisions: ['split-required', 'stop-escalate'],
3717
- hardStop: true,
3718
- humanApprovalRequired: false,
3719
- },
3720
- };
3721
- break;
3722
3983
  case 'replan-required':
3723
3984
  guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
3724
3985
  break;
@@ -3726,13 +3987,29 @@ export class WorkflowService {
3726
3987
  {
3727
3988
  const replacementDiscoveries = this.store.listDiscoveries(projectId)
3728
3989
  .filter((discovery) => discovery.status === 'ready_to_materialize');
3729
- const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
3730
3990
  const replacementMilestone = task.milestoneId
3731
3991
  ? this.store.readMilestone(projectId, task.milestoneId)
3732
3992
  : null;
3993
+ const incidentDependencies = replacementMilestone
3994
+ ? replacementMilestone.memberships.flatMap((membership) => {
3995
+ if (!Array.isArray(membership.dependsOnTaskIds))
3996
+ return [];
3997
+ return [
3998
+ ...(membership.taskId === task.id
3999
+ ? membership.dependsOnTaskIds.map((predecessorTaskId) => ({
4000
+ predecessorTaskId,
4001
+ consumerTaskId: membership.taskId,
4002
+ }))
4003
+ : []),
4004
+ ...(membership.dependsOnTaskIds.includes(task.id)
4005
+ ? [{ predecessorTaskId: task.id, consumerTaskId: membership.taskId }]
4006
+ : []),
4007
+ ];
4008
+ })
4009
+ : [];
3733
4010
  guardedCorrectiveOverlay = {
3734
- action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
3735
- ...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
4011
+ action: 'doctor',
4012
+ blockedAction: 'task replacement-materialize',
3736
4013
  stepId: guardedCorrectivePosture.stepId,
3737
4014
  correctiveDecisionGate: {
3738
4015
  attemptCount: guardedCorrectivePosture.attemptCount,
@@ -3746,29 +4023,17 @@ export class WorkflowService {
3746
4023
  requiredDiscoveryStatus: 'ready_to_materialize',
3747
4024
  oldMembershipDisposition: 'cancelled',
3748
4025
  replacementMembershipDisposition: 'required',
3749
- transaction: 'journal-recoverable',
4026
+ transaction: 'disabled',
4027
+ retainedImplementation: 'journal-recoverable-1-to-1',
4028
+ diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
4029
+ structuralReplacementAvailable: false,
4030
+ incidentDependencies,
3750
4031
  discoveryCandidates: replacementDiscoveries.map((discovery) => ({
3751
4032
  id: discovery.id,
3752
4033
  revision: discovery.revision,
3753
4034
  goal: discovery.goal,
3754
4035
  })),
3755
- ...(replacementDiscovery && replacementMilestone
3756
- ? {
3757
- exactTransition: {
3758
- command: 'task replacement-materialize',
3759
- taskId: task.id,
3760
- expectedTaskRevision: task.revision,
3761
- discoveryId: replacementDiscovery.id,
3762
- expectedDiscoveryRevision: replacementDiscovery.revision,
3763
- milestoneId: replacementMilestone.id,
3764
- expectedMilestoneRevision: replacementMilestone.revision,
3765
- },
3766
- }
3767
- : {
3768
- requiredAction: replacementDiscoveries.length === 0
3769
- ? 'Create and complete one thin replacement Discovery, then run next again.'
3770
- : 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
3771
- }),
4036
+ requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
3772
4037
  },
3773
4038
  };
3774
4039
  }
@@ -3841,19 +4106,21 @@ export class WorkflowService {
3841
4106
  },
3842
4107
  }
3843
4108
  : {}),
3844
- ...(task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2
4109
+ ...guardedCorrectiveOverlay,
4110
+ ...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
3845
4111
  ? {
3846
- action: 'task plan-set with independent corrective audit',
3847
- correctivePlanGate: {
3848
- failedReviewAttempts,
3849
- auditFileRequired: true,
3850
- auditorMustDifferFromFailedReviewer: true,
3851
- allowedDecision: 'continue-fix',
3852
- escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
4112
+ action: 'task plan-set',
4113
+ blockedAction: 'task run',
4114
+ reviewRemediation: {
4115
+ route: 'replan',
4116
+ source: 'strict-step-review',
4117
+ stepId: failedStep?.id ?? null,
4118
+ planConflicts: failedStepReviewRouting.planConflicts,
4119
+ boundary: 'Task objective, requirements, and acceptance remain immutable; only implementation Steps may change.',
3853
4120
  },
3854
4121
  }
3855
4122
  : {}),
3856
- ...guardedCorrectiveOverlay,
4123
+ ...mechanicalFeasibilityOverlay,
3857
4124
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3858
4125
  };
3859
4126
  if (handoffPosture.state === 'pending') {
@@ -3863,7 +4130,7 @@ export class WorkflowService {
3863
4130
  blockedAction: next.action,
3864
4131
  });
3865
4132
  }
3866
- const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
4133
+ const navigableResult = ['task plan-set']
3867
4134
  .includes(String(result.action))
3868
4135
  ? {
3869
4136
  ...result,
@@ -3967,8 +4234,9 @@ export class WorkflowService {
3967
4234
  }));
3968
4235
  }
3969
4236
  findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
4237
+ const tasksById = new Map(tasks.map((task) => [task.id, task]));
3970
4238
  for (const task of tasks) {
3971
- if (!taskIsActionableFromRepositoryNext(task, milestonesById)
4239
+ if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
3972
4240
  || !task.planHash
3973
4241
  || !taskCanRebindKnowledge(task)
3974
4242
  || this.taskHasTerminalCorrectiveDecision(task))
@@ -4007,7 +4275,7 @@ export class WorkflowService {
4007
4275
  if (policy.scope.kind === 'milestone')
4008
4276
  this.store.readMilestone(projectId, policy.scope.id);
4009
4277
  }
4010
- assertExecutionAuthorizationFresh(task) {
4278
+ assertExecutionAuthorizationFresh(task, repositoryRoot) {
4011
4279
  const root = this.store.taskRoot(task.projectId, task.id);
4012
4280
  const authorization = [...task.authorizations]
4013
4281
  .reverse()
@@ -4016,14 +4284,35 @@ export class WorkflowService {
4016
4284
  throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is missing.');
4017
4285
  const briefHash = this.store.hashArtifact(root, 'brief.md');
4018
4286
  const planHash = this.store.hashArtifact(root, 'plan.md');
4019
- if (authorization.briefHash !== briefHash || authorization.planHash !== planHash) {
4287
+ if (authorization.briefHash !== briefHash
4288
+ || authorization.planHash !== planHash
4289
+ || task.briefHash !== briefHash
4290
+ || task.planHash !== planHash) {
4020
4291
  throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is stale.', {
4021
4292
  authorizedBriefHash: authorization.briefHash,
4022
4293
  currentBriefHash: briefHash,
4023
4294
  authorizedPlanHash: authorization.planHash,
4024
4295
  currentPlanHash: planHash,
4296
+ stateBriefHash: task.briefHash,
4297
+ statePlanHash: task.planHash,
4298
+ });
4299
+ }
4300
+ const mechanicalFeasibility = authorization.mechanicalFeasibility;
4301
+ if (!mechanicalFeasibility)
4302
+ return authorization;
4303
+ const evidenceHash = hashMechanicalFeasibilityEvidence(mechanicalFeasibility);
4304
+ if (authorization.evidenceHash !== evidenceHash
4305
+ || authorization.headCommit !== mechanicalFeasibility.evaluatedHead) {
4306
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical-feasibility evidence does not match its authorization event.', {
4307
+ taskId: task.id,
4308
+ authorizedEvidenceHash: authorization.evidenceHash,
4309
+ currentEvidenceHash: evidenceHash,
4310
+ authorizedHeadCommit: authorization.headCommit,
4311
+ evaluatedHead: mechanicalFeasibility.evaluatedHead,
4025
4312
  });
4026
4313
  }
4314
+ assertMechanicalFeasibilityFresh(repositoryRoot, task, mechanicalFeasibility, briefHash, planHash);
4315
+ return authorization;
4027
4316
  }
4028
4317
  assertMilestoneAuthorizationFresh(milestone) {
4029
4318
  const authorization = [...milestone.authorizations]
@@ -4562,6 +4851,24 @@ function validateReviewInput(review) {
4562
4851
  if (review.status === 'failed' && review.findings.length === 0) {
4563
4852
  throw new WorkflowError('INVALID_ARGUMENT', 'A failed review requires at least one finding.');
4564
4853
  }
4854
+ for (const finding of review.findings) {
4855
+ const route = finding.route ?? 'fix';
4856
+ if (route !== 'fix' && route !== 'replan') {
4857
+ throw new WorkflowError('INVALID_ARGUMENT', `Review finding ${finding.id} has an invalid remediation route.`);
4858
+ }
4859
+ if (route === 'fix' && finding.planConflict !== undefined && finding.planConflict !== null) {
4860
+ throw new WorkflowError('INVALID_ARGUMENT', `Review finding ${finding.id} cannot carry a Plan conflict on the fix route.`);
4861
+ }
4862
+ if (route === 'replan') {
4863
+ const conflict = finding.planConflict;
4864
+ if (!conflict
4865
+ || !conflict.planClause?.trim()
4866
+ || !conflict.whyFixCannotFit?.trim()
4867
+ || !conflict.minimalPlanChange?.trim()) {
4868
+ throw new WorkflowError('INVALID_ARGUMENT', `Review finding ${finding.id} requires exact Plan-conflict evidence for replan.`);
4869
+ }
4870
+ }
4871
+ }
4565
4872
  }
4566
4873
  function countFailedReviewAttempts(taskRoot) {
4567
4874
  const file = path.join(taskRoot, 'findings.jsonl');
@@ -4581,34 +4888,6 @@ function countFailedReviewAttempts(taskRoot) {
4581
4888
  }
4582
4889
  return attempts.size;
4583
4890
  }
4584
- function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
4585
- if (!audit) {
4586
- throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has ${failedReviewAttempts} failed independent reviews. A separate corrective Plan audit is required before another plan-set.`, {
4587
- failedReviewAttempts,
4588
- requiredAction: 'Run a separate read-only Auditor and pass its evidence through --corrective-audit-file.',
4589
- });
4590
- }
4591
- if (!audit.auditor.trim() || !audit.summary.trim()) {
4592
- throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit requires auditor and summary.');
4593
- }
4594
- if (audit.auditor.trim() === task.review.reviewer.trim()) {
4595
- throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit must be performed by an Auditor distinct from the failed-review author.');
4596
- }
4597
- const latestFindingIds = new Set(task.review.findings.map((finding) => finding.id));
4598
- const reviewedFindingIds = new Set(audit.reviewedFindingIds);
4599
- if (reviewedFindingIds.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
4600
- throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
4601
- }
4602
- const missing = [...latestFindingIds].filter((findingId) => !reviewedFindingIds.has(findingId));
4603
- if (missing.length > 0) {
4604
- throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every latest finding.', {
4605
- missingFindingIds: missing,
4606
- });
4607
- }
4608
- if (audit.decision !== 'continue-fix') {
4609
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
4610
- }
4611
- }
4612
4891
  function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
4613
4892
  appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
4614
4893
  recordedAt: now.toISOString(),
@@ -4744,12 +5023,25 @@ function validateMilestonePlan(plan) {
4744
5023
  if (plan.acceptance.length === 0 || plan.acceptance.some((value) => !value.trim())) {
4745
5024
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone acceptance must contain substantive entries.');
4746
5025
  }
4747
- if (plan.memberships.length === 0) {
5026
+ if (!Array.isArray(plan.memberships) || plan.memberships.length === 0) {
4748
5027
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone requires at least one Task membership.');
4749
5028
  }
4750
5029
  for (const membership of plan.memberships) {
4751
- if (!membership.taskId.trim())
5030
+ if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
5031
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.');
5032
+ }
5033
+ if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
4752
5034
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires Task ID.');
5035
+ }
5036
+ if (typeof membership.reason !== 'string') {
5037
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.');
5038
+ }
5039
+ if (!Array.isArray(membership.dependsOnTaskIds)) {
5040
+ throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.');
5041
+ }
5042
+ if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
5043
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.');
5044
+ }
4753
5045
  if (membership.disposition !== 'required' && !membership.reason.trim()) {
4754
5046
  throw new WorkflowError('INVALID_ARGUMENT', `Milestone ${membership.disposition} membership requires a reason.`);
4755
5047
  }
@@ -4779,7 +5071,7 @@ function taskCanRebindKnowledge(task) {
4779
5071
  'awaiting_final_acceptance',
4780
5072
  ].includes(task.status);
4781
5073
  }
4782
- function taskIsActionableFromRepositoryNext(task, milestonesById) {
5074
+ function taskIsActionableFromRepositoryNext(task, milestonesById, tasksById) {
4783
5075
  if (task.status === 'merged' || task.status === 'cancelled')
4784
5076
  return false;
4785
5077
  if (!task.milestoneId)
@@ -4787,10 +5079,18 @@ function taskIsActionableFromRepositoryNext(task, milestonesById) {
4787
5079
  const milestone = milestonesById.get(task.milestoneId);
4788
5080
  if (!milestone)
4789
5081
  return false;
4790
- if (taskHasStarted(task))
4791
- return !['accepted', 'cancelled'].includes(milestone.status);
4792
- const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
4793
- return milestone.status === 'active' && membership?.disposition === 'required';
5082
+ if (milestone.status !== 'active')
5083
+ return false;
5084
+ const assessment = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
5085
+ const candidate = tasksById.get(candidateTaskId);
5086
+ if (!candidate)
5087
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
5088
+ return candidate;
5089
+ });
5090
+ return assessment.runnable;
5091
+ }
5092
+ function structuralReplacementIsDisabled() {
5093
+ return true;
4794
5094
  }
4795
5095
  function milestoneFinalAcceptanceGate(milestone, headCommit) {
4796
5096
  if (!milestone.resultHash || !milestone.evidenceHash) {
@@ -4920,6 +5220,7 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
4920
5220
  'task review-sealed-record',
4921
5221
  'task corrective-decision-recover',
4922
5222
  'task remediation-mode-recover',
5223
+ 'task plan-integrity-recover',
4923
5224
  'task corrective-decision',
4924
5225
  'task corrective-yield',
4925
5226
  'task handoff',