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

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 (67) hide show
  1. package/README.md +43 -9
  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 +2 -1
  15. package/dist/src/alpha6/remediation.js +168 -3
  16. package/dist/src/alpha6/remediation.js.map +1 -1
  17. package/dist/src/cli.js +13 -1
  18. package/dist/src/cli.js.map +1 -1
  19. package/dist/src/contracts.d.ts +69 -1
  20. package/dist/src/git.d.ts +1 -1
  21. package/dist/src/git.js +8 -1
  22. package/dist/src/git.js.map +1 -1
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.js +1 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/lifecycle/corrective-replan.js +8 -3
  27. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  28. package/dist/src/version.d.ts +1 -1
  29. package/dist/src/version.js +1 -1
  30. package/dist/src/workflow.d.ts +5 -0
  31. package/dist/src/workflow.js +399 -90
  32. package/dist/src/workflow.js.map +1 -1
  33. package/docs/autonomy-guardrails.md +21 -0
  34. package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
  35. package/docs/development-flow.md +31 -6
  36. package/docs/lifecycle/state-machine-stabilization.md +1 -1
  37. package/docs/pdf/README.md +24 -0
  38. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  39. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  40. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  41. package/docs/pdf/requirements.txt +1 -0
  42. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +234 -0
  43. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
  44. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +413 -0
  45. package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
  46. package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
  47. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
  48. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
  49. package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
  50. package/docs/problem-briefs/README.md +120 -0
  51. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
  52. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
  53. package/docs/release.md +15 -6
  54. package/docs/split-required-recovery.md +19 -26
  55. package/docs/validation-report.md +116 -77
  56. package/package.json +5 -1
  57. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
  58. package/plugins/codex-workflow-gateway/references/protocol.md +19 -2
  59. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +17 -3
  60. package/references/state-machine.md +9 -5
  61. package/schemas/authorization-event.schema.json +64 -1
  62. package/schemas/corrective-decision-event.schema.json +48 -3
  63. package/schemas/milestone-scope-change-event.schema.json +6 -1
  64. package/schemas/milestone.schema.json +6 -1
  65. package/schemas/task-handoff-event.schema.json +18 -2
  66. package/scripts/generate-pdf-docs.py +512 -0
  67. package/scripts/run-pdf-docs.mjs +62 -0
@@ -13,8 +13,10 @@ 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
21
  import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
20
22
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.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');
@@ -1397,7 +1413,24 @@ export class WorkflowService {
1397
1413
  const root = this.store.taskRoot(identity.projectId, taskId);
1398
1414
  const briefHash = this.store.hashArtifact(root, 'brief.md');
1399
1415
  const planHash = this.store.hashArtifact(root, 'plan.md');
1416
+ if (task.briefHash !== briefHash || task.planHash !== planHash) {
1417
+ throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
1418
+ taskId: task.id,
1419
+ stateBriefHash: task.briefHash,
1420
+ artifactBriefHash: briefHash,
1421
+ statePlanHash: task.planHash,
1422
+ artifactPlanHash: planHash,
1423
+ });
1424
+ }
1400
1425
  readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
1426
+ const mechanicalFeasibility = evaluateMechanicalFeasibility(identity.repositoryRoot, task, briefHash, planHash);
1427
+ if (mechanicalFeasibility.decision === 'blocked') {
1428
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task Plan contains a proven mechanical contradiction.', {
1429
+ diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
1430
+ mechanicalFeasibility,
1431
+ requiredAction: 'task plan-set',
1432
+ });
1433
+ }
1401
1434
  const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
1402
1435
  const event = {
1403
1436
  kind: 'execution',
@@ -1407,11 +1440,12 @@ export class WorkflowService {
1407
1440
  briefHash,
1408
1441
  planHash,
1409
1442
  resultHash: null,
1410
- evidenceHash: null,
1411
- headCommit: null,
1443
+ evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
1444
+ headCommit: mechanicalFeasibility.evaluatedHead,
1412
1445
  reason,
1413
1446
  authorizationMode: delegation ? 'delegated' : 'human',
1414
1447
  ...(delegation ? { delegation } : {}),
1448
+ mechanicalFeasibility,
1415
1449
  };
1416
1450
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
1417
1451
  }
@@ -1443,6 +1477,9 @@ export class WorkflowService {
1443
1477
  if (grantId)
1444
1478
  this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
1445
1479
  const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
1480
+ const dependencyBinding = milestone
1481
+ ? this.currentMilestoneDependencyBinding(milestone, task.id)
1482
+ : null;
1446
1483
  const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
1447
1484
  const milestoneDisplayNumber = milestone
1448
1485
  ? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
@@ -1488,6 +1525,7 @@ export class WorkflowService {
1488
1525
  writerToken: lease.token,
1489
1526
  milestoneDisplayNumber,
1490
1527
  taskDisplayNumber,
1528
+ dependencyBinding,
1491
1529
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1492
1530
  grantId,
1493
1531
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
@@ -1510,7 +1548,11 @@ export class WorkflowService {
1510
1548
  if (!normalizedActor) {
1511
1549
  throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
1512
1550
  }
1513
- const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now());
1551
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1552
+ const dependencyBinding = task.milestoneId
1553
+ ? this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id)
1554
+ : null;
1555
+ const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now(), dependencyBinding);
1514
1556
  if (pendingHandoff.latestEvent.grantId) {
1515
1557
  this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
1516
1558
  }
@@ -1543,7 +1585,7 @@ export class WorkflowService {
1543
1585
  lease = context.locks.acquire(taskId, normalizedActor);
1544
1586
  }
1545
1587
  try {
1546
- const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1588
+ const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now(), dependencyBinding);
1547
1589
  return {
1548
1590
  task,
1549
1591
  event,
@@ -1567,6 +1609,22 @@ export class WorkflowService {
1567
1609
  if (!normalizedActor)
1568
1610
  throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
1569
1611
  const posture = readTaskC1Posture(this.store, task);
1612
+ if (posture.state === 'claimed' && task.milestoneId) {
1613
+ const dependencyBinding = this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id);
1614
+ const claimedBinding = posture.latestEvent.dependencyBinding ?? null;
1615
+ if (claimedBinding === null
1616
+ || claimedBinding.milestoneRevision !== dependencyBinding.milestoneRevision
1617
+ || claimedBinding.membershipRevision !== dependencyBinding.membershipRevision
1618
+ || claimedBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
1619
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Writer credential replacement requires a current Milestone dependency binding.', {
1620
+ diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
1621
+ taskId: task.id,
1622
+ milestoneId: task.milestoneId,
1623
+ claimDependencyBinding: claimedBinding,
1624
+ currentDependencyBinding: dependencyBinding,
1625
+ });
1626
+ }
1627
+ }
1570
1628
  const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1571
1629
  const action = String(navigation.action);
1572
1630
  const currentLease = context.locks.inspect(taskId);
@@ -1615,17 +1673,10 @@ export class WorkflowService {
1615
1673
  this.requireExecutionAdoptionPosture(identity, taskId);
1616
1674
  if (task.status !== 'ready')
1617
1675
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
1618
- this.assertExecutionAuthorizationFresh(task);
1676
+ const executionAuthorization = this.assertExecutionAuthorizationFresh(task, identity.repositoryRoot);
1619
1677
  if (task.milestoneId) {
1620
1678
  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
- }
1679
+ const dependencyBinding = this.currentMilestoneDependencyBinding(milestone, task.id);
1629
1680
  const handoffPosture = readTaskC1Posture(this.store, task);
1630
1681
  if (handoffPosture.state !== 'claimed') {
1631
1682
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
@@ -1635,6 +1686,20 @@ export class WorkflowService {
1635
1686
  requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
1636
1687
  });
1637
1688
  }
1689
+ const claimBinding = handoffPosture.latestEvent.dependencyBinding ?? null;
1690
+ if (claimBinding === null
1691
+ || claimBinding.milestoneRevision !== dependencyBinding.milestoneRevision
1692
+ || claimBinding.membershipRevision !== dependencyBinding.membershipRevision
1693
+ || claimBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
1694
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a current dependency-bound C1 claim.', {
1695
+ diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
1696
+ taskId: task.id,
1697
+ milestoneId: milestone.id,
1698
+ claimDependencyBinding: handoffPosture.latestEvent.dependencyBinding ?? null,
1699
+ currentDependencyBinding: dependencyBinding,
1700
+ requiredAction: 'Prepare and claim a new C1 handoff from the current Milestone Plan.',
1701
+ });
1702
+ }
1638
1703
  }
1639
1704
  if (!isClean(identity.repositoryRoot)) {
1640
1705
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
@@ -1644,12 +1709,22 @@ export class WorkflowService {
1644
1709
  const branch = `codex/task-${task.id.slice(5, 15).toLowerCase()}-${task.slug}`;
1645
1710
  let baseCommit;
1646
1711
  let taskBranch;
1712
+ const authorizedBaseCommit = executionAuthorization.mechanicalFeasibility?.sourceKind === 'pre-start-base'
1713
+ ? executionAuthorization.mechanicalFeasibility.evaluatedHead
1714
+ : null;
1647
1715
  if (workspaceOwner === 'local') {
1648
- baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch);
1716
+ baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch, authorizedBaseCommit);
1649
1717
  taskBranch = branch;
1650
1718
  }
1651
1719
  else {
1652
1720
  baseCommit = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
1721
+ if (authorizedBaseCommit !== null && baseCommit !== authorizedBaseCommit) {
1722
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
1723
+ baseBranch: task.baseBranch,
1724
+ authorizedBaseCommit,
1725
+ currentBaseCommit: baseCommit,
1726
+ });
1727
+ }
1653
1728
  taskBranch = currentBranch(identity.repositoryRoot);
1654
1729
  if (!taskBranch || taskBranch === task.baseBranch) {
1655
1730
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `Externally owned execution requires a dedicated branch different from ${task.baseBranch}.`);
@@ -1668,7 +1743,7 @@ export class WorkflowService {
1668
1743
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
1669
1744
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
1670
1745
  }
1671
- this.assertExecutionAuthorizationFresh(task);
1746
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1672
1747
  const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
1673
1748
  .filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
1674
1749
  if (currentPlanBlockingDecisions.length > 0) {
@@ -2304,7 +2379,7 @@ export class WorkflowService {
2304
2379
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
2305
2380
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
2306
2381
  }
2307
- this.assertExecutionAuthorizationFresh(task);
2382
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2308
2383
  this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2309
2384
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
2310
2385
  }
@@ -2315,6 +2390,47 @@ export class WorkflowService {
2315
2390
  this.requireExecutionAdoptionPosture(context.identity, taskId);
2316
2391
  return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
2317
2392
  }
2393
+ recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
2394
+ const context = this.#mutationContext(repository);
2395
+ const task = this.store.readTask(context.identity.projectId, taskId);
2396
+ assertExpectedRevision(task, expectedRevision);
2397
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
2398
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2399
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2400
+ assertTaskBranch(context.identity.repositoryRoot, task);
2401
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2402
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2403
+ const handoff = readTaskC1Posture(this.store, task);
2404
+ const requiredActor = handoff.state === 'claimed'
2405
+ ? handoff.claimant
2406
+ : defaultPlanIntegrityRecoveryActor(task.id);
2407
+ if (actor.trim() !== requiredActor) {
2408
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery actor does not match the fresh Workflow route.', {
2409
+ taskId,
2410
+ stepId,
2411
+ requiredActor,
2412
+ actualActor: actor.trim(),
2413
+ });
2414
+ }
2415
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2416
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2417
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the failed Step to be guarded by the current Plan Risk Audit.', {
2418
+ taskId,
2419
+ stepId,
2420
+ });
2421
+ }
2422
+ const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
2423
+ if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
2424
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
2425
+ taskId,
2426
+ stepId,
2427
+ applicable: assessment.applicable,
2428
+ blockers: assessment.blockers,
2429
+ });
2430
+ }
2431
+ const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
2432
+ return { assessment, event };
2433
+ }
2318
2434
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
2319
2435
  const { identity } = this.#mutationContext(repository);
2320
2436
  this.readCurrentAdoptionPostureStrict(identity.projectId);
@@ -2693,6 +2809,13 @@ export class WorkflowService {
2693
2809
  if (!task)
2694
2810
  throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
2695
2811
  const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
2812
+ const dependency = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
2813
+ const candidate = tasksById.get(candidateTaskId);
2814
+ if (!candidate)
2815
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
2816
+ return candidate;
2817
+ });
2818
+ const executionEligible = milestone.status === 'active' && !['merged', 'cancelled'].includes(task.status);
2696
2819
  return {
2697
2820
  ordinal: `T${String(index + 1).padStart(2, '0')}`,
2698
2821
  taskId: task.id,
@@ -2701,6 +2824,15 @@ export class WorkflowService {
2701
2824
  revision: task.revision,
2702
2825
  disposition: membership.disposition,
2703
2826
  membershipReason: membership.reason,
2827
+ dependencyState: dependency.dependencyState,
2828
+ dependsOnTaskIds: dependency.dependsOnTaskIds,
2829
+ unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
2830
+ runnable: executionEligible && dependency.runnable,
2831
+ dependencyBlockReason: !executionEligible
2832
+ ? milestone.status !== 'active'
2833
+ ? `Milestone is ${milestone.status}, not active.`
2834
+ : `Task is terminal (${task.status}).`
2835
+ : dependency.blockReason,
2704
2836
  replacementForTaskId: task.replacementForTaskId ?? null,
2705
2837
  replacedByTaskId: task.replacedByTaskId ?? null,
2706
2838
  stepsCompleted,
@@ -3030,21 +3162,34 @@ export class WorkflowService {
3030
3162
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
3031
3163
  const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
3032
3164
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
3165
+ const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
3033
3166
  const requestedTask = taskId === null
3034
3167
  ? null
3035
3168
  : status.tasks.find((task) => task.id === taskId) ?? null;
3036
3169
  if (taskId !== null && !requestedTask) {
3037
3170
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3038
3171
  }
3039
- if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
3172
+ if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById, tasksById)) {
3173
+ const requestedMilestone = requestedTask.milestoneId
3174
+ ? milestonesById.get(requestedTask.milestoneId) ?? null
3175
+ : null;
3176
+ const dependency = requestedMilestone
3177
+ ? assessMilestoneTaskDependencies(requestedMilestone, requestedTask.id, (candidateTaskId) => {
3178
+ const candidate = tasksById.get(candidateTaskId);
3179
+ if (!candidate)
3180
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
3181
+ return candidate;
3182
+ })
3183
+ : null;
3040
3184
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
3041
3185
  taskId: requestedTask.id,
3042
3186
  taskStatus: requestedTask.status,
3043
3187
  milestoneId: requestedTask.milestoneId,
3188
+ ...(dependency ? { dependency } : {}),
3044
3189
  });
3045
3190
  }
3046
3191
  const activeTask = requestedTask
3047
- ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
3192
+ ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
3048
3193
  if (activeTask) {
3049
3194
  const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3050
3195
  if (dependencyRecovery.eligible) {
@@ -3109,7 +3254,7 @@ export class WorkflowService {
3109
3254
  const taskNext = activeTask.status === 'planning'
3110
3255
  ? null
3111
3256
  : planRiskAuditRecovery
3112
- ?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
3257
+ ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
3113
3258
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
3114
3259
  // strict review are executable while context refresh correctly remains forbidden.
3115
3260
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -3118,6 +3263,8 @@ export class WorkflowService {
3118
3263
  if (taskNext?.action === 'task step-complete'
3119
3264
  || taskNext?.action === 'task step-review'
3120
3265
  || taskNext?.action === 'task remediation-mode-recover'
3266
+ || taskNext?.action === 'task plan-integrity-recover'
3267
+ || taskNext?.blockedAction === 'task plan-integrity-recover'
3121
3268
  || hardStop
3122
3269
  || terminalCorrectiveDecision === 'split-required'
3123
3270
  || terminalCorrectiveDecision === 'stop-escalate') {
@@ -3216,7 +3363,7 @@ export class WorkflowService {
3216
3363
  knowledgeMapHash,
3217
3364
  }));
3218
3365
  }
3219
- return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
3366
+ return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)));
3220
3367
  }
3221
3368
  let repositoryKnowledgeMap;
3222
3369
  try {
@@ -3351,7 +3498,21 @@ export class WorkflowService {
3351
3498
  .filter((membership) => membership.disposition === 'required')
3352
3499
  .map((membership) => {
3353
3500
  const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
3354
- return { taskId: membership.taskId, status: task?.status ?? 'missing' };
3501
+ const dependency = assessMilestoneTaskDependencies(activeMilestone, membership.taskId, (candidateTaskId) => {
3502
+ const candidate = status.tasks.find((taskCandidate) => taskCandidate.id === candidateTaskId);
3503
+ if (!candidate)
3504
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
3505
+ return candidate;
3506
+ });
3507
+ return {
3508
+ taskId: membership.taskId,
3509
+ status: task?.status ?? 'missing',
3510
+ dependencyState: dependency.dependencyState,
3511
+ dependsOnTaskIds: dependency.dependsOnTaskIds,
3512
+ unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
3513
+ runnable: Boolean(task && !['merged', 'cancelled'].includes(task.status) && dependency.runnable),
3514
+ dependencyBlockReason: dependency.blockReason,
3515
+ };
3355
3516
  }),
3356
3517
  },
3357
3518
  }
@@ -3474,7 +3635,21 @@ export class WorkflowService {
3474
3635
  }
3475
3636
  }
3476
3637
  }
3477
- nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
3638
+ currentMilestoneDependencyBinding(milestone, taskId) {
3639
+ assertMilestoneTaskRunnable(milestone, taskId, (candidateTaskId) => this.store.readTask(milestone.projectId, candidateTaskId));
3640
+ if (!milestone.planHash) {
3641
+ throw new WorkflowError('STATE_CORRUPT', `Active Milestone ${milestone.id} has no Plan hash.`, {
3642
+ milestoneId: milestone.id,
3643
+ taskId,
3644
+ });
3645
+ }
3646
+ return {
3647
+ milestoneRevision: milestone.revision,
3648
+ membershipRevision: milestone.membershipRevision,
3649
+ milestonePlanHash: milestone.planHash,
3650
+ };
3651
+ }
3652
+ nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
3478
3653
  const transition = task.status === 'awaiting_execution_authorization'
3479
3654
  ? 'task.execution_authorize'
3480
3655
  : task.status === 'awaiting_final_acceptance'
@@ -3510,7 +3685,59 @@ export class WorkflowService {
3510
3685
  }
3511
3686
  }
3512
3687
  const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
3688
+ let mechanicalFeasibilityOverlay = {};
3689
+ if (task.status === 'awaiting_execution_authorization' && task.planHash) {
3690
+ try {
3691
+ const taskRoot = this.store.taskRoot(projectId, task.id);
3692
+ const briefHash = this.store.hashArtifact(taskRoot, 'brief.md');
3693
+ const planHash = this.store.hashArtifact(taskRoot, 'plan.md');
3694
+ if (task.briefHash !== briefHash || task.planHash !== planHash) {
3695
+ throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
3696
+ taskId: task.id,
3697
+ stateBriefHash: task.briefHash,
3698
+ artifactBriefHash: briefHash,
3699
+ statePlanHash: task.planHash,
3700
+ artifactPlanHash: planHash,
3701
+ });
3702
+ }
3703
+ const mechanicalFeasibility = evaluateMechanicalFeasibility(repositoryRoot, task, briefHash, planHash);
3704
+ mechanicalFeasibilityOverlay = mechanicalFeasibility.decision === 'blocked'
3705
+ ? {
3706
+ action: 'task plan-set',
3707
+ blockedAction: 'task authorize',
3708
+ mechanicalFeasibility,
3709
+ mechanicalFeasibilityGate: {
3710
+ state: 'blocked',
3711
+ diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
3712
+ requiredAction: 'Replace the current Plan through task plan-set, then request fresh execution authorization.',
3713
+ },
3714
+ }
3715
+ : {
3716
+ mechanicalFeasibility,
3717
+ mechanicalFeasibilityGate: {
3718
+ state: 'clear',
3719
+ meaning: 'No supported analyzer proved infeasibility; semantic sufficiency is not asserted.',
3720
+ },
3721
+ };
3722
+ }
3723
+ catch (error) {
3724
+ mechanicalFeasibilityOverlay = {
3725
+ action: 'doctor',
3726
+ blockedAction: 'task authorize',
3727
+ mechanicalFeasibilityGate: {
3728
+ state: 'unavailable',
3729
+ diagnosticCode: error instanceof WorkflowError ? error.code : 'UNKNOWN_ERROR',
3730
+ reason: error instanceof Error ? error.message : String(error),
3731
+ requiredAction: 'Restore the exact Plan/source authority before execution authorization.',
3732
+ },
3733
+ };
3734
+ }
3735
+ }
3513
3736
  const handoffPosture = readTaskC1Posture(this.store, task);
3737
+ const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
3738
+ const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
3739
+ ? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
3740
+ : null;
3514
3741
  const strictStepReviewStep = planRiskAudit
3515
3742
  ? task.steps.find((step) => {
3516
3743
  if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
@@ -3623,6 +3850,39 @@ export class WorkflowService {
3623
3850
  };
3624
3851
  };
3625
3852
  let guardedCorrectiveOverlay = {};
3853
+ if (planIntegrityAssessment?.applicable) {
3854
+ const requiredActor = handoffPosture.state === 'claimed'
3855
+ ? handoffPosture.claimant
3856
+ : defaultPlanIntegrityRecoveryActor(task.id);
3857
+ guardedCorrectiveOverlay = planIntegrityAssessment.eligible
3858
+ ? {
3859
+ action: 'task plan-integrity-recover',
3860
+ blockedAction: 'task run',
3861
+ stepId: planIntegrityAssessment.stepId,
3862
+ planIntegrityRecovery: {
3863
+ state: 'eligible',
3864
+ reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
3865
+ expectedRevision: task.revision,
3866
+ requiredActor,
3867
+ evidence: planIntegrityAssessment.evidence,
3868
+ boundedEffect: 'record-replan-required-decision',
3869
+ preservesWorktree: true,
3870
+ },
3871
+ }
3872
+ : {
3873
+ action: 'doctor',
3874
+ blockedAction: 'task plan-integrity-recover',
3875
+ stepId: planIntegrityAssessment.stepId,
3876
+ planIntegrityRecovery: {
3877
+ state: 'blocked',
3878
+ reasonCode: 'PLAN_INTEGRITY_RECOVERY_BLOCKED',
3879
+ expectedRevision: task.revision,
3880
+ requiredActor,
3881
+ blockers: planIntegrityAssessment.blockers,
3882
+ evidence: planIntegrityAssessment.evidence,
3883
+ },
3884
+ };
3885
+ }
3626
3886
  if (remediationModeRecovery) {
3627
3887
  guardedCorrectiveOverlay = {
3628
3888
  action: 'task remediation-mode-recover',
@@ -3726,13 +3986,29 @@ export class WorkflowService {
3726
3986
  {
3727
3987
  const replacementDiscoveries = this.store.listDiscoveries(projectId)
3728
3988
  .filter((discovery) => discovery.status === 'ready_to_materialize');
3729
- const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
3730
3989
  const replacementMilestone = task.milestoneId
3731
3990
  ? this.store.readMilestone(projectId, task.milestoneId)
3732
3991
  : null;
3992
+ const incidentDependencies = replacementMilestone
3993
+ ? replacementMilestone.memberships.flatMap((membership) => {
3994
+ if (!Array.isArray(membership.dependsOnTaskIds))
3995
+ return [];
3996
+ return [
3997
+ ...(membership.taskId === task.id
3998
+ ? membership.dependsOnTaskIds.map((predecessorTaskId) => ({
3999
+ predecessorTaskId,
4000
+ consumerTaskId: membership.taskId,
4001
+ }))
4002
+ : []),
4003
+ ...(membership.dependsOnTaskIds.includes(task.id)
4004
+ ? [{ predecessorTaskId: task.id, consumerTaskId: membership.taskId }]
4005
+ : []),
4006
+ ];
4007
+ })
4008
+ : [];
3733
4009
  guardedCorrectiveOverlay = {
3734
- action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
3735
- ...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
4010
+ action: 'doctor',
4011
+ blockedAction: 'task replacement-materialize',
3736
4012
  stepId: guardedCorrectivePosture.stepId,
3737
4013
  correctiveDecisionGate: {
3738
4014
  attemptCount: guardedCorrectivePosture.attemptCount,
@@ -3746,29 +4022,17 @@ export class WorkflowService {
3746
4022
  requiredDiscoveryStatus: 'ready_to_materialize',
3747
4023
  oldMembershipDisposition: 'cancelled',
3748
4024
  replacementMembershipDisposition: 'required',
3749
- transaction: 'journal-recoverable',
4025
+ transaction: 'disabled',
4026
+ retainedImplementation: 'journal-recoverable-1-to-1',
4027
+ diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
4028
+ structuralReplacementAvailable: false,
4029
+ incidentDependencies,
3750
4030
  discoveryCandidates: replacementDiscoveries.map((discovery) => ({
3751
4031
  id: discovery.id,
3752
4032
  revision: discovery.revision,
3753
4033
  goal: discovery.goal,
3754
4034
  })),
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
- }),
4035
+ requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
3772
4036
  },
3773
4037
  };
3774
4038
  }
@@ -3854,6 +4118,7 @@ export class WorkflowService {
3854
4118
  }
3855
4119
  : {}),
3856
4120
  ...guardedCorrectiveOverlay,
4121
+ ...mechanicalFeasibilityOverlay,
3857
4122
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3858
4123
  };
3859
4124
  if (handoffPosture.state === 'pending') {
@@ -3967,8 +4232,9 @@ export class WorkflowService {
3967
4232
  }));
3968
4233
  }
3969
4234
  findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
4235
+ const tasksById = new Map(tasks.map((task) => [task.id, task]));
3970
4236
  for (const task of tasks) {
3971
- if (!taskIsActionableFromRepositoryNext(task, milestonesById)
4237
+ if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
3972
4238
  || !task.planHash
3973
4239
  || !taskCanRebindKnowledge(task)
3974
4240
  || this.taskHasTerminalCorrectiveDecision(task))
@@ -4007,7 +4273,7 @@ export class WorkflowService {
4007
4273
  if (policy.scope.kind === 'milestone')
4008
4274
  this.store.readMilestone(projectId, policy.scope.id);
4009
4275
  }
4010
- assertExecutionAuthorizationFresh(task) {
4276
+ assertExecutionAuthorizationFresh(task, repositoryRoot) {
4011
4277
  const root = this.store.taskRoot(task.projectId, task.id);
4012
4278
  const authorization = [...task.authorizations]
4013
4279
  .reverse()
@@ -4016,14 +4282,35 @@ export class WorkflowService {
4016
4282
  throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is missing.');
4017
4283
  const briefHash = this.store.hashArtifact(root, 'brief.md');
4018
4284
  const planHash = this.store.hashArtifact(root, 'plan.md');
4019
- if (authorization.briefHash !== briefHash || authorization.planHash !== planHash) {
4285
+ if (authorization.briefHash !== briefHash
4286
+ || authorization.planHash !== planHash
4287
+ || task.briefHash !== briefHash
4288
+ || task.planHash !== planHash) {
4020
4289
  throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is stale.', {
4021
4290
  authorizedBriefHash: authorization.briefHash,
4022
4291
  currentBriefHash: briefHash,
4023
4292
  authorizedPlanHash: authorization.planHash,
4024
4293
  currentPlanHash: planHash,
4294
+ stateBriefHash: task.briefHash,
4295
+ statePlanHash: task.planHash,
4296
+ });
4297
+ }
4298
+ const mechanicalFeasibility = authorization.mechanicalFeasibility;
4299
+ if (!mechanicalFeasibility)
4300
+ return authorization;
4301
+ const evidenceHash = hashMechanicalFeasibilityEvidence(mechanicalFeasibility);
4302
+ if (authorization.evidenceHash !== evidenceHash
4303
+ || authorization.headCommit !== mechanicalFeasibility.evaluatedHead) {
4304
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical-feasibility evidence does not match its authorization event.', {
4305
+ taskId: task.id,
4306
+ authorizedEvidenceHash: authorization.evidenceHash,
4307
+ currentEvidenceHash: evidenceHash,
4308
+ authorizedHeadCommit: authorization.headCommit,
4309
+ evaluatedHead: mechanicalFeasibility.evaluatedHead,
4025
4310
  });
4026
4311
  }
4312
+ assertMechanicalFeasibilityFresh(repositoryRoot, task, mechanicalFeasibility, briefHash, planHash);
4313
+ return authorization;
4027
4314
  }
4028
4315
  assertMilestoneAuthorizationFresh(milestone) {
4029
4316
  const authorization = [...milestone.authorizations]
@@ -4744,12 +5031,25 @@ function validateMilestonePlan(plan) {
4744
5031
  if (plan.acceptance.length === 0 || plan.acceptance.some((value) => !value.trim())) {
4745
5032
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone acceptance must contain substantive entries.');
4746
5033
  }
4747
- if (plan.memberships.length === 0) {
5034
+ if (!Array.isArray(plan.memberships) || plan.memberships.length === 0) {
4748
5035
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone requires at least one Task membership.');
4749
5036
  }
4750
5037
  for (const membership of plan.memberships) {
4751
- if (!membership.taskId.trim())
5038
+ if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
5039
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.');
5040
+ }
5041
+ if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
4752
5042
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires Task ID.');
5043
+ }
5044
+ if (typeof membership.reason !== 'string') {
5045
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.');
5046
+ }
5047
+ if (!Array.isArray(membership.dependsOnTaskIds)) {
5048
+ throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.');
5049
+ }
5050
+ if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
5051
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.');
5052
+ }
4753
5053
  if (membership.disposition !== 'required' && !membership.reason.trim()) {
4754
5054
  throw new WorkflowError('INVALID_ARGUMENT', `Milestone ${membership.disposition} membership requires a reason.`);
4755
5055
  }
@@ -4779,7 +5079,7 @@ function taskCanRebindKnowledge(task) {
4779
5079
  'awaiting_final_acceptance',
4780
5080
  ].includes(task.status);
4781
5081
  }
4782
- function taskIsActionableFromRepositoryNext(task, milestonesById) {
5082
+ function taskIsActionableFromRepositoryNext(task, milestonesById, tasksById) {
4783
5083
  if (task.status === 'merged' || task.status === 'cancelled')
4784
5084
  return false;
4785
5085
  if (!task.milestoneId)
@@ -4787,10 +5087,18 @@ function taskIsActionableFromRepositoryNext(task, milestonesById) {
4787
5087
  const milestone = milestonesById.get(task.milestoneId);
4788
5088
  if (!milestone)
4789
5089
  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';
5090
+ if (milestone.status !== 'active')
5091
+ return false;
5092
+ const assessment = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
5093
+ const candidate = tasksById.get(candidateTaskId);
5094
+ if (!candidate)
5095
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
5096
+ return candidate;
5097
+ });
5098
+ return assessment.runnable;
5099
+ }
5100
+ function structuralReplacementIsDisabled() {
5101
+ return true;
4794
5102
  }
4795
5103
  function milestoneFinalAcceptanceGate(milestone, headCommit) {
4796
5104
  if (!milestone.resultHash || !milestone.evidenceHash) {
@@ -4920,6 +5228,7 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
4920
5228
  'task review-sealed-record',
4921
5229
  'task corrective-decision-recover',
4922
5230
  'task remediation-mode-recover',
5231
+ 'task plan-integrity-recover',
4923
5232
  'task corrective-decision',
4924
5233
  'task corrective-yield',
4925
5234
  'task handoff',