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

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 (124) hide show
  1. package/README.md +60 -16
  2. package/dist/reviewer-runtime-build.json +1092 -0
  3. package/dist/src/alpha6/check-support-anchor.d.ts +24 -0
  4. package/dist/src/alpha6/check-support-anchor.js +102 -0
  5. package/dist/src/alpha6/check-support-anchor.js.map +1 -0
  6. package/dist/src/alpha6/component-owner.d.ts +28 -0
  7. package/dist/src/alpha6/component-owner.js +95 -0
  8. package/dist/src/alpha6/component-owner.js.map +1 -0
  9. package/dist/src/alpha6/downstream-proof.js +4 -2
  10. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  11. package/dist/src/alpha6/mechanical-feasibility.js +42 -12
  12. package/dist/src/alpha6/mechanical-feasibility.js.map +1 -1
  13. package/dist/src/alpha6/milestone.d.ts +6 -2
  14. package/dist/src/alpha6/milestone.js +164 -10
  15. package/dist/src/alpha6/milestone.js.map +1 -1
  16. package/dist/src/alpha6/npm-check-contract.d.ts +11 -0
  17. package/dist/src/alpha6/npm-check-contract.js +36 -0
  18. package/dist/src/alpha6/npm-check-contract.js.map +1 -0
  19. package/dist/src/alpha6/plan-integrity.js +157 -27
  20. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  21. package/dist/src/alpha6/plan-risk.js +3 -1
  22. package/dist/src/alpha6/plan-risk.js.map +1 -1
  23. package/dist/src/alpha6/remediation-cause.d.ts +13 -0
  24. package/dist/src/alpha6/remediation-cause.js +77 -0
  25. package/dist/src/alpha6/remediation-cause.js.map +1 -0
  26. package/dist/src/alpha6/remediation.d.ts +15 -4
  27. package/dist/src/alpha6/remediation.js +430 -67
  28. package/dist/src/alpha6/remediation.js.map +1 -1
  29. package/dist/src/alpha6/review.d.ts +6 -0
  30. package/dist/src/alpha6/review.js +46 -4
  31. package/dist/src/alpha6/review.js.map +1 -1
  32. package/dist/src/artifacts.js +6 -0
  33. package/dist/src/artifacts.js.map +1 -1
  34. package/dist/src/cli-actions.d.ts +25 -0
  35. package/dist/src/cli-actions.js +109 -0
  36. package/dist/src/cli-actions.js.map +1 -0
  37. package/dist/src/cli.js +244 -253
  38. package/dist/src/cli.js.map +1 -1
  39. package/dist/src/contracts.d.ts +202 -5
  40. package/dist/src/credential-transport.d.ts +38 -0
  41. package/dist/src/credential-transport.js +156 -0
  42. package/dist/src/credential-transport.js.map +1 -0
  43. package/dist/src/dependency-provenance.d.ts +23 -2
  44. package/dist/src/dependency-provenance.js +239 -36
  45. package/dist/src/dependency-provenance.js.map +1 -1
  46. package/dist/src/domain/completed-step-carryover.d.ts +13 -0
  47. package/dist/src/domain/completed-step-carryover.js +71 -0
  48. package/dist/src/domain/completed-step-carryover.js.map +1 -0
  49. package/dist/src/domain/validation.d.ts +9 -1
  50. package/dist/src/domain/validation.js +86 -0
  51. package/dist/src/domain/validation.js.map +1 -1
  52. package/dist/src/gateway-handshake.d.ts +11 -0
  53. package/dist/src/gateway-handshake.js +92 -0
  54. package/dist/src/gateway-handshake.js.map +1 -0
  55. package/dist/src/git.js +1 -0
  56. package/dist/src/git.js.map +1 -1
  57. package/dist/src/historical-step-provenance.d.ts +20 -0
  58. package/dist/src/historical-step-provenance.js +111 -0
  59. package/dist/src/historical-step-provenance.js.map +1 -0
  60. package/dist/src/index.d.ts +2 -0
  61. package/dist/src/index.js +2 -0
  62. package/dist/src/index.js.map +1 -1
  63. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
  64. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
  65. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
  66. package/dist/src/lifecycle/corrective-replan.js +1 -1
  67. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  68. package/dist/src/lifecycle/semantic-registry.js +1 -1
  69. package/dist/src/observation.js +1 -2
  70. package/dist/src/observation.js.map +1 -1
  71. package/dist/src/observed-routes.d.ts +114 -0
  72. package/dist/src/observed-routes.js +545 -0
  73. package/dist/src/observed-routes.js.map +1 -0
  74. package/dist/src/operational-contract.d.ts +26 -0
  75. package/dist/src/operational-contract.js +94 -0
  76. package/dist/src/operational-contract.js.map +1 -0
  77. package/dist/src/reviewer.d.ts +218 -8
  78. package/dist/src/reviewer.js +927 -25
  79. package/dist/src/reviewer.js.map +1 -1
  80. package/dist/src/state/corrective-replan-executor.js +23 -15
  81. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  82. package/dist/src/state/corrective-replan-public.js +35 -10
  83. package/dist/src/state/corrective-replan-public.js.map +1 -1
  84. package/dist/src/state/corrective-replan-transaction.js +12 -4
  85. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  86. package/dist/src/state/corrective-yield-transaction.js +11 -2
  87. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  88. package/dist/src/state/lock.d.ts +1 -0
  89. package/dist/src/state/lock.js +24 -0
  90. package/dist/src/state/lock.js.map +1 -1
  91. package/dist/src/version.d.ts +1 -1
  92. package/dist/src/version.js +1 -1
  93. package/dist/src/version.js.map +1 -1
  94. package/dist/src/workflow.d.ts +70 -4
  95. package/dist/src/workflow.js +1888 -152
  96. package/dist/src/workflow.js.map +1 -1
  97. package/docs/autonomy-guardrails.md +41 -14
  98. package/docs/delegated-approval.md +6 -1
  99. package/docs/development-flow.md +42 -12
  100. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  101. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  102. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  103. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +191 -22
  104. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +163 -45
  105. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +346 -52
  106. package/docs/problem-briefs/01-pre-implementation-integrity.md +11 -7
  107. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +13 -6
  108. package/docs/problem-briefs/README.md +11 -11
  109. package/docs/release.md +89 -9
  110. package/docs/updating-existing-project.md +235 -0
  111. package/docs/validation-report.md +156 -98
  112. package/package.json +12 -3
  113. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
  114. package/plugins/codex-workflow-gateway/references/protocol.md +9 -0
  115. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +67 -1
  116. package/schemas/authorization-event.schema.json +10 -3
  117. package/schemas/corrective-decision-event.schema.json +164 -9
  118. package/schemas/corrective-replan-credentials.private.schema.json +4 -2
  119. package/schemas/milestone-scope-change-event.schema.json +25 -1
  120. package/schemas/remediation-event.schema.json +17 -1
  121. package/schemas/review-result.schema.json +1 -0
  122. package/schemas/step-review-event.schema.json +15 -0
  123. package/schemas/task.schema.json +104 -1
  124. package/schemas/transition-definition.schema.json +8 -0
@@ -2,24 +2,28 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
5
- import { inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
5
+ import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
6
+ import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
6
7
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
7
8
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
8
9
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
10
+ import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
9
11
  import { pathAllowed, validatePlan } from './domain/validation.js';
10
12
  import { WorkflowError } from './errors.js';
13
+ import { withObservedRouteMetadata } from './observed-routes.js';
11
14
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
12
15
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
13
16
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
14
- import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, } from './alpha6/downstream-proof.js';
17
+ import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
15
18
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
16
19
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
17
- import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
20
+ import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
18
21
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
22
+ import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
19
23
  import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
20
- import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
24
+ import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
21
25
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
22
- import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
26
+ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
23
27
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
24
28
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
25
29
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
@@ -27,7 +31,7 @@ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
27
31
  import { ensureDirectory } from './fs-utils.js';
28
32
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
29
33
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
30
- import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
34
+ import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
31
35
  import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
32
36
  import { FileStateStore } from './state/store.js';
33
37
  import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
@@ -600,6 +604,353 @@ export class WorkflowService {
600
604
  const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
601
605
  return { discovery: savedDiscovery, task: savedTask, taskRoot };
602
606
  }
607
+ materializeAuditedRemediationTask(repository, milestoneId, expectedMilestoneRevision, blockedTaskId, expectedBlockedTaskRevision, input) {
608
+ const { identity, locks } = this.#mutationContext(repository);
609
+ const observation = this.observationSnapshot(repository).observation;
610
+ if (observation.kind !== 'assessment'
611
+ || observation.leases.length > 0
612
+ || observation.coreTaskOperations.length > 0
613
+ || observation.pendingProjectTransactions.length > 0
614
+ || observation.pendingMilestoneTransactions.length > 0
615
+ || observation.pendingTaskTransactions.length > 0
616
+ || observation.corruptTaskTransactions.length > 0) {
617
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation materialization requires a coherent, idle observation boundary.', { observation });
618
+ }
619
+ if (!isClean(identity.repositoryRoot)) {
620
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Audited remediation materialization requires a clean Milestone base checkout.', { changedFiles: changedFiles(identity.repositoryRoot) });
621
+ }
622
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
623
+ assertExpectedRevision(milestone, expectedMilestoneRevision);
624
+ if (milestone.status !== 'active' || currentBranch(identity.repositoryRoot) !== milestone.baseBranch) {
625
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation materialization requires the active Milestone base branch.', {
626
+ milestoneStatus: milestone.status,
627
+ expectedBranch: milestone.baseBranch,
628
+ actualBranch: currentBranch(identity.repositoryRoot),
629
+ });
630
+ }
631
+ this.assertMilestoneAuthorizationFresh(milestone);
632
+ const blockedTask = this.store.readTask(identity.projectId, blockedTaskId);
633
+ assertExpectedRevision(blockedTask, expectedBlockedTaskRevision);
634
+ if (blockedTask.milestoneId !== milestone.id
635
+ || blockedTask.status !== 'awaiting_execution_authorization'
636
+ || !blockedTask.planHash
637
+ || blockedTask.workspaceOwner !== null
638
+ || blockedTask.steps.some((step) => step.status !== 'planned')) {
639
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation is available only for an unstarted Milestone Task with a rejected current Plan.', {
640
+ taskId: blockedTask.id,
641
+ taskStatus: blockedTask.status,
642
+ milestoneId: blockedTask.milestoneId,
643
+ workspaceOwner: blockedTask.workspaceOwner,
644
+ });
645
+ }
646
+ const blockedMembership = milestone.memberships.find((membership) => membership.taskId === blockedTask.id);
647
+ if (blockedMembership?.disposition !== 'required') {
648
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The blocked Task must remain a required Milestone member.', { taskId: blockedTask.id, disposition: blockedMembership?.disposition ?? null });
649
+ }
650
+ const audit = readPlanRiskAuditEvents(this.store, blockedTask).at(-1);
651
+ if (!audit
652
+ || audit.decision !== 'stop-escalate'
653
+ || audit.taskRevision !== blockedTask.revision
654
+ || audit.briefHash !== blockedTask.briefHash
655
+ || audit.planHash !== blockedTask.planHash) {
656
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation requires the current stop-escalate Plan Risk Audit.', {
657
+ taskId: blockedTask.id,
658
+ currentTaskRevision: blockedTask.revision,
659
+ currentBriefHash: blockedTask.briefHash,
660
+ currentPlanHash: blockedTask.planHash,
661
+ latestAudit: audit ?? null,
662
+ });
663
+ }
664
+ const duplicate = readMilestoneScopeChangeEvents(this.store, identity.projectId, milestone.id)
665
+ .find((event) => event.authorizationMode === 'audited-remediation'
666
+ && event.remediationEvidence?.planRiskAuditEventHash === audit.eventHash);
667
+ if (duplicate) {
668
+ throw new WorkflowError('TRANSITION_BLOCKED', 'This blocking Plan Risk Audit already has a materialized remediation Task.', {
669
+ planRiskAuditEventId: audit.eventId,
670
+ remediationTaskId: duplicate.remediationEvidence?.remediationTaskId ?? null,
671
+ });
672
+ }
673
+ const discovery = this.store.readDiscovery(identity.projectId, input.discoveryId);
674
+ assertExpectedRevision(discovery, input.expectedDiscoveryRevision);
675
+ const readiness = assessDiscovery(discovery);
676
+ if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
677
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation Discovery is not ready to materialize.', {
678
+ discoveryId: discovery.id,
679
+ missing: readiness.missing,
680
+ });
681
+ }
682
+ const title = input.title.trim();
683
+ if (!title)
684
+ throw new WorkflowError('INVALID_ARGUMENT', 'Remediation Task title is required.');
685
+ const predecessorTaskIds = [...new Set(input.predecessorTaskIds.map((taskId) => taskId.trim()))].sort();
686
+ if (predecessorTaskIds.length === 0 || predecessorTaskIds.some((taskId) => !taskId)) {
687
+ throw new WorkflowError('INVALID_ARGUMENT', 'Audited remediation requires at least one explicit merged predecessor Task.');
688
+ }
689
+ const requiredPredecessorTaskIds = [...(blockedMembership.dependsOnTaskIds ?? [])].sort();
690
+ if (canonicalJsonStringify(predecessorTaskIds) !== canonicalJsonStringify(requiredPredecessorTaskIds)) {
691
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation must inherit the blocked Task exact direct predecessor frontier.', {
692
+ blockedTaskId: blockedTask.id,
693
+ requiredPredecessorTaskIds,
694
+ actualPredecessorTaskIds: predecessorTaskIds,
695
+ });
696
+ }
697
+ const membershipsByTaskId = new Map(milestone.memberships.map((membership) => [membership.taskId, membership]));
698
+ const ancestorTaskIds = new Set();
699
+ const visitAncestors = (taskId) => {
700
+ const membership = membershipsByTaskId.get(taskId);
701
+ for (const dependencyId of membership?.dependsOnTaskIds ?? []) {
702
+ if (ancestorTaskIds.has(dependencyId))
703
+ continue;
704
+ ancestorTaskIds.add(dependencyId);
705
+ visitAncestors(dependencyId);
706
+ }
707
+ };
708
+ visitAncestors(blockedTask.id);
709
+ for (const predecessorTaskId of predecessorTaskIds) {
710
+ const predecessorMembership = membershipsByTaskId.get(predecessorTaskId);
711
+ const predecessor = this.store.readTask(identity.projectId, predecessorTaskId);
712
+ if (!ancestorTaskIds.has(predecessorTaskId)
713
+ || predecessorMembership?.disposition !== 'required'
714
+ || predecessor.status !== 'merged') {
715
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Every remediation predecessor must be a merged required ancestor of the blocked Task.', {
716
+ predecessorTaskId,
717
+ isAncestor: ancestorTaskIds.has(predecessorTaskId),
718
+ disposition: predecessorMembership?.disposition ?? null,
719
+ status: predecessor.status,
720
+ });
721
+ }
722
+ }
723
+ const now = this.now();
724
+ const timestamp = now.toISOString();
725
+ const actor = `agent:milestone-remediation:${milestone.id}`;
726
+ const storedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
727
+ const scannedKnowledgeMap = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, now);
728
+ const inspectedKnowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scannedKnowledgeMap);
729
+ let dependencyKnowledgeRefresh = null;
730
+ let refreshedKnowledgeMap = null;
731
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
732
+ dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(identity.repositoryRoot, milestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
733
+ if (!dependencyKnowledgeRefresh.eligible) {
734
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation cannot bypass unrelated or unproven Project Knowledge drift.', { dependencyKnowledgeRefresh });
735
+ }
736
+ const reconciled = reconcileKnowledgeMap(storedKnowledgeMap, scannedKnowledgeMap);
737
+ const approved = applyKnowledgeSelections(reconciled, [], actor, now);
738
+ if (!approved.approval) {
739
+ throw new WorkflowError('STATE_CORRUPT', 'Dependency-only Knowledge refresh did not produce approval evidence.');
740
+ }
741
+ refreshedKnowledgeMap = {
742
+ ...approved,
743
+ revision: storedKnowledgeMap.revision + 2,
744
+ createdAt: storedKnowledgeMap.createdAt,
745
+ updatedAt: timestamp,
746
+ approval: {
747
+ ...approved.approval,
748
+ authorizationMode: 'corrective-derived',
749
+ },
750
+ };
751
+ }
752
+ const remediationTaskId = createEntityId('TASK', now.getTime());
753
+ const briefContent = renderBrief(discovery, title);
754
+ const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
755
+ const briefHash = sha256Hex(normalizedBriefContent);
756
+ const remediationTask = {
757
+ schemaVersion: STATE_SCHEMA_VERSION,
758
+ revision: 1,
759
+ id: remediationTaskId,
760
+ projectId: identity.projectId,
761
+ createdAt: timestamp,
762
+ updatedAt: timestamp,
763
+ kind: 'task',
764
+ status: 'planning',
765
+ title,
766
+ slug: slugify(title),
767
+ discoveryId: discovery.id,
768
+ milestoneId: milestone.id,
769
+ repositoryRoot: identity.repositoryRoot,
770
+ baseBranch: milestone.baseBranch,
771
+ baseCommit: null,
772
+ taskBranch: null,
773
+ workspaceOwner: null,
774
+ briefHash,
775
+ planHash: null,
776
+ planObjective: null,
777
+ resultHash: null,
778
+ evidenceHash: null,
779
+ requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
780
+ acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
781
+ risks: discovery.assumptions,
782
+ knowledgeImpact: null,
783
+ knowledgeImpactReason: null,
784
+ knowledgeTargets: [],
785
+ knowledgeMapRevision: null,
786
+ knowledgeMapHash: null,
787
+ graphUse: null,
788
+ steps: [],
789
+ authorizations: [],
790
+ knowledgeRebinds: [],
791
+ review: emptyReview(),
792
+ blockReason: null,
793
+ mergedCommit: null,
794
+ systemCommits: [],
795
+ };
796
+ const consumedDiscovery = {
797
+ ...discovery,
798
+ revision: discovery.revision + 1,
799
+ updatedAt: timestamp,
800
+ status: 'materialized',
801
+ materializedEntityId: remediationTaskId,
802
+ };
803
+ const remediationEvidence = {
804
+ blockedTaskId: blockedTask.id,
805
+ blockedTaskRevision: blockedTask.revision,
806
+ blockedBriefHash: blockedTask.briefHash,
807
+ blockedPlanHash: blockedTask.planHash,
808
+ planRiskAuditEventId: audit.eventId,
809
+ planRiskAuditEventHash: audit.eventHash,
810
+ remediationTaskId,
811
+ predecessorTaskIds,
812
+ };
813
+ const reason = `Audited remediation for ${blockedTask.id} after stop-escalate ${audit.eventId}.`;
814
+ const plan = {
815
+ outcome: milestone.outcome,
816
+ successSignal: milestone.successSignal,
817
+ acceptance: [...milestone.acceptance],
818
+ checks: [...milestone.checks],
819
+ memberships: [
820
+ ...milestone.memberships.map((membership) => membership.taskId === blockedTask.id
821
+ ? {
822
+ ...membership,
823
+ dependsOnTaskIds: [...new Set([...(membership.dependsOnTaskIds ?? []), remediationTaskId])],
824
+ }
825
+ : { ...membership, dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])] }),
826
+ {
827
+ taskId: remediationTaskId,
828
+ disposition: 'required',
829
+ reason,
830
+ dependsOnTaskIds: predecessorTaskIds,
831
+ },
832
+ ],
833
+ };
834
+ const readCandidateTask = (taskId) => taskId === remediationTaskId
835
+ ? remediationTask
836
+ : this.store.readTask(identity.projectId, taskId);
837
+ const preparedCandidate = prepareMilestoneScopeChangeCandidate({
838
+ milestone,
839
+ plan,
840
+ readTask: readCandidateTask,
841
+ actor,
842
+ auditedRemediationEvidence: remediationEvidence,
843
+ });
844
+ const sourceAuthorization = [...milestone.authorizations]
845
+ .reverse()
846
+ .find((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved');
847
+ if (!sourceAuthorization) {
848
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation has no current Milestone execution authority.');
849
+ }
850
+ const derivedAuthorization = {
851
+ kind: 'execution',
852
+ decision: 'approved',
853
+ actor,
854
+ recordedAt: timestamp,
855
+ briefHash: null,
856
+ planHash: preparedCandidate.candidatePlanHash,
857
+ resultHash: null,
858
+ evidenceHash: sha256Hex(canonicalJsonStringify({
859
+ sourcePlanHash: sourceAuthorization.planHash,
860
+ sourceAuthorizationMode: sourceAuthorization.authorizationMode ?? 'human',
861
+ planRiskAuditEventHash: audit.eventHash,
862
+ remediationEvidence,
863
+ })),
864
+ headCommit: headCommit(identity.repositoryRoot),
865
+ reason: 'Derived from the current Milestone execution authority and exact blocking Plan Risk Audit.',
866
+ authorizationMode: 'corrective-derived',
867
+ };
868
+ const prepared = {
869
+ ...preparedCandidate,
870
+ authorizations: [...preparedCandidate.authorizations, derivedAuthorization],
871
+ };
872
+ const bundle = prepareMilestoneScopeChangeBundle({
873
+ store: this.store,
874
+ milestone,
875
+ prepared,
876
+ now,
877
+ readTask: readCandidateTask,
878
+ });
879
+ const projectRoot = this.store.projectRoot(identity.projectId);
880
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
881
+ applyProjectTransaction({
882
+ projectRoot,
883
+ projectId: identity.projectId,
884
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
885
+ kind: 'audited-remediation-materialization',
886
+ now,
887
+ targets: [
888
+ ...(refreshedKnowledgeMap
889
+ ? [{
890
+ path: relative(path.join(projectRoot, 'knowledge-map.json')),
891
+ content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n`,
892
+ }]
893
+ : []),
894
+ {
895
+ path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')),
896
+ content: `${JSON.stringify(consumedDiscovery, null, 2)}\n`,
897
+ },
898
+ {
899
+ path: relative(path.join(this.store.taskRoot(identity.projectId, remediationTaskId), 'brief.md')),
900
+ content: normalizedBriefContent,
901
+ },
902
+ {
903
+ path: relative(path.join(this.store.taskRoot(identity.projectId, remediationTaskId), 'state.json')),
904
+ content: `${JSON.stringify(remediationTask, null, 2)}\n`,
905
+ },
906
+ {
907
+ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')),
908
+ content: bundle.planContent,
909
+ },
910
+ {
911
+ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')),
912
+ content: bundle.sidecarContent,
913
+ },
914
+ {
915
+ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')),
916
+ content: bundle.stateContent,
917
+ },
918
+ ],
919
+ validate: () => {
920
+ const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
921
+ const savedRemediationTask = this.store.readTask(identity.projectId, remediationTaskId);
922
+ const savedMilestone = this.readMilestoneCurrent(identity.projectId, milestone.id);
923
+ const savedBlockedMembership = savedMilestone.memberships
924
+ .find((membership) => membership.taskId === blockedTask.id);
925
+ const savedRemediationMembership = savedMilestone.memberships
926
+ .find((membership) => membership.taskId === remediationTaskId);
927
+ const savedAuthorization = [...savedMilestone.authorizations]
928
+ .reverse()
929
+ .find((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved');
930
+ const savedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
931
+ if (savedDiscovery.materializedEntityId !== remediationTaskId
932
+ || savedRemediationTask.milestoneId !== milestone.id
933
+ || !savedBlockedMembership?.dependsOnTaskIds?.includes(remediationTaskId)
934
+ || savedRemediationMembership?.disposition !== 'required'
935
+ || savedAuthorization?.authorizationMode !== 'corrective-derived'
936
+ || savedMilestone.status !== 'active'
937
+ || (refreshedKnowledgeMap !== null
938
+ && (savedKnowledgeMap.revision !== refreshedKnowledgeMap.revision
939
+ || savedKnowledgeMap.status !== 'active'
940
+ || savedKnowledgeMap.approval?.authorizationMode !== 'corrective-derived'))
941
+ || locks.inspect(blockedTask.id) !== null) {
942
+ throw new WorkflowError('STATE_CORRUPT', 'Audited remediation transaction readback failed.');
943
+ }
944
+ },
945
+ });
946
+ return {
947
+ discovery: this.store.readDiscovery(identity.projectId, discovery.id),
948
+ remediationTask: this.store.readTask(identity.projectId, remediationTaskId),
949
+ blockedTask: this.store.readTask(identity.projectId, blockedTask.id),
950
+ milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
951
+ remediationEvidence,
952
+ };
953
+ }
603
954
  replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
604
955
  const { identity, locks } = this.#mutationContext(repository);
605
956
  const oldTask = this.store.readTask(identity.projectId, oldTaskId);
@@ -1265,11 +1616,14 @@ export class WorkflowService {
1265
1616
  || (event.decision === 'stop-escalate' && !overriddenStopDecisionEventIds.has(event.eventId)));
1266
1617
  const downstreamReplanRecovered = readDownstreamProofReplanRecoveries(this.store, task)
1267
1618
  .some((event) => event.resultTaskRevision === task.revision && event.reboundPlanHash === task.planHash);
1268
- const currentPlanRiskAudit = (task.status === 'needs_fix' || currentReplanDecisions.length > 0)
1619
+ const currentPlanRiskAuditExists = task.planHash !== null && readPlanRiskAuditEvents(this.store, task).some((event) => event.briefHash === task.briefHash && event.planHash === task.planHash);
1620
+ const currentPlanRiskAudit = (task.status === 'needs_fix'
1621
+ || (task.steps.some((step) => step.status === 'failed') && currentPlanRiskAuditExists)
1622
+ || currentReplanDecisions.length > 0)
1269
1623
  && !downstreamReplanRecovered
1270
1624
  ? readCurrentPlanRiskAudit(this.store, task)
1271
1625
  : null;
1272
- const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
1626
+ const classifiedCorrectivePosture = currentPlanRiskAudit
1273
1627
  ? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
1274
1628
  : null;
1275
1629
  const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
@@ -1299,7 +1653,10 @@ export class WorkflowService {
1299
1653
  : 'Use task corrective-replan-prepare, confirm, and execute.',
1300
1654
  });
1301
1655
  }
1302
- else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
1656
+ else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)
1657
+ && !(task.status === 'ready'
1658
+ && correctiveReplanPosture?.posture === 'replan-required'
1659
+ && correctiveReplanPosture.correctiveDecision === null)) {
1303
1660
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
1304
1661
  }
1305
1662
  if (currentHardBlockingDecisions.length > 0) {
@@ -1369,7 +1726,8 @@ export class WorkflowService {
1369
1726
  throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
1370
1727
  }
1371
1728
  }
1372
- if (task.planHash && task.status === 'needs_fix') {
1729
+ if (task.planHash && (task.status === 'needs_fix'
1730
+ || correctiveReplanPosture?.posture === 'replan-required')) {
1373
1731
  if (task.planObjective !== null && plan.objective !== task.planObjective) {
1374
1732
  throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation may change the implementation Plan but not the Task objective.', {
1375
1733
  taskId: task.id,
@@ -1391,29 +1749,82 @@ export class WorkflowService {
1391
1749
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
1392
1750
  ? { ...authorization, decision: 'superseded' }
1393
1751
  : authorization);
1394
- const completedIds = task.steps
1395
- .filter((candidate) => candidate.status === 'completed')
1396
- .map((candidate) => candidate.id);
1397
- const plannedSteps = plan.steps.map((step) => preserveCompletedStep(task, step) ?? ({
1398
- ...step,
1399
- status: dependenciesComplete(step, completedIds) ? 'planned' : 'blocked',
1400
- evidence: null,
1401
- }));
1752
+ const retainedCompletedSteps = deriveCompletedStepCarryover(task, plan.steps);
1753
+ const retainedCompletedIds = [...retainedCompletedSteps.keys()];
1754
+ const plannedSteps = plan.steps.map((candidate) => {
1755
+ const step = normalizePlanStep(candidate);
1756
+ return retainedCompletedSteps.get(step.id) ?? ({
1757
+ ...step,
1758
+ status: dependenciesComplete(step, retainedCompletedIds) ? 'planned' : 'blocked',
1759
+ evidence: null,
1760
+ });
1761
+ });
1762
+ const invalidatedStepCommits = [...new Set([
1763
+ ...(task.invalidatedStepCommits ?? []),
1764
+ ...omittedStepEvidenceCommits(task, plannedSteps),
1765
+ ])];
1402
1766
  if (task.status === 'needs_fix' &&
1403
1767
  task.review.status === 'failed' &&
1404
1768
  plannedSteps.every((step) => step.status === 'completed')) {
1405
1769
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
1406
1770
  }
1407
1771
  if (correctiveReplanPosture?.posture === 'replan-required') {
1408
- const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
1409
- if (!executionEvaluation.allowed) {
1410
- throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
1411
- transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1412
- evaluationPhase: executionEvaluation.phase,
1413
- blockingGuardIds: executionEvaluation.blockingGuardIds,
1414
- pendingGuardIds: executionEvaluation.pendingGuardIds,
1415
- guardResults: executionEvaluation.guardResults,
1416
- });
1772
+ if (correctiveReplanPosture.correctiveDecision === null) {
1773
+ const currentStep = task.steps.find((step) => step.id === correctiveReplanPosture.stepId) ?? null;
1774
+ const candidateStep = plan.steps.find((step) => step.id === correctiveReplanPosture.stepId) ?? null;
1775
+ const currentClassification = currentPlanRiskAudit?.audit.stepClassifications
1776
+ .find((classification) => classification.stepId === correctiveReplanPosture.stepId) ?? null;
1777
+ const candidateClassification = planRiskAudit?.stepClassifications
1778
+ .find((classification) => classification.stepId === correctiveReplanPosture.stepId) ?? null;
1779
+ const priorProductionEdges = task.productionEdges ?? [];
1780
+ const candidateProductionEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => `${edge.ownerStepId}\0${edge.consumerStepId}\0${edge.checkRef}`));
1781
+ const c1Posture = readTaskC1Posture(this.store, task);
1782
+ const rawWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
1783
+ const blockingGuardIds = [
1784
+ ...(task.steps.some((step) => step.status === 'in_progress')
1785
+ ? ['task.rolling-replan.no-step-in-progress']
1786
+ : []),
1787
+ ...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
1788
+ ...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
1789
+ ...(preparedPlan.planHash === task.planHash
1790
+ ? ['task.rolling-replan.semantic-plan-change']
1791
+ : []),
1792
+ ...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
1793
+ ? ['task.rolling-replan.guarded-step-retained']
1794
+ : []),
1795
+ ...(!planRiskAudit || !candidateClassification || !currentClassification
1796
+ || !candidateClassification.reviewRequired
1797
+ || currentClassification.categories.some((category) => !candidateClassification.categories.includes(category))
1798
+ ? ['task.rolling-replan.guarded-review-classification-preserved']
1799
+ : []),
1800
+ ...(currentStep && candidateStep
1801
+ && currentStep.forbiddenScope.some((scope) => !candidateStep.forbiddenScope.includes(scope))
1802
+ ? ['task.rolling-replan.forbidden-scope-preserved']
1803
+ : []),
1804
+ ...((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
1805
+ || priorProductionEdges.some((edge) => !candidateProductionEdgeKeys.has(`${edge.ownerStepId}\0${edge.consumerStepId}\0${edge.checkRef}`))
1806
+ ? ['task.rolling-replan.production-edge-boundary-preserved']
1807
+ : []),
1808
+ ];
1809
+ if (blockingGuardIds.length > 0) {
1810
+ throw new WorkflowError('TRANSITION_BLOCKED', `Rolling-wave root-cause replan rejected the candidate Plan for Task ${task.id}.`, {
1811
+ transitionId: 'task.plan.rolling-root-cause-replan',
1812
+ evaluationPhase: 'execution',
1813
+ blockingGuardIds,
1814
+ });
1815
+ }
1816
+ }
1817
+ else {
1818
+ const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
1819
+ if (!executionEvaluation.allowed) {
1820
+ throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
1821
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1822
+ evaluationPhase: executionEvaluation.phase,
1823
+ blockingGuardIds: executionEvaluation.blockingGuardIds,
1824
+ pendingGuardIds: executionEvaluation.pendingGuardIds,
1825
+ guardResults: executionEvaluation.guardResults,
1826
+ });
1827
+ }
1417
1828
  }
1418
1829
  }
1419
1830
  if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
@@ -1436,7 +1847,10 @@ export class WorkflowService {
1436
1847
  knowledgeMapRevision: plan.knowledgeMapRevision,
1437
1848
  knowledgeMapHash: plan.knowledgeMapHash,
1438
1849
  graphUse: plan.graphUse,
1850
+ productionEdges: structuredClone(plan.productionEdges ?? []),
1851
+ requiresProductionEdges: plan.requiresProductionEdges === true,
1439
1852
  steps: plannedSteps,
1853
+ invalidatedStepCommits,
1440
1854
  authorizations,
1441
1855
  review: emptyReview(),
1442
1856
  resultHash: null,
@@ -1672,7 +2086,12 @@ export class WorkflowService {
1672
2086
  requiredAction: 'task plan-set',
1673
2087
  });
1674
2088
  }
1675
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
2089
+ const correctiveAuthority = delegationGrantId === null
2090
+ ? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
2091
+ : null;
2092
+ const delegation = correctiveAuthority
2093
+ ? null
2094
+ : this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
1676
2095
  const event = {
1677
2096
  kind: 'execution',
1678
2097
  decision: 'approved',
@@ -1684,7 +2103,11 @@ export class WorkflowService {
1684
2103
  evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
1685
2104
  headCommit: mechanicalFeasibility.evaluatedHead,
1686
2105
  reason,
1687
- authorizationMode: delegation ? 'delegated' : 'human',
2106
+ authorizationMode: correctiveAuthority
2107
+ ? 'corrective-derived'
2108
+ : delegation
2109
+ ? 'delegated'
2110
+ : 'human',
1688
2111
  ...(delegation ? { delegation } : {}),
1689
2112
  mechanicalFeasibility,
1690
2113
  };
@@ -1869,7 +2292,7 @@ export class WorkflowService {
1869
2292
  const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1870
2293
  const action = String(navigation.action);
1871
2294
  const currentLease = context.locks.inspect(taskId);
1872
- const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
2295
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
1873
2296
  const requiredActor = writerTokenContract
1874
2297
  ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1875
2298
  : null;
@@ -1883,9 +2306,7 @@ export class WorkflowService {
1883
2306
  nextAction: action,
1884
2307
  });
1885
2308
  }
1886
- const lease = currentLease
1887
- ? context.locks.replaceCredential(taskId, normalizedActor)
1888
- : context.locks.acquire(taskId, normalizedActor);
2309
+ const lease = context.locks.replaceOrRecoverCredential(taskId, normalizedActor);
1889
2310
  if (posture.state === 'claimed') {
1890
2311
  appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1891
2312
  }
@@ -2004,6 +2425,7 @@ export class WorkflowService {
2004
2425
  }
2005
2426
  assertTaskBranch(context.identity.repositoryRoot, task);
2006
2427
  const step = requireStep(task, stepId);
2428
+ const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2007
2429
  const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
2008
2430
  if (!['planned', 'failed'].includes(step.status)) {
2009
2431
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
@@ -2038,18 +2460,25 @@ export class WorkflowService {
2038
2460
  const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
2039
2461
  ? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
2040
2462
  : null;
2463
+ const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2464
+ ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2465
+ : null;
2041
2466
  if (step.status === 'planned'
2042
2467
  && dirty.length > 0
2043
2468
  && !resumesCorrectiveReplan
2044
- && !downstreamProofCarryover?.eligible) {
2469
+ && !downstreamProofCarryover?.eligible
2470
+ && !planIntegrityDirtyCarryover?.eligible) {
2045
2471
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2046
2472
  changedFiles: dirty,
2047
2473
  ...(downstreamProofCarryover?.applicable
2048
2474
  ? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
2049
2475
  : {}),
2476
+ ...(planIntegrityDirtyCarryover?.applicable
2477
+ ? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
2478
+ : {}),
2050
2479
  });
2051
2480
  }
2052
- const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
2481
+ const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
2053
2482
  if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
2054
2483
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
2055
2484
  changedFiles: dirtyOutsideStep,
@@ -2096,7 +2525,9 @@ export class WorkflowService {
2096
2525
  return context.locks.acquire(taskId, actor.trim() || 'worker');
2097
2526
  })();
2098
2527
  }
2099
- const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
2528
+ const steps = task.steps.map((candidate) => candidate.id === stepId
2529
+ ? { ...candidate, status: 'in_progress', evidence: null, failurePause: null }
2530
+ : candidate);
2100
2531
  let saved;
2101
2532
  try {
2102
2533
  saved = this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
@@ -2120,7 +2551,7 @@ export class WorkflowService {
2120
2551
  path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
2121
2552
  ],
2122
2553
  allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
2123
- allowedWrites: step.allowedWrites,
2554
+ allowedWrites,
2124
2555
  commands: step.checks,
2125
2556
  forbiddenScope: [
2126
2557
  ...step.forbiddenScope,
@@ -2239,6 +2670,7 @@ export class WorkflowService {
2239
2670
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
2240
2671
  assertTaskBranch(context.identity.repositoryRoot, task);
2241
2672
  const step = requireStep(task, stepId);
2673
+ const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2242
2674
  if (task.status !== 'in_progress' || step.status !== 'in_progress') {
2243
2675
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
2244
2676
  }
@@ -2271,18 +2703,114 @@ export class WorkflowService {
2271
2703
  const checks = runChecks(context.identity.repositoryRoot, step.checks);
2272
2704
  const failed = checks.filter((check) => !check.passed);
2273
2705
  if (failed.length > 0) {
2274
- if (reviewRequired && planRiskAudit) {
2275
- appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
2706
+ const now = this.now();
2707
+ const normalizedFailures = normalizeMechanicalFailures(failed, context.identity.repositoryRoot);
2708
+ // Bind useful uncommitted work to this failure without treating it as
2709
+ // completed-Step authority.
2710
+ const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
2711
+ const failurePause = {
2712
+ recordedAt: now.toISOString(),
2713
+ dirtyFiles,
2714
+ dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
2715
+ checksHash: sha256Hex(canonicalJsonStringify(checks)),
2716
+ failedChecks: normalizedFailures,
2717
+ };
2718
+ const preparedRemediation = reviewRequired && planRiskAudit
2719
+ ? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
2276
2720
  completionCommit: headCommit(context.identity.repositoryRoot),
2277
2721
  checksHash: sha256Hex(canonicalJsonStringify(checks)),
2278
2722
  reviewEventHash: null,
2279
2723
  reviewerAttestationHash: null,
2280
- }, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
2281
- }
2282
- const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
2283
- return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
2724
+ executionTaskRevision: task.revision,
2725
+ dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
2726
+ }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
2727
+ : null;
2728
+ const steps = task.steps.map((candidate) => candidate.id === stepId
2729
+ ? { ...candidate, status: 'failed', evidence: null, failurePause }
2730
+ : candidate);
2731
+ const paused = {
2732
+ ...task,
2733
+ revision: task.revision + 1,
2734
+ updatedAt: now.toISOString(),
2735
+ status: 'needs_fix',
2736
+ steps,
2737
+ blockReason: `${stepId} checks failed.`,
2738
+ };
2739
+ const stepHistory = readRemediationEvents(this.store, task)
2740
+ .filter((event) => event.stepId === stepId);
2741
+ const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
2742
+ ? stepHistory
2743
+ : [...stepHistory, preparedRemediation.event];
2744
+ const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
2745
+ const c1Posture = readTaskC1Posture(this.store, task);
2746
+ const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
2747
+ ? prepareTaskCorrectiveYield(this.store, paused, {
2748
+ actor: c1Posture.claimant,
2749
+ writerToken: lease.token,
2750
+ reason: `Yield execution authority after repeated mechanical cause ${preparedRemediation.event.eventId}.`,
2751
+ eventId: `HOF-${sha256Hex([
2752
+ 'checks-failed-root-cause-replan-pause-v1',
2753
+ task.id,
2754
+ String(task.revision),
2755
+ preparedRemediation.event.eventHash,
2756
+ c1Posture.claimant,
2757
+ ].join('\0'))}`,
2758
+ }, now)
2759
+ : null;
2760
+ const projectRoot = this.store.projectRoot(task.projectId);
2761
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2762
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2763
+ const failureHead = headCommit(context.identity.repositoryRoot);
2764
+ applyProjectTransaction({
2765
+ projectRoot,
2766
+ projectId: task.projectId,
2767
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2768
+ kind: rollingCauseDecision === 'root-cause-replan'
2769
+ ? 'failed-step-root-cause-replan-pause'
2770
+ : 'failed-step-continuation',
2771
+ now,
2772
+ targets: [
2773
+ ...(preparedRemediation?.postimage
2774
+ ? [{
2775
+ path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
2776
+ content: preparedRemediation.postimage,
2777
+ }]
2778
+ : []),
2779
+ ...(preparedYield
2780
+ ? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
2781
+ : []),
2782
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
2783
+ { path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null },
2784
+ ],
2785
+ validate: () => {
2786
+ const saved = this.store.readTask(task.projectId, task.id);
2787
+ const savedStep = requireStep(saved, stepId);
2788
+ const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
2789
+ const remediationHash = preparedRemediation
2790
+ ? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
2791
+ : null;
2792
+ if (saved.revision !== paused.revision
2793
+ || saved.status !== 'needs_fix'
2794
+ || savedStep.status !== 'failed'
2795
+ || savedStep.failurePause?.dirtyWorktreeHash !== failurePause.dirtyWorktreeHash
2796
+ || savedStep.failurePause?.checksHash !== failurePause.checksHash
2797
+ || remediationHash !== (preparedRemediation?.event.eventHash ?? null)
2798
+ || context.locks.inspect(task.id) !== null
2799
+ || (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
2800
+ || headCommit(context.identity.repositoryRoot) !== failureHead
2801
+ || JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
2802
+ || hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== failurePause.dirtyWorktreeHash) {
2803
+ throw new WorkflowError('STATE_CORRUPT', 'Failed-Step continuation transaction readback failed.', {
2804
+ taskId,
2805
+ stepId,
2806
+ });
2807
+ }
2808
+ validateTaskHistory(context.identity.repositoryRoot, saved);
2809
+ },
2810
+ });
2811
+ return this.store.readTask(task.projectId, task.id);
2284
2812
  }
2285
- const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, step.allowedWrites);
2813
+ const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, allowedWrites);
2286
2814
  const evidence = {
2287
2815
  recordedAt: this.now().toISOString(),
2288
2816
  commitSha: committed.commitSha,
@@ -2321,7 +2849,7 @@ export class WorkflowService {
2321
2849
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
2322
2850
  const task = this.store.readTask(context.identity.projectId, taskId);
2323
2851
  assertExpectedRevision(task, expectedRevision);
2324
- this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2852
+ const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
2325
2853
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
2326
2854
  if (task.status !== 'in_progress' && task.status !== 'blocked') {
2327
2855
  throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
@@ -2392,18 +2920,14 @@ export class WorkflowService {
2392
2920
  resolution: currentCycle.resolution,
2393
2921
  });
2394
2922
  }
2395
- appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2396
- completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
2397
- checksHash: currentCycle.reviewRecordedEvent.checksHash,
2398
- reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
2399
- reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
2400
- }, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
2923
+ return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, currentCycle.reviewRecordedEvent, currentCycle.reviewerAttestationEvent.eventHash, planRiskAudit, undefined, reviewLease);
2401
2924
  }
2402
2925
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
2403
2926
  }
2404
2927
  ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2405
2928
  const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2406
- const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
2929
+ const reviewTask = taskWithEffectiveStepAllowedWrites(this.store, task, stepId);
2930
+ const launch = launchStrictStepReviewer(context.identity.repositoryRoot, reviewTask, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner, reviewScopeAugmentations(this.store, task, stepId));
2407
2931
  const recorded = recordStrictStepReview(this.store, task, stepId, {
2408
2932
  review: launch.review,
2409
2933
  reviewerOutputHash: launch.reviewerOutputHash,
@@ -2420,16 +2944,11 @@ export class WorkflowService {
2420
2944
  resolution: recorded.resolution,
2421
2945
  });
2422
2946
  }
2423
- appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2424
- completionCommit: reviewRecordedEvent.completionCommit,
2425
- checksHash: reviewRecordedEvent.checksHash,
2426
- reviewEventHash: reviewRecordedEvent.eventHash,
2427
- reviewerAttestationHash: reviewerAttestationEvent.eventHash,
2428
- }, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
2947
+ return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewRecordedEvent, reviewerAttestationEvent.eventHash, planRiskAudit, launch.review.summary, reviewLease);
2429
2948
  }
2430
2949
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
2431
2950
  }
2432
- prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2951
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision, reviewMode = 'ordinary') {
2433
2952
  const identity = resolveRepositoryIdentity(repository);
2434
2953
  const task = this.store.readTask(identity.projectId, taskId);
2435
2954
  assertExpectedRevision(task, expectedRevision);
@@ -2449,15 +2968,18 @@ export class WorkflowService {
2449
2968
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2450
2969
  }
2451
2970
  const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2452
- return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
2971
+ return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, taskWithEffectiveStepAllowedWrites(this.store, task, stepId), this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, reviewScopeAugmentations(this.store, task, stepId), reviewMode);
2453
2972
  }
2454
2973
  recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2455
2974
  const context = this.#mutationContext(repository);
2456
2975
  const task = this.store.readTask(context.identity.projectId, taskId);
2457
2976
  assertExpectedRevision(task, expectedRevision);
2458
- this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2977
+ const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
2459
2978
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2460
- const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2979
+ if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
2980
+ throw new WorkflowError('INVALID_ARGUMENT', 'External Step Review requires reviewMode ordinary or security.');
2981
+ }
2982
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision, input.reviewMode);
2461
2983
  let review;
2462
2984
  try {
2463
2985
  review = validateExternalStrictStepReviewInput(input, packet);
@@ -2492,23 +3014,131 @@ export class WorkflowService {
2492
3014
  isolationResult: 'verified',
2493
3015
  }, this.now());
2494
3016
  if (recorded.resolution === 'failed') {
2495
- appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2496
- completionCommit: recorded.reviewRecordedEvent.completionCommit,
2497
- checksHash: recorded.reviewRecordedEvent.checksHash,
2498
- reviewEventHash: recorded.reviewRecordedEvent.eventHash,
2499
- reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
2500
- }, readCurrentPlanRiskAudit(this.store, task), this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
3017
+ const saved = this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, recorded.reviewRecordedEvent, recorded.reviewerAttestationEvent.eventHash, readCurrentPlanRiskAudit(this.store, task), review.summary, reviewLease);
3018
+ return {
3019
+ mode: 'external-sealed',
3020
+ reviewMode: packet.reviewMode,
3021
+ packetHash: packet.packetHash,
3022
+ repositorySealHash: packet.repositorySealHash,
3023
+ reviewerThreadId: input.reviewerThreadId,
3024
+ task: saved,
3025
+ };
2501
3026
  }
2502
3027
  const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2503
3028
  return {
2504
3029
  mode: 'external-sealed',
3030
+ reviewMode: packet.reviewMode,
2505
3031
  packetHash: packet.packetHash,
2506
3032
  repositorySealHash: packet.repositorySealHash,
2507
3033
  reviewerThreadId: input.reviewerThreadId,
2508
3034
  task: saved,
2509
3035
  };
2510
3036
  }
2511
- prepareExternalTaskReview(repository, taskId, expectedRevision) {
3037
+ finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewEvent, reviewerAttestationHash, planRiskAudit, summary, reviewLease) {
3038
+ const now = this.now();
3039
+ const preparedRemediation = prepareRemediationEvent(this.store, task, stepId, 'review-failed', {
3040
+ completionCommit: reviewEvent.completionCommit,
3041
+ checksHash: reviewEvent.checksHash,
3042
+ reviewEventHash: reviewEvent.eventHash,
3043
+ reviewerAttestationHash,
3044
+ }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), reviewCauseBindings(reviewEvent));
3045
+ const stepHistory = readRemediationEvents(this.store, task)
3046
+ .filter((event) => event.stepId === stepId);
3047
+ const resultingHistory = preparedRemediation.postimage === null
3048
+ ? stepHistory
3049
+ : [...stepHistory, preparedRemediation.event];
3050
+ const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3051
+ const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
3052
+ const paused = {
3053
+ ...resolved,
3054
+ revision: task.revision + 1,
3055
+ updatedAt: now.toISOString(),
3056
+ };
3057
+ const projectRoot = this.store.projectRoot(task.projectId);
3058
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
3059
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
3060
+ const failureHead = headCommit(context.identity.repositoryRoot);
3061
+ const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3062
+ const dirtyWorktreeHash = hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles);
3063
+ const c1Posture = readTaskC1Posture(this.store, task);
3064
+ if (rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && !reviewLease) {
3065
+ throw new WorkflowError('STATE_CONFLICT', 'Root-cause replan pause requires the claimed writer authority it will release.', {
3066
+ taskId: task.id,
3067
+ stepId,
3068
+ });
3069
+ }
3070
+ const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && reviewLease
3071
+ ? prepareTaskCorrectiveYield(this.store, paused, {
3072
+ actor: c1Posture.claimant,
3073
+ writerToken: reviewLease.token,
3074
+ reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
3075
+ eventId: `HOF-${sha256Hex([
3076
+ 'strict-review-root-cause-replan-pause-v1',
3077
+ task.id,
3078
+ String(task.revision),
3079
+ preparedRemediation.event.eventHash,
3080
+ c1Posture.claimant,
3081
+ ].join('\0'))}`,
3082
+ }, now)
3083
+ : null;
3084
+ if (reviewLease) {
3085
+ const currentLease = context.locks.inspect(task.id);
3086
+ if (!currentLease || currentLease.token !== reviewLease.token || currentLease.owner !== reviewLease.owner) {
3087
+ throw new WorkflowError('STATE_CONFLICT', 'Strict Step Review writer authority changed before the failure pause could be recorded.', {
3088
+ taskId: task.id,
3089
+ stepId,
3090
+ });
3091
+ }
3092
+ }
3093
+ applyProjectTransaction({
3094
+ projectRoot,
3095
+ projectId: task.projectId,
3096
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
3097
+ kind: rollingCauseDecision === 'root-cause-replan'
3098
+ ? 'strict-review-root-cause-replan-pause'
3099
+ : 'strict-review-failed-step-pause',
3100
+ now,
3101
+ targets: [
3102
+ ...(preparedRemediation.postimage
3103
+ ? [{
3104
+ path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
3105
+ content: preparedRemediation.postimage,
3106
+ }]
3107
+ : []),
3108
+ ...(preparedYield
3109
+ ? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
3110
+ : []),
3111
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
3112
+ ...(reviewLease
3113
+ ? [{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null }]
3114
+ : []),
3115
+ ],
3116
+ validate: () => {
3117
+ const saved = this.store.readTask(task.projectId, task.id);
3118
+ const savedStep = requireStep(saved, stepId);
3119
+ const savedRemediation = readRemediationEvents(this.store, saved)
3120
+ .find((event) => event.eventId === preparedRemediation.event.eventId) ?? null;
3121
+ const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3122
+ if (saved.revision !== paused.revision
3123
+ || saved.status !== 'needs_fix'
3124
+ || savedStep.status !== 'failed'
3125
+ || savedRemediation?.eventHash !== preparedRemediation.event.eventHash
3126
+ || context.locks.inspect(task.id) !== null
3127
+ || (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
3128
+ || headCommit(context.identity.repositoryRoot) !== failureHead
3129
+ || JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
3130
+ || hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== dirtyWorktreeHash) {
3131
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review failure-pause transaction readback failed.', {
3132
+ taskId: task.id,
3133
+ stepId,
3134
+ });
3135
+ }
3136
+ validateTaskHistory(context.identity.repositoryRoot, saved);
3137
+ },
3138
+ });
3139
+ return this.store.readTask(task.projectId, task.id);
3140
+ }
3141
+ prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
2512
3142
  const identity = resolveRepositoryIdentity(repository);
2513
3143
  const task = this.store.readTask(identity.projectId, taskId);
2514
3144
  assertExpectedRevision(task, expectedRevision);
@@ -2525,7 +3155,7 @@ export class WorkflowService {
2525
3155
  if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2526
3156
  throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2527
3157
  }
2528
- return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
3158
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'), task.steps.flatMap((step) => reviewScopeAugmentations(this.store, task, step.id)), reviewMode);
2529
3159
  }
2530
3160
  recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2531
3161
  const context = this.#mutationContext(repository);
@@ -2533,7 +3163,10 @@ export class WorkflowService {
2533
3163
  assertExpectedRevision(task, expectedRevision);
2534
3164
  this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2535
3165
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2536
- const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
3166
+ if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
3167
+ throw new WorkflowError('INVALID_ARGUMENT', 'External Task Review requires reviewMode ordinary or security.');
3168
+ }
3169
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision, input.reviewMode);
2537
3170
  let review;
2538
3171
  try {
2539
3172
  review = validateExternalStrictTaskReviewInput(input, packet);
@@ -2547,6 +3180,7 @@ export class WorkflowService {
2547
3180
  const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2548
3181
  return {
2549
3182
  mode: 'external-sealed',
3183
+ reviewMode: packet.reviewMode,
2550
3184
  packetHash: packet.packetHash,
2551
3185
  repositorySealHash: packet.repositorySealHash,
2552
3186
  reviewerThreadId: input.reviewerThreadId,
@@ -2651,8 +3285,6 @@ export class WorkflowService {
2651
3285
  const task = this.store.readTask(context.identity.projectId, taskId);
2652
3286
  assertExpectedRevision(task, expectedRevision);
2653
3287
  this.requireExecutionAdoptionPosture(context.identity, taskId);
2654
- this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2655
- this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2656
3288
  assertTaskBranch(context.identity.repositoryRoot, task);
2657
3289
  this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2658
3290
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
@@ -2684,6 +3316,20 @@ export class WorkflowService {
2684
3316
  blockers: assessment.blockers,
2685
3317
  });
2686
3318
  }
3319
+ const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
3320
+ ? [assessment.evidence.remediationEventId]
3321
+ : assessment.evidence.remediationEvents.map((event) => event.eventId);
3322
+ const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
3323
+ && record.packageVersion === PACKAGE_VERSION
3324
+ && task.systemCommits.includes(record.commitSha)
3325
+ && record.planIntegrity?.stepId === stepId
3326
+ && record.planIntegrity.planHash === task.planHash
3327
+ && canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
3328
+ && record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
3329
+ if (!compatibility) {
3330
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
3331
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
3332
+ }
2687
3333
  const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
2688
3334
  return { assessment, event };
2689
3335
  }
@@ -2769,7 +3415,12 @@ export class WorkflowService {
2769
3415
  this.assertReviewFresh(identity.repositoryRoot, task);
2770
3416
  const resultHash = this.store.hashArtifact(root, 'result.md');
2771
3417
  const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
2772
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
3418
+ const correctiveAuthority = delegationGrantId === null
3419
+ ? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
3420
+ : null;
3421
+ const delegation = correctiveAuthority
3422
+ ? null
3423
+ : this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
2773
3424
  const event = {
2774
3425
  kind: 'final_acceptance',
2775
3426
  decision: 'approved',
@@ -2781,7 +3432,11 @@ export class WorkflowService {
2781
3432
  evidenceHash,
2782
3433
  headCommit: headCommit(identity.repositoryRoot),
2783
3434
  reason: '',
2784
- authorizationMode: delegation ? 'delegated' : 'human',
3435
+ authorizationMode: correctiveAuthority
3436
+ ? 'corrective-derived'
3437
+ : delegation
3438
+ ? 'delegated'
3439
+ : 'human',
2785
3440
  ...(delegation ? { delegation } : {}),
2786
3441
  };
2787
3442
  return this.store.writeTask({
@@ -2884,7 +3539,9 @@ export class WorkflowService {
2884
3539
  return saved;
2885
3540
  }
2886
3541
  status(repository) {
2887
- const snapshot = this.observationSnapshot(repository);
3542
+ return this.statusFromSnapshot(this.observationSnapshot(repository));
3543
+ }
3544
+ statusFromSnapshot(snapshot) {
2888
3545
  return {
2889
3546
  projectId: snapshot.identity.projectId,
2890
3547
  discoveries: snapshot.discoveries,
@@ -2898,6 +3555,73 @@ export class WorkflowService {
2898
3555
  observation: snapshot.observation,
2899
3556
  };
2900
3557
  }
3558
+ compactStatus(repository) {
3559
+ const snapshot = this.observationSnapshot(repository);
3560
+ const status = this.statusFromSnapshot(snapshot);
3561
+ const next = this.nextFromStatus(repository, null, status);
3562
+ const routedId = typeof next.id === 'string'
3563
+ ? next.id
3564
+ : typeof next.taskId === 'string'
3565
+ ? next.taskId
3566
+ : null;
3567
+ const activeTask = routedId === null
3568
+ ? null
3569
+ : snapshot.tasks.find((task) => task.id === routedId) ?? null;
3570
+ const routedStepId = typeof next.stepId === 'string' ? next.stepId : null;
3571
+ const activeStep = activeTask === null
3572
+ ? null
3573
+ : routedStepId === null
3574
+ ? activeTask.steps.find((step) => step.status === 'in_progress')
3575
+ ?? activeTask.steps.find((step) => step.status === 'failed')
3576
+ ?? null
3577
+ : activeTask.steps.find((step) => step.id === routedStepId) ?? null;
3578
+ const action = typeof next.action === 'string' ? next.action : 'blocked';
3579
+ const blockedAction = typeof next.blockedAction === 'string' ? next.blockedAction : null;
3580
+ const navigation = next.navigation && typeof next.navigation === 'object' && !Array.isArray(next.navigation)
3581
+ ? next.navigation
3582
+ : null;
3583
+ const route = {
3584
+ scope: typeof next.scope === 'string' ? next.scope : null,
3585
+ status: typeof next.status === 'string' ? next.status : null,
3586
+ entityId: routedId,
3587
+ navigationKind: navigation && typeof navigation.kind === 'string' ? navigation.kind : null,
3588
+ transitionId: navigation && typeof navigation.transitionId === 'string' ? navigation.transitionId : null,
3589
+ };
3590
+ const routeFingerprint = sha256Hex(canonicalJsonStringify({
3591
+ projectId: snapshot.identity.projectId,
3592
+ taskId: activeTask?.id ?? null,
3593
+ taskRevision: activeTask?.revision ?? null,
3594
+ stepId: activeStep?.id ?? routedStepId,
3595
+ action,
3596
+ blockedAction,
3597
+ route,
3598
+ observationKind: snapshot.observation.kind,
3599
+ observationReasonCodes: snapshot.observation.reasonCodes,
3600
+ }));
3601
+ return {
3602
+ projection: 'codex-workflow-compact-status-v1',
3603
+ bounded: true,
3604
+ projectId: snapshot.identity.projectId,
3605
+ counts: {
3606
+ discoveries: snapshot.discoveries.length,
3607
+ tasks: snapshot.tasks.length,
3608
+ milestones: snapshot.milestones.length,
3609
+ delegations: snapshot.delegations.length,
3610
+ },
3611
+ activeTask: activeTask
3612
+ ? { id: activeTask.id, revision: activeTask.revision, status: activeTask.status }
3613
+ : null,
3614
+ activeStep: activeStep ? { id: activeStep.id, status: activeStep.status } : null,
3615
+ action,
3616
+ blockedAction,
3617
+ routeFingerprint,
3618
+ route,
3619
+ observation: {
3620
+ kind: snapshot.observation.kind,
3621
+ reasonCodes: [...snapshot.observation.reasonCodes],
3622
+ },
3623
+ };
3624
+ }
2901
3625
  recoverMilestoneTransactions(repository) {
2902
3626
  const { identity } = this.#mutationContext(repository);
2903
3627
  const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
@@ -3012,6 +3736,137 @@ export class WorkflowService {
3012
3736
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3013
3737
  return this.inspectDependencyProvenanceCandidate(snapshot, task);
3014
3738
  }
3739
+ historicalStepProvenanceRecoveryPreflight(repository, taskId) {
3740
+ const snapshot = this.observationSnapshot(repository);
3741
+ if (snapshot.observation.kind !== 'assessment') {
3742
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
3743
+ observation: snapshot.observation,
3744
+ });
3745
+ }
3746
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3747
+ if (!task)
3748
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3749
+ return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
3750
+ }
3751
+ historicalStepDependencyRecoveryPreflight(repository, taskId) {
3752
+ const snapshot = this.observationSnapshot(repository);
3753
+ if (snapshot.observation.kind !== 'assessment') {
3754
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
3755
+ observation: snapshot.observation,
3756
+ });
3757
+ }
3758
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3759
+ if (!task)
3760
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3761
+ let candidateParent = null;
3762
+ try {
3763
+ candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
3764
+ }
3765
+ catch {
3766
+ candidateParent = null;
3767
+ }
3768
+ const assessment = candidateParent
3769
+ ? this.inspectHistoricalStepProvenanceCandidate(snapshot, task, {
3770
+ historyBoundary: 'HEAD^',
3771
+ preservedHeadCommit: candidateParent,
3772
+ })
3773
+ : null;
3774
+ if (!assessment?.eligible
3775
+ || !assessment.activeStepId
3776
+ || !assessment.commitSha
3777
+ || !task.planHash) {
3778
+ const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
3779
+ return {
3780
+ ...ordinary,
3781
+ eligible: false,
3782
+ action: null,
3783
+ historicalStepProvenance: null,
3784
+ blockers: [...new Set([
3785
+ ...ordinary.blockers,
3786
+ ...(assessment?.blockers ?? []),
3787
+ 'The dependency candidate parent must contain exactly one accepted and strictly reviewed historical Step commit omitted by the current corrective Plan.',
3788
+ ])],
3789
+ };
3790
+ }
3791
+ let dirtyWorktreeHash;
3792
+ try {
3793
+ dirtyWorktreeHash = hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.dirtyFiles);
3794
+ }
3795
+ catch (error) {
3796
+ const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
3797
+ return {
3798
+ ...ordinary,
3799
+ eligible: false,
3800
+ action: null,
3801
+ historicalStepProvenance: null,
3802
+ blockers: [...new Set([
3803
+ ...ordinary.blockers,
3804
+ error instanceof Error ? error.message : String(error),
3805
+ ])],
3806
+ };
3807
+ }
3808
+ return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, {
3809
+ activeStepId: assessment.activeStepId,
3810
+ planHash: task.planHash,
3811
+ dirtyFiles: assessment.dirtyFiles,
3812
+ dirtyWorktreeHash,
3813
+ retainedCommitSha: assessment.commitSha,
3814
+ });
3815
+ }
3816
+ recoverHistoricalStepProvenance(repository, taskId, expectedRevision, commitSha, actor, reason) {
3817
+ const normalizedActor = actor.trim();
3818
+ const normalizedReason = reason.trim();
3819
+ if (!normalizedActor)
3820
+ throw new WorkflowError('INVALID_ARGUMENT', 'Historical Step provenance recovery requires an actor.');
3821
+ if (!normalizedReason)
3822
+ throw new WorkflowError('INVALID_ARGUMENT', 'Historical Step provenance recovery requires a reason.');
3823
+ const context = this.#mutationContext(repository);
3824
+ const current = context.store.readTask(context.identity.projectId, taskId);
3825
+ const prior = current.historicalStepProvenanceRecoveries?.find((record) => record.commitSha === commitSha);
3826
+ if (prior && (current.invalidatedStepCommits ?? []).includes(commitSha)) {
3827
+ validateTaskHistory(context.identity.repositoryRoot, current);
3828
+ return current;
3829
+ }
3830
+ assertExpectedRevision(current, expectedRevision);
3831
+ const preflight = this.historicalStepProvenanceRecoveryPreflight(repository, taskId);
3832
+ if (!preflight.eligible || preflight.commitSha !== commitSha || !preflight.sourceFinalAcceptanceRecordedAt) {
3833
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded historical Step provenance recovery preflight failed.', {
3834
+ taskId,
3835
+ requestedCommit: commitSha,
3836
+ candidateCommit: preflight.commitSha,
3837
+ blockers: preflight.blockers,
3838
+ });
3839
+ }
3840
+ const sourceAcceptance = current.authorizations.find((authorization) => authorization.kind === 'final_acceptance'
3841
+ && ['approved', 'superseded'].includes(authorization.decision)
3842
+ && authorization.headCommit === commitSha
3843
+ && authorization.recordedAt === preflight.sourceFinalAcceptanceRecordedAt
3844
+ && authorization.planHash
3845
+ && authorization.resultHash
3846
+ && authorization.evidenceHash);
3847
+ if (!sourceAcceptance?.planHash || !sourceAcceptance.resultHash || !sourceAcceptance.evidenceHash) {
3848
+ throw new WorkflowError('STATE_CONFLICT', 'The source final acceptance changed after historical provenance preflight.');
3849
+ }
3850
+ const saved = context.store.writeTask({
3851
+ ...current,
3852
+ invalidatedStepCommits: [...new Set([...(current.invalidatedStepCommits ?? []), commitSha])],
3853
+ historicalStepProvenanceRecoveries: [
3854
+ ...(current.historicalStepProvenanceRecoveries ?? []),
3855
+ {
3856
+ commitSha,
3857
+ actor: normalizedActor,
3858
+ reason: normalizedReason,
3859
+ recordedAt: this.now().toISOString(),
3860
+ sourceFinalAcceptanceRecordedAt: sourceAcceptance.recordedAt,
3861
+ sourcePlanHash: sourceAcceptance.planHash,
3862
+ sourceResultHash: sourceAcceptance.resultHash,
3863
+ sourceEvidenceHash: sourceAcceptance.evidenceHash,
3864
+ },
3865
+ ],
3866
+ }, expectedRevision);
3867
+ validateTaskHistory(context.identity.repositoryRoot, saved);
3868
+ return saved;
3869
+ }
3015
3870
  activeDownstreamProofDependencyRecoveryPreflight(repository, taskId) {
3016
3871
  const snapshot = this.observationSnapshot(repository);
3017
3872
  if (snapshot.observation.kind !== 'assessment') {
@@ -3108,6 +3963,173 @@ export class WorkflowService {
3108
3963
  blockers,
3109
3964
  };
3110
3965
  }
3966
+ planIntegrityUpdatePreflight(repository, taskId) {
3967
+ const snapshot = this.observationSnapshot(repository);
3968
+ if (snapshot.observation.kind !== 'assessment') {
3969
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
3970
+ observation: snapshot.observation,
3971
+ });
3972
+ }
3973
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3974
+ if (!task)
3975
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3976
+ const assessments = task.steps
3977
+ .filter((step) => step.status === 'failed')
3978
+ .map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
3979
+ .filter((assessment) => assessment.applicable);
3980
+ const assessment = assessments.length === 1 ? assessments[0] : null;
3981
+ const adoptedCompatibility = assessment
3982
+ ? null
3983
+ : this.postRebindCheckSupportCompatibility(snapshot, task);
3984
+ const dirtyCarryoverAssessment = assessment || adoptedCompatibility
3985
+ ? null
3986
+ : this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
3987
+ const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
3988
+ && dirtyCarryoverAssessment.compatibility
3989
+ ? {
3990
+ ...dirtyCarryoverAssessment.compatibility,
3991
+ planHash: task.planHash,
3992
+ mode: 'post-rebind-dirty-carryover',
3993
+ }
3994
+ : null;
3995
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
3996
+ const versionSurfaces = {
3997
+ declared: versions.declared,
3998
+ locked: versions.locked,
3999
+ installed: versions.installed,
4000
+ currentBranch: versions.currentBranch,
4001
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4002
+ };
4003
+ const sourceVersion = versionSurfaces.declared;
4004
+ const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
4005
+ const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
4006
+ const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
4007
+ && sourceBetaVersion.patch !== null
4008
+ && sourceBetaVersion.patch >= 11;
4009
+ const targetIsNewer = sourceBetaVersion !== null
4010
+ && targetBetaVersion !== null
4011
+ && (targetBetaVersion.train > sourceBetaVersion.train
4012
+ || (targetBetaVersion.train === sourceBetaVersion.train
4013
+ && targetBetaVersion.patch !== null
4014
+ && sourceBetaVersion.patch !== null
4015
+ && targetBetaVersion.patch > sourceBetaVersion.patch));
4016
+ const blockers = [
4017
+ ...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
4018
+ ...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
4019
+ ? []
4020
+ : (dirtyCarryoverAssessment?.blockers.length
4021
+ ? dirtyCarryoverAssessment.blockers
4022
+ : ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
4023
+ ...(!sourceIsEligibleBeta12 || !targetIsNewer
4024
+ ? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
4025
+ : []),
4026
+ ...Object.entries(versionSurfaces)
4027
+ .filter(([, version]) => version !== sourceVersion)
4028
+ .map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
4029
+ ...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
4030
+ ...observationPreflightBlockers(snapshot.observation),
4031
+ ];
4032
+ return {
4033
+ readOnly: true,
4034
+ eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
4035
+ action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
4036
+ ? 'update plan-integrity-transport'
4037
+ : null,
4038
+ projectId: task.projectId,
4039
+ taskId: task.id,
4040
+ taskRevision: task.revision,
4041
+ stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
4042
+ sourceVersion,
4043
+ targetVersion: PACKAGE_VERSION,
4044
+ taskBranch: task.taskBranch,
4045
+ baseBranch: task.baseBranch,
4046
+ dirtyFiles: assessment?.evidence?.changedFiles
4047
+ ?? adoptedCompatibility?.dirtyFiles
4048
+ ?? dirtyCarryoverCompatibility?.dirtyFiles
4049
+ ?? [],
4050
+ conflictHash: assessment?.evidence?.conflictHash
4051
+ ?? adoptedCompatibility?.conflictHash
4052
+ ?? dirtyCarryoverCompatibility?.conflictHash
4053
+ ?? null,
4054
+ compatibilityMode: adoptedCompatibility?.mode
4055
+ ?? dirtyCarryoverCompatibility?.mode
4056
+ ?? 'failed-step-conflict',
4057
+ requiredTransport: [
4058
+ 'Commit only package.json and package-lock.json on the active Milestone base.',
4059
+ 'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
4060
+ 'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
4061
+ ],
4062
+ blockers,
4063
+ };
4064
+ }
4065
+ planIntegrityDependencyRecoveryPreflight(repository, taskId) {
4066
+ const snapshot = this.observationSnapshot(repository);
4067
+ if (snapshot.observation.kind !== 'assessment') {
4068
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
4069
+ observation: snapshot.observation,
4070
+ });
4071
+ }
4072
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4073
+ if (!task)
4074
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4075
+ const assessmentBlockers = [];
4076
+ const assessments = task.steps
4077
+ .filter((step) => step.status === 'failed')
4078
+ .flatMap((step) => {
4079
+ try {
4080
+ return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
4081
+ }
4082
+ catch (error) {
4083
+ if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
4084
+ assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
4085
+ return [];
4086
+ }
4087
+ throw error;
4088
+ }
4089
+ })
4090
+ .filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
4091
+ const assessment = assessments.length === 1 ? assessments[0] : null;
4092
+ const adoptedCompatibility = assessment?.evidence
4093
+ ? null
4094
+ : this.postRebindCheckSupportCompatibility(snapshot, task);
4095
+ const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
4096
+ ? null
4097
+ : this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
4098
+ const carriedCompatibility = !assessment?.evidence && task.planHash
4099
+ ? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
4100
+ : null;
4101
+ if ((!assessment?.evidence
4102
+ && !carriedCompatibility
4103
+ && !adoptedCompatibility
4104
+ && !dirtyCarryoverCompatibility) || !task.planHash) {
4105
+ const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
4106
+ return {
4107
+ ...ordinary,
4108
+ eligible: false,
4109
+ action: null,
4110
+ planIntegrity: null,
4111
+ blockers: [...new Set([
4112
+ ...ordinary.blockers,
4113
+ ...assessmentBlockers,
4114
+ 'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
4115
+ ])],
4116
+ };
4117
+ }
4118
+ const compatibility = carriedCompatibility
4119
+ ?? adoptedCompatibility
4120
+ ?? dirtyCarryoverCompatibility
4121
+ ?? {
4122
+ stepId: assessment.stepId,
4123
+ planHash: task.planHash,
4124
+ conflictHash: assessment.evidence.conflictHash,
4125
+ dirtyFiles: [...assessment.evidence.changedFiles],
4126
+ dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
4127
+ remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
4128
+ ? [assessment.evidence.remediationEventId]
4129
+ : assessment.evidence.remediationEvents.map((event) => event.eventId),
4130
+ };
4131
+ return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
4132
+ }
3111
4133
  downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
3112
4134
  const snapshot = this.observationSnapshot(repository);
3113
4135
  if (snapshot.observation.kind !== 'assessment') {
@@ -3159,6 +4181,12 @@ export class WorkflowService {
3159
4181
  recoverDownstreamProofReplanDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
3160
4182
  return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'downstream-replan');
3161
4183
  }
4184
+ recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4185
+ return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
4186
+ }
4187
+ recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4188
+ return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
4189
+ }
3162
4190
  recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
3163
4191
  const normalizedActor = actor.trim();
3164
4192
  const normalizedReason = reason.trim();
@@ -3173,7 +4201,11 @@ export class WorkflowService {
3173
4201
  && record.packageVersion === PACKAGE_VERSION
3174
4202
  && (recoveryMode === 'downstream-replan'
3175
4203
  ? Boolean(record.downstreamProofReplan)
3176
- : Boolean(record.activeDownstreamProof) === recoveryMode));
4204
+ : recoveryMode === 'historical-step'
4205
+ ? Boolean(record.historicalStepProvenance)
4206
+ : recoveryMode === 'plan-integrity'
4207
+ ? Boolean(record.planIntegrity)
4208
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
3177
4209
  if (prior && current.systemCommits.includes(currentHead))
3178
4210
  return current;
3179
4211
  assertExpectedRevision(current, expectedRevision);
@@ -3183,39 +4215,115 @@ export class WorkflowService {
3183
4215
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3184
4216
  const preflight = recoveryMode === 'downstream-replan'
3185
4217
  ? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
3186
- : recoveryMode
3187
- ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
3188
- : this.inspectDependencyProvenanceCandidate(snapshot, task);
4218
+ : recoveryMode === 'historical-step'
4219
+ ? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
4220
+ : recoveryMode === 'plan-integrity'
4221
+ ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
4222
+ : recoveryMode
4223
+ ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4224
+ : this.inspectDependencyProvenanceCandidate(snapshot, task);
3189
4225
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
3190
4226
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
3191
4227
  taskId,
3192
4228
  blockers: preflight.blockers,
3193
4229
  });
3194
4230
  }
3195
- return context.store.writeTask({
4231
+ const now = this.now();
4232
+ const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
4233
+ const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
4234
+ return {
4235
+ commitSha: dependencyCommit.commitSha,
4236
+ parentCommitSha: dependencyCommit.parentCommitSha,
4237
+ packageName: PACKAGE_NAME,
4238
+ packageVersion: dependencyCommit.packageVersion,
4239
+ files: ['package-lock.json', 'package.json'],
4240
+ actor: normalizedActor,
4241
+ reason: normalizedReason,
4242
+ recordedAt: now.toISOString(),
4243
+ retainedHistoricalCommits: isTerminalCommit ? preflight.retainedHistoricalCommits : [],
4244
+ ...(isTerminalCommit && preflight.activeDownstreamProof
4245
+ ? { activeDownstreamProof: preflight.activeDownstreamProof }
4246
+ : {}),
4247
+ ...(isTerminalCommit && preflight.downstreamProofReplan
4248
+ ? { downstreamProofReplan: preflight.downstreamProofReplan }
4249
+ : {}),
4250
+ ...(isTerminalCommit && preflight.historicalStepProvenance
4251
+ ? { historicalStepProvenance: preflight.historicalStepProvenance }
4252
+ : {}),
4253
+ ...(isTerminalCommit && preflight.planIntegrity
4254
+ ? { planIntegrity: preflight.planIntegrity }
4255
+ : {}),
4256
+ };
4257
+ });
4258
+ const nextTask = {
3196
4259
  ...task,
3197
- systemCommits: [...task.systemCommits, preflight.commitSha],
4260
+ revision: task.revision + 1,
4261
+ updatedAt: now.toISOString(),
4262
+ systemCommits: [
4263
+ ...task.systemCommits,
4264
+ ...preflight.dependencyCommits.map((dependencyCommit) => dependencyCommit.commitSha),
4265
+ ],
3198
4266
  dependencyProvenanceRecoveries: [
3199
4267
  ...(task.dependencyProvenanceRecoveries ?? []),
4268
+ ...recoveredRecords,
4269
+ ],
4270
+ };
4271
+ if (recoveryMode !== 'plan-integrity' || !preflight.planIntegrity) {
4272
+ return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
4273
+ }
4274
+ const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
4275
+ const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
4276
+ const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
4277
+ let refreshedKnowledgeMap = null;
4278
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
4279
+ const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
4280
+ if (!knowledgeRefresh.eligible) {
4281
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
4282
+ taskId,
4283
+ dependencyKnowledgeRefresh: knowledgeRefresh,
4284
+ });
4285
+ }
4286
+ const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
4287
+ const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
4288
+ if (!approved.approval) {
4289
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
4290
+ }
4291
+ refreshedKnowledgeMap = {
4292
+ ...approved,
4293
+ revision: currentKnowledgeMap.revision + 2,
4294
+ createdAt: currentKnowledgeMap.createdAt,
4295
+ updatedAt: now.toISOString(),
4296
+ approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
4297
+ };
4298
+ }
4299
+ const projectRoot = this.store.projectRoot(task.projectId);
4300
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
4301
+ applyProjectTransaction({
4302
+ projectRoot,
4303
+ projectId: task.projectId,
4304
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
4305
+ kind: 'plan-integrity-dependency-recovery',
4306
+ now,
4307
+ targets: [
4308
+ ...(refreshedKnowledgeMap
4309
+ ? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
4310
+ : []),
3200
4311
  {
3201
- commitSha: preflight.commitSha,
3202
- parentCommitSha: preflight.parentCommitSha,
3203
- packageName: PACKAGE_NAME,
3204
- packageVersion: PACKAGE_VERSION,
3205
- files: ['package-lock.json', 'package.json'],
3206
- actor: normalizedActor,
3207
- reason: normalizedReason,
3208
- recordedAt: this.now().toISOString(),
3209
- retainedHistoricalCommits: preflight.retainedHistoricalCommits,
3210
- ...(preflight.activeDownstreamProof
3211
- ? { activeDownstreamProof: preflight.activeDownstreamProof }
3212
- : {}),
3213
- ...(preflight.downstreamProofReplan
3214
- ? { downstreamProofReplan: preflight.downstreamProofReplan }
3215
- : {}),
4312
+ path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
4313
+ content: `${JSON.stringify(nextTask, null, 2)}\n`,
3216
4314
  },
3217
4315
  ],
3218
- }, expectedRevision);
4316
+ validate: () => {
4317
+ const saved = this.store.readTask(task.projectId, task.id);
4318
+ if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
4319
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
4320
+ }
4321
+ if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
4322
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
4323
+ }
4324
+ },
4325
+ });
4326
+ return this.store.readTask(task.projectId, task.id);
3219
4327
  }
3220
4328
  milestoneProgress(repository, milestoneId) {
3221
4329
  const snapshot = this.observationSnapshot(repository);
@@ -3472,7 +4580,9 @@ export class WorkflowService {
3472
4580
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
3473
4581
  }
3474
4582
  next(repository, taskId = null) {
3475
- const status = this.status(repository);
4583
+ return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
4584
+ }
4585
+ nextFromStatus(repository, taskId, status) {
3476
4586
  if (status.observation.kind === 'blocked') {
3477
4587
  return {
3478
4588
  scope: 'project',
@@ -3612,9 +4722,69 @@ export class WorkflowService {
3612
4722
  ...(dependency ? { dependency } : {}),
3613
4723
  });
3614
4724
  }
4725
+ const prioritizedRemediationTask = status.milestones.flatMap((milestone) => {
4726
+ if (milestone.status !== 'active')
4727
+ return [];
4728
+ const event = [...readMilestoneScopeChangeEvents(this.store, status.projectId, milestone.id)]
4729
+ .reverse()
4730
+ .find((candidate) => candidate.authorizationMode === 'audited-remediation'
4731
+ && candidate.candidatePlanHash === milestone.planHash
4732
+ && candidate.remediationEvidence?.remediationTaskId);
4733
+ if (!event?.remediationEvidence)
4734
+ return [];
4735
+ const task = tasksById.get(event.remediationEvidence.remediationTaskId);
4736
+ return task
4737
+ && taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
4738
+ ? [task]
4739
+ : [];
4740
+ }).at(0) ?? null;
3615
4741
  const activeTask = requestedTask
4742
+ ?? prioritizedRemediationTask
3616
4743
  ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
3617
4744
  if (activeTask) {
4745
+ const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
4746
+ if (historicalStepProvenanceRecovery.eligible) {
4747
+ return withAdoption({
4748
+ scope: 'task',
4749
+ id: activeTask.id,
4750
+ status: activeTask.status,
4751
+ action: 'task historical-step-provenance-recover',
4752
+ revision: activeTask.revision,
4753
+ historicalStepProvenanceRecovery,
4754
+ });
4755
+ }
4756
+ const historicalStepDependencyRecovery = this.historicalStepDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
4757
+ if (historicalStepDependencyRecovery.eligible) {
4758
+ return withAdoption({
4759
+ scope: 'task',
4760
+ id: activeTask.id,
4761
+ status: activeTask.status,
4762
+ action: 'update historical-step-dependency-recover',
4763
+ revision: activeTask.revision,
4764
+ dependencyProvenanceRecovery: historicalStepDependencyRecovery,
4765
+ compatibilityBoundary: {
4766
+ preservesWorktree: true,
4767
+ prerequisite: 'The active Milestone base and Task branch contain only the exact target dependency commits; the candidate parent still binds the reviewed historical Step commit.',
4768
+ continuation: 'Register the dependency commit and historical provenance atomically, then follow fresh next.',
4769
+ },
4770
+ });
4771
+ }
4772
+ const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
4773
+ if (planIntegrityDependencyRecovery.eligible) {
4774
+ return withAdoption({
4775
+ scope: 'task',
4776
+ id: activeTask.id,
4777
+ status: activeTask.status,
4778
+ action: 'update plan-integrity-dependency-recover',
4779
+ revision: activeTask.revision,
4780
+ dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
4781
+ compatibilityBoundary: {
4782
+ preservesWorktree: true,
4783
+ prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
4784
+ continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
4785
+ },
4786
+ });
4787
+ }
3618
4788
  const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3619
4789
  if (dependencyRecovery.eligible) {
3620
4790
  return withAdoption({
@@ -3741,41 +4911,132 @@ export class WorkflowService {
3741
4911
  if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
3742
4912
  throw error;
3743
4913
  const blockingDecision = error.details.decision;
3744
- planRiskAuditRecovery = blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
4914
+ const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
4915
+ const existingRemediationEvent = blockingAudit && activeTask.milestoneId
4916
+ ? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
4917
+ .reverse()
4918
+ .find((event) => event.authorizationMode === 'audited-remediation'
4919
+ && event.remediationEvidence?.planRiskAuditEventHash === blockingAudit.eventHash) ?? null
4920
+ : null;
4921
+ const existingRemediationTask = existingRemediationEvent?.remediationEvidence
4922
+ ? tasksById.get(existingRemediationEvent.remediationEvidence.remediationTaskId) ?? null
4923
+ : null;
4924
+ const remediationDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status)) ?? null;
4925
+ const remediationMilestone = activeTask.milestoneId
4926
+ ? milestonesById.get(activeTask.milestoneId) ?? null
4927
+ : null;
4928
+ const remediationMemberships = remediationMilestone
4929
+ ? new Map(remediationMilestone.memberships.map((membership) => [membership.taskId, membership]))
4930
+ : new Map();
4931
+ const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
4932
+ const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
4933
+ && remediationMilestone?.status === 'active'
4934
+ && remediationMembership?.disposition === 'required';
4935
+ const remediationAncestorIds = new Set();
4936
+ const visitRemediationAncestors = (candidateTaskId) => {
4937
+ for (const predecessorTaskId of remediationMemberships.get(candidateTaskId)?.dependsOnTaskIds ?? []) {
4938
+ if (remediationAncestorIds.has(predecessorTaskId))
4939
+ continue;
4940
+ remediationAncestorIds.add(predecessorTaskId);
4941
+ visitRemediationAncestors(predecessorTaskId);
4942
+ }
4943
+ };
4944
+ visitRemediationAncestors(activeTask.id);
4945
+ const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
4946
+ .filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
4947
+ .sort();
4948
+ planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
3745
4949
  ? {
3746
4950
  scope: 'task',
3747
4951
  id: activeTask.id,
3748
4952
  status: activeTask.status,
3749
- action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
4953
+ action: existingRemediationTask?.status === 'merged'
4954
+ ? 'task plan-set'
4955
+ : existingRemediationTask
4956
+ ? 'doctor'
4957
+ : remediationDiscovery?.status === 'ready_to_materialize'
4958
+ ? 'milestone remediation-materialize'
4959
+ : remediationDiscovery
4960
+ ? 'discovery update'
4961
+ : 'discovery start',
3750
4962
  blockedAction: 'task authorize',
3751
4963
  revision: activeTask.revision,
3752
4964
  planRiskAuditDecision: {
3753
4965
  state: 'recorded-blocking',
3754
4966
  decision: blockingDecision,
3755
4967
  reason: error.message,
3756
- requiredAction: blockingDecision === 'stop-escalate'
3757
- ? 'Replace the rejected Step Plan before requesting fresh authorization.'
3758
- : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
4968
+ requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
3759
4969
  },
3760
- ...(blockingDecision === 'stop-escalate'
3761
- ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
3762
- : {}),
3763
- }
3764
- : {
3765
- scope: 'task',
3766
- id: activeTask.id,
3767
- status: activeTask.status,
3768
- action: 'task plan-risk-audit',
3769
- revision: activeTask.revision,
3770
- planRiskAuditRecovery: {
3771
- state: 'required',
3772
- reason: error.message,
3773
- currentBriefHash: activeTask.briefHash,
3774
- currentPlanHash: activeTask.planHash,
3775
- knowledgeMapRevision: activeTask.knowledgeMapRevision,
3776
- knowledgeMapHash: activeTask.knowledgeMapHash,
4970
+ auditedRemediation: {
4971
+ state: existingRemediationTask?.status === 'merged'
4972
+ ? 'merged-replan-required'
4973
+ : existingRemediationTask
4974
+ ? 'materialized-not-merged'
4975
+ : remediationDiscovery?.status === 'ready_to_materialize'
4976
+ ? 'ready-to-materialize'
4977
+ : 'discovery-required',
4978
+ humanApprovalRequired: false,
4979
+ blockedTaskId: activeTask.id,
4980
+ blockedTaskRevision: activeTask.revision,
4981
+ milestoneId: remediationMilestone.id,
4982
+ milestoneRevision: remediationMilestone.revision,
4983
+ planRiskAuditEventId: blockingAudit?.eventId ?? null,
4984
+ planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
4985
+ discoveryId: remediationDiscovery?.id ?? null,
4986
+ discoveryRevision: remediationDiscovery?.revision ?? null,
4987
+ remediationTaskId: existingRemediationTask?.id ?? null,
4988
+ remediationTaskStatus: existingRemediationTask?.status ?? null,
4989
+ requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
4990
+ eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
4991
+ command: existingRemediationTask?.status === 'merged'
4992
+ ? 'task plan-set'
4993
+ : existingRemediationTask
4994
+ ? 'doctor'
4995
+ : remediationDiscovery?.status === 'ready_to_materialize'
4996
+ ? 'milestone remediation-materialize'
4997
+ : remediationDiscovery
4998
+ ? 'discovery update'
4999
+ : 'discovery start',
5000
+ boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5001
+ preservesMilestoneSemantics: true,
3777
5002
  },
3778
- };
5003
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5004
+ }
5005
+ : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5006
+ ? {
5007
+ scope: 'task',
5008
+ id: activeTask.id,
5009
+ status: activeTask.status,
5010
+ action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5011
+ blockedAction: 'task authorize',
5012
+ revision: activeTask.revision,
5013
+ planRiskAuditDecision: {
5014
+ state: 'recorded-blocking',
5015
+ decision: blockingDecision,
5016
+ reason: error.message,
5017
+ requiredAction: blockingDecision === 'stop-escalate'
5018
+ ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5019
+ : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5020
+ },
5021
+ ...(blockingDecision === 'stop-escalate'
5022
+ ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5023
+ : {}),
5024
+ }
5025
+ : {
5026
+ scope: 'task',
5027
+ id: activeTask.id,
5028
+ status: activeTask.status,
5029
+ action: 'task plan-risk-audit',
5030
+ revision: activeTask.revision,
5031
+ planRiskAuditRecovery: {
5032
+ state: 'required',
5033
+ reason: error.message,
5034
+ currentBriefHash: activeTask.briefHash,
5035
+ currentPlanHash: activeTask.planHash,
5036
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
5037
+ knowledgeMapHash: activeTask.knowledgeMapHash,
5038
+ },
5039
+ };
3779
5040
  }
3780
5041
  }
3781
5042
  const taskNext = activeTask.status === 'planning'
@@ -3784,6 +5045,37 @@ export class WorkflowService {
3784
5045
  ? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
3785
5046
  : planRiskAuditRecovery
3786
5047
  ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
5048
+ const auditedRemediationNavigation = taskNext?.auditedRemediation;
5049
+ let auditedDependencyKnowledgeRefreshBlocked = null;
5050
+ if (auditedRemediationNavigation
5051
+ && activeTaskMilestone
5052
+ && ['discovery start', 'discovery update', 'milestone remediation-materialize'].includes(String(taskNext?.action))) {
5053
+ try {
5054
+ const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
5055
+ const inspectedKnowledgeMap = inspectCurrentKnowledgeMap();
5056
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
5057
+ const dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(repositoryRoot, activeTaskMilestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
5058
+ if (dependencyKnowledgeRefresh.eligible) {
5059
+ return withAdoption(withMilestoneCancellation({
5060
+ ...taskNext,
5061
+ auditedRemediation: {
5062
+ ...auditedRemediationNavigation,
5063
+ dependencyKnowledgeRefresh: {
5064
+ ...dependencyKnowledgeRefresh,
5065
+ humanApprovalRequired: false,
5066
+ application: 'atomic-with-remediation-materialization',
5067
+ },
5068
+ },
5069
+ }));
5070
+ }
5071
+ auditedDependencyKnowledgeRefreshBlocked = dependencyKnowledgeRefresh;
5072
+ }
5073
+ }
5074
+ catch (error) {
5075
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
5076
+ throw error;
5077
+ }
5078
+ }
3787
5079
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
3788
5080
  // strict review are executable while context refresh correctly remains forbidden.
3789
5081
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -3793,8 +5085,12 @@ export class WorkflowService {
3793
5085
  || taskNext?.action === 'task step-review'
3794
5086
  || taskNext?.action === 'task downstream-proof-recover'
3795
5087
  || taskNext?.action === 'task remediation-mode-recover'
5088
+ || (taskNext?.action === 'task writer-credential-replace'
5089
+ && taskNext?.blockedAction === 'task remediation-mode-recover')
3796
5090
  || taskNext?.action === 'task plan-integrity-recover'
3797
5091
  || taskNext?.blockedAction === 'task plan-integrity-recover'
5092
+ || taskNext?.action === 'task corrective-yield'
5093
+ || String(taskNext?.action ?? '').startsWith('task corrective-replan')
3798
5094
  || hardStop
3799
5095
  || terminalCorrectiveDecision === 'split-required'
3800
5096
  || terminalCorrectiveDecision === 'stop-escalate') {
@@ -3831,8 +5127,10 @@ export class WorkflowService {
3831
5127
  ? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
3832
5128
  : null)
3833
5129
  : null;
5130
+ const rollingRootCausePlanSet = taskNext?.action === 'task plan-set'
5131
+ && taskNext.rollingWave?.state === 'root-cause-replan-required';
3834
5132
  const contextRefreshOptions = action === 'project-memory reconcile'
3835
- && taskNext?.action !== 'task plan-set'
5133
+ && (taskNext?.action !== 'task plan-set' || rollingRootCausePlanSet)
3836
5134
  && refreshAssessment?.eligible
3837
5135
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
3838
5136
  : [];
@@ -3878,6 +5176,15 @@ export class WorkflowService {
3878
5176
  },
3879
5177
  }
3880
5178
  : {}),
5179
+ ...(auditedDependencyKnowledgeRefreshBlocked
5180
+ ? {
5181
+ auditedDependencyKnowledgeRefreshBlocked: {
5182
+ ...auditedDependencyKnowledgeRefreshBlocked,
5183
+ humanApprovalRequired: true,
5184
+ requiredAction: 'Resolve or explicitly approve the non-dependency Project Knowledge drift.',
5185
+ },
5186
+ }
5187
+ : {}),
3881
5188
  }));
3882
5189
  }
3883
5190
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
@@ -4184,6 +5491,32 @@ export class WorkflowService {
4184
5491
  milestonePlanHash: milestone.planHash,
4185
5492
  };
4186
5493
  }
5494
+ currentCorrectiveDerivedTaskAuthority(projectId, task, actor) {
5495
+ if (!task.milestoneId || actor.trim() !== `agent:milestone-remediation:${task.milestoneId}`)
5496
+ return null;
5497
+ const milestone = this.readMilestoneCurrent(projectId, task.milestoneId);
5498
+ if (milestone.status !== 'active' || !milestone.planHash)
5499
+ return null;
5500
+ const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
5501
+ if (membership?.disposition !== 'required')
5502
+ return null;
5503
+ const authorization = [...milestone.authorizations]
5504
+ .reverse()
5505
+ .find((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved');
5506
+ if (authorization?.authorizationMode !== 'corrective-derived'
5507
+ || authorization.planHash !== milestone.planHash
5508
+ || authorization.actor !== actor.trim())
5509
+ return null;
5510
+ const scopeChange = [...readMilestoneScopeChangeEvents(this.store, projectId, milestone.id)]
5511
+ .reverse()
5512
+ .find((candidate) => candidate.authorizationMode === 'audited-remediation'
5513
+ && candidate.candidatePlanHash === milestone.planHash
5514
+ && candidate.remediationEvidence !== null
5515
+ && candidate.remediationEvidence !== undefined);
5516
+ if (!scopeChange)
5517
+ return null;
5518
+ return { milestone, authorization, scopeChange };
5519
+ }
4187
5520
  nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
4188
5521
  const transition = task.status === 'awaiting_execution_authorization'
4189
5522
  ? 'task.execution_authorize'
@@ -4193,6 +5526,12 @@ export class WorkflowService {
4193
5526
  const delegatedApprovalOptions = transition
4194
5527
  ? this.delegatedApprovalOptions(projectId, transition, task)
4195
5528
  : [];
5529
+ const correctiveDerivedActor = task.milestoneId
5530
+ ? `agent:milestone-remediation:${task.milestoneId}`
5531
+ : null;
5532
+ const correctiveDerivedAuthority = transition && correctiveDerivedActor
5533
+ ? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
5534
+ : null;
4196
5535
  let remediationModeRecovery = null;
4197
5536
  let guardedCorrectivePosture = null;
4198
5537
  if (planRiskAudit) {
@@ -4200,7 +5539,7 @@ export class WorkflowService {
4200
5539
  guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
4201
5540
  }
4202
5541
  catch (error) {
4203
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
5542
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
4204
5543
  if (!remediationModeRecovery)
4205
5544
  throw error;
4206
5545
  }
@@ -4213,7 +5552,7 @@ export class WorkflowService {
4213
5552
  catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
4214
5553
  }
4215
5554
  catch (error) {
4216
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
5555
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
4217
5556
  if (!remediationModeRecovery)
4218
5557
  throw error;
4219
5558
  }
@@ -4225,6 +5564,11 @@ export class WorkflowService {
4225
5564
  && changedFiles(repositoryRoot).length > 0
4226
5565
  ? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
4227
5566
  : null;
5567
+ const planIntegrityDirtyCarryover = next.action === 'task run'
5568
+ && nextStepId
5569
+ && changedFiles(repositoryRoot).length > 0
5570
+ ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
5571
+ : null;
4228
5572
  let mechanicalFeasibilityOverlay = {};
4229
5573
  if (task.status === 'awaiting_execution_authorization' && task.planHash) {
4230
5574
  try {
@@ -4275,6 +5619,9 @@ export class WorkflowService {
4275
5619
  }
4276
5620
  const handoffPosture = readTaskC1Posture(this.store, task);
4277
5621
  const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
5622
+ const failedStepRemediations = failedStep && !remediationModeRecovery
5623
+ ? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
5624
+ : [];
4278
5625
  const failedStepReviewRouting = failedStep
4279
5626
  ? latestFailedStepReviewRouting(this.store, task, failedStep.id)
4280
5627
  : null;
@@ -4405,11 +5752,21 @@ export class WorkflowService {
4405
5752
  stepId: planIntegrityAssessment.stepId,
4406
5753
  planIntegrityRecovery: {
4407
5754
  state: 'eligible',
4408
- reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
5755
+ reasonCode: planIntegrityAssessment.evidence?.kind === 'missing-npm-script-outside-allowed-writes'
5756
+ ? 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES'
5757
+ : planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
5758
+ ? 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES'
5759
+ : planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
5760
+ ? 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES'
5761
+ : 'NON_EXECUTABLE_NPM_CHECK_OUTSIDE_ALLOWED_WRITES',
4409
5762
  expectedRevision: task.revision,
4410
5763
  requiredActor,
4411
5764
  evidence: planIntegrityAssessment.evidence,
4412
- boundedEffect: 'record-replan-required-decision',
5765
+ boundedEffect: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
5766
+ ? 'record-machine-derived-check-support-scope'
5767
+ : planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
5768
+ ? 'record-machine-derived-component-owner-scope'
5769
+ : 'record-replan-required-decision',
4413
5770
  preservesWorktree: true,
4414
5771
  },
4415
5772
  }
@@ -4542,7 +5899,48 @@ export class WorkflowService {
4542
5899
  };
4543
5900
  break;
4544
5901
  case 'replan-required':
4545
- guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
5902
+ guardedCorrectiveOverlay = guardedCorrectivePosture.correctiveDecision
5903
+ ? correctiveReplanOverlay(guardedCorrectivePosture)
5904
+ : (() => {
5905
+ const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
5906
+ const rollingC1Posture = readTaskC1Posture(this.store, task);
5907
+ const blockers = [
5908
+ ...(!['needs_fix', 'ready'].includes(task.status)
5909
+ ? [`Task must be needs_fix or context-refreshed ready, not ${task.status}.`]
5910
+ : []),
5911
+ ...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may remain in progress.'] : []),
5912
+ ...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
5913
+ ? [`${guardedCorrectivePosture.stepId} must remain failed.`]
5914
+ : []),
5915
+ ...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
5916
+ ...(rollingC1Posture.state !== 'none'
5917
+ ? ['No C1 claim may remain after the root-cause failure pause.']
5918
+ : []),
5919
+ ];
5920
+ return blockers.length > 0
5921
+ ? {
5922
+ action: 'doctor',
5923
+ blockedAction: 'task plan-set',
5924
+ stepId: guardedCorrectivePosture.stepId,
5925
+ rollingWave: {
5926
+ state: 'root-cause-replan-blocked',
5927
+ attemptCount: guardedCorrectivePosture.attemptCount,
5928
+ humanApprovalRequired: false,
5929
+ blockers,
5930
+ },
5931
+ }
5932
+ : {
5933
+ action: 'task plan-set',
5934
+ blockedAction: 'task run',
5935
+ stepId: guardedCorrectivePosture.stepId,
5936
+ rollingWave: {
5937
+ state: 'root-cause-replan-required',
5938
+ attemptCount: guardedCorrectivePosture.attemptCount,
5939
+ humanApprovalRequired: false,
5940
+ boundary: 'Task objective, requirements, acceptance, and security semantics remain unchanged.',
5941
+ },
5942
+ };
5943
+ })();
4546
5944
  break;
4547
5945
  case 'split-required':
4548
5946
  {
@@ -4700,6 +6098,11 @@ export class WorkflowService {
4700
6098
  externalSealedReview: {
4701
6099
  packetCommand: 'task step-review-packet',
4702
6100
  recordCommand: 'task step-review-record',
6101
+ reviewMode: 'ordinary',
6102
+ allowedReviewModes: ['ordinary', 'security'],
6103
+ packetReviewModeOption: '--review-mode',
6104
+ implicitSecurityScan: false,
6105
+ reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${strictStepReviewStep.id}:${strictStepReviewStep.evidence.commitSha}:ordinary`),
4703
6106
  reviewerMustUseSeparateChat: true,
4704
6107
  repositorySealRequired: true,
4705
6108
  },
@@ -4715,6 +6118,11 @@ export class WorkflowService {
4715
6118
  externalSealedReview: {
4716
6119
  packetCommand: 'task review-packet',
4717
6120
  recordCommand: 'task review-sealed-record',
6121
+ reviewMode: 'ordinary',
6122
+ allowedReviewModes: ['ordinary', 'security'],
6123
+ packetReviewModeOption: '--review-mode',
6124
+ implicitSecurityScan: false,
6125
+ reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${headCommit(repositoryRoot)}:ordinary`),
4718
6126
  reviewerMustUseSeparateChat: true,
4719
6127
  repositorySealRequired: true,
4720
6128
  },
@@ -4733,7 +6141,11 @@ export class WorkflowService {
4733
6141
  : {}),
4734
6142
  ...guardedCorrectiveOverlay,
4735
6143
  ...downstreamProofOverlay,
4736
- ...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
6144
+ ...(failedStepReviewRouting?.route === 'replan'
6145
+ && next.action === 'task run'
6146
+ && (!failedStep
6147
+ || deriveRollingCauseDecision(failedStepRemediations) === null
6148
+ || failedStepRemediations.at(-1)?.causeBindings?.length === 0)
4737
6149
  ? {
4738
6150
  action: 'task plan-set',
4739
6151
  blockedAction: 'task run',
@@ -4770,7 +6182,44 @@ export class WorkflowService {
4770
6182
  },
4771
6183
  }
4772
6184
  : {}),
6185
+ ...(planIntegrityDirtyCarryover?.applicable
6186
+ ? planIntegrityDirtyCarryover.eligible
6187
+ ? {
6188
+ planIntegrityDirtyCarryover: {
6189
+ state: 'validated',
6190
+ stepId: nextStepId,
6191
+ dependencyRecoveryCommit: planIntegrityDirtyCarryover.dependencyRecoveryCommit,
6192
+ dirtyFiles: planIntegrityDirtyCarryover.compatibility?.dirtyFiles ?? [],
6193
+ dirtyWorktreeHash: planIntegrityDirtyCarryover.compatibility?.dirtyWorktreeHash ?? null,
6194
+ scopeExtensions: planIntegrityDirtyCarryover.compatibility?.scopeExtensions ?? [],
6195
+ meaning: 'The current Step may adopt exactly these hash-bound check-support bytes on task run.',
6196
+ },
6197
+ }
6198
+ : {
6199
+ action: 'doctor',
6200
+ blockedAction: 'task run',
6201
+ planIntegrityDirtyCarryover: {
6202
+ state: 'blocked',
6203
+ stepId: nextStepId,
6204
+ blockers: planIntegrityDirtyCarryover.blockers,
6205
+ },
6206
+ }
6207
+ : {}),
4773
6208
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6209
+ ...(correctiveDerivedAuthority && correctiveDerivedActor
6210
+ ? {
6211
+ correctiveDerivedApproval: {
6212
+ action: transition === 'task.execution_authorize' ? 'task authorize' : 'task accept',
6213
+ actor: correctiveDerivedActor,
6214
+ humanApprovalRequired: false,
6215
+ milestoneId: correctiveDerivedAuthority.milestone.id,
6216
+ milestoneRevision: correctiveDerivedAuthority.milestone.revision,
6217
+ milestonePlanHash: correctiveDerivedAuthority.milestone.planHash,
6218
+ planRiskAuditEventId: correctiveDerivedAuthority.scopeChange.remediationEvidence?.planRiskAuditEventId ?? null,
6219
+ authority: 'current-corrective-derived-milestone-execution-authorization',
6220
+ },
6221
+ }
6222
+ : {}),
4774
6223
  };
4775
6224
  if (handoffPosture.state === 'pending') {
4776
6225
  return this.withTaskNavigationContracts(task, {
@@ -4829,7 +6278,20 @@ export class WorkflowService {
4829
6278
  return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
4830
6279
  }
4831
6280
  taskHasTerminalCorrectiveDecision(task) {
4832
- return this.taskTerminalCorrectivePosture(task) !== null;
6281
+ try {
6282
+ return this.taskTerminalCorrectivePosture(task) !== null;
6283
+ }
6284
+ catch (error) {
6285
+ if (error instanceof WorkflowError
6286
+ && error.code === 'TRANSITION_BLOCKED'
6287
+ && (error.details.decision === 'stop-escalate' || error.details.decision === 'split-required')) {
6288
+ // A blocking Plan Risk Audit is not a guarded-Step terminal decision, but it
6289
+ // equally cannot own an atomic delegated context refresh. Treat it as
6290
+ // ineligible here so standalone Knowledge reconciliation remains executable.
6291
+ return true;
6292
+ }
6293
+ throw error;
6294
+ }
4833
6295
  }
4834
6296
  taskTerminalCorrectivePosture(task) {
4835
6297
  if (!task.planHash)
@@ -4992,6 +6454,10 @@ export class WorkflowService {
4992
6454
  statePlanHash: task.planHash,
4993
6455
  });
4994
6456
  }
6457
+ if (authorization.authorizationMode === 'corrective-derived'
6458
+ && !this.currentCorrectiveDerivedTaskAuthority(task.projectId, task, authorization.actor)) {
6459
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective-derived Task authorization is no longer backed by the current Milestone authority.', { taskId: task.id, milestoneId: task.milestoneId, actor: authorization.actor });
6460
+ }
4995
6461
  const mechanicalFeasibility = authorization.mechanicalFeasibility;
4996
6462
  if (!mechanicalFeasibility)
4997
6463
  return authorization;
@@ -5063,7 +6529,218 @@ export class WorkflowService {
5063
6529
  },
5064
6530
  };
5065
6531
  }
5066
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null) {
6532
+ carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
6533
+ let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
6534
+ let dependencyCommitCount = 0;
6535
+ while (!task.systemCommits.includes(boundaryCommit)) {
6536
+ const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
6537
+ if (!dependencyCommit)
6538
+ return null;
6539
+ boundaryCommit = dependencyCommit.parentCommitSha;
6540
+ dependencyCommitCount += 1;
6541
+ }
6542
+ if (dependencyCommitCount === 0)
6543
+ return null;
6544
+ const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
6545
+ const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
6546
+ const compatibility = record.planIntegrity;
6547
+ if (!compatibility
6548
+ || record.commitSha !== boundaryCommit
6549
+ || !task.systemCommits.includes(record.commitSha)
6550
+ || compatibility.planHash !== task.planHash
6551
+ || canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
6552
+ || compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
6553
+ return false;
6554
+ const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
6555
+ if (!step || step.status !== 'failed')
6556
+ return false;
6557
+ const remediationEventIds = readRemediationEvents(this.store, task)
6558
+ .filter((event) => event.stepId === compatibility.stepId)
6559
+ .map((event) => event.eventId);
6560
+ if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
6561
+ return false;
6562
+ return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
6563
+ && event.stepId === compatibility.stepId
6564
+ && event.decision === 'replan-required')
6565
+ && readTaskC1Posture(this.store, task).state === 'none';
6566
+ });
6567
+ return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
6568
+ }
6569
+ postRebindCheckSupportCompatibility(snapshot, task) {
6570
+ if (task.status !== 'awaiting_execution_authorization'
6571
+ || !task.planHash
6572
+ || task.steps.some((step) => step.status === 'in_progress')
6573
+ || readTaskC1Posture(this.store, task).state !== 'none')
6574
+ return null;
6575
+ const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
6576
+ const candidates = readCorrectiveDecisionEvents(this.store, task).flatMap((event) => {
6577
+ const evidence = event.planIntegrityEvidence;
6578
+ if (event.planHash === task.planHash
6579
+ || event.decision !== 'continue-fix'
6580
+ || event.recoveryBasis !== 'plan-integrity'
6581
+ || evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
6582
+ || !task.systemCommits.includes(evidence.gitHead)
6583
+ || canonicalJsonStringify([...evidence.changedFiles].sort()) !== canonicalJsonStringify(dirtyFiles)) {
6584
+ return [];
6585
+ }
6586
+ const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
6587
+ if (!step
6588
+ || step.status !== 'planned'
6589
+ || evidence.scopeExtensions.some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
6590
+ || !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath)))
6591
+ return [];
6592
+ return [{
6593
+ stepId: step.id,
6594
+ planHash: task.planHash,
6595
+ conflictHash: evidence.conflictHash,
6596
+ dirtyFiles: [...dirtyFiles],
6597
+ dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, dirtyFiles),
6598
+ remediationEventIds: evidence.remediationEvents.map((binding) => binding.eventId),
6599
+ mode: 'post-rebind-scope-adoption',
6600
+ scopeExtensions: [...evidence.scopeExtensions],
6601
+ }];
6602
+ });
6603
+ return candidates.length === 1 ? candidates[0] : null;
6604
+ }
6605
+ carriedPostRebindCheckSupportDirtyCarryover(snapshot, task) {
6606
+ let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
6607
+ let dependencyCommitCount = 0;
6608
+ while (!task.systemCommits.includes(boundaryCommit)) {
6609
+ const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
6610
+ if (!dependencyCommit)
6611
+ return null;
6612
+ boundaryCommit = dependencyCommit.parentCommitSha;
6613
+ dependencyCommitCount += 1;
6614
+ }
6615
+ if (dependencyCommitCount === 0)
6616
+ return null;
6617
+ const assessment = this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, boundaryCommit);
6618
+ if (!assessment.eligible || !assessment.compatibility || !task.planHash)
6619
+ return null;
6620
+ return {
6621
+ ...assessment.compatibility,
6622
+ planHash: task.planHash,
6623
+ mode: 'post-rebind-dirty-carryover',
6624
+ };
6625
+ }
6626
+ assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, stepId, historyBoundary = headCommit(repositoryRoot)) {
6627
+ const relevantRecords = [...(task.dependencyProvenanceRecoveries ?? [])]
6628
+ .reverse()
6629
+ .filter((record) => {
6630
+ const compatibility = record.planIntegrity;
6631
+ return compatibility
6632
+ && ['post-rebind-scope-adoption', 'post-rebind-dirty-carryover'].includes(compatibility.mode ?? '')
6633
+ && (!stepId || compatibility.stepId === stepId);
6634
+ });
6635
+ if (relevantRecords.length === 0) {
6636
+ return {
6637
+ applicable: false,
6638
+ eligible: false,
6639
+ compatibility: null,
6640
+ dependencyRecoveryCommit: null,
6641
+ blockers: [],
6642
+ };
6643
+ }
6644
+ const blockers = [];
6645
+ const record = relevantRecords[0];
6646
+ const compatibility = record.planIntegrity;
6647
+ const step = task.steps.find((candidate) => candidate.id === compatibility.stepId) ?? null;
6648
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
6649
+ let dirtyWorktreeHash = null;
6650
+ try {
6651
+ dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
6652
+ }
6653
+ catch (error) {
6654
+ blockers.push(error instanceof Error ? error.message : String(error));
6655
+ }
6656
+ if (record.commitSha !== historyBoundary || !task.systemCommits.includes(record.commitSha)) {
6657
+ blockers.push('The latest check-support compatibility record must bind the exact registered history boundary.');
6658
+ }
6659
+ if (task.workspaceOwner !== 'local') {
6660
+ blockers.push('Check-support dirty carryover requires the recorded local Task workspace.');
6661
+ }
6662
+ if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
6663
+ blockers.push('Current branch must equal the recorded Task branch.');
6664
+ }
6665
+ if (!['ready', 'in_progress'].includes(task.status)) {
6666
+ blockers.push('The Task must retain an execution-authorized posture before dirty carryover.');
6667
+ }
6668
+ if (!task.planHash) {
6669
+ blockers.push('The Task must retain a current Plan hash.');
6670
+ }
6671
+ if (!step || step.status !== 'planned' || step.evidence !== null) {
6672
+ blockers.push(`The bound Step ${compatibility.stepId} must remain planned with no evidence.`);
6673
+ }
6674
+ if (task.steps.some((candidate) => candidate.status === 'in_progress')) {
6675
+ blockers.push('No Step may already be in progress before dirty carryover.');
6676
+ }
6677
+ if (canonicalJsonStringify(dirtyFiles)
6678
+ !== canonicalJsonStringify([...compatibility.dirtyFiles].sort())) {
6679
+ blockers.push('Dirty files changed after the registered check-support compatibility boundary.');
6680
+ }
6681
+ if (dirtyWorktreeHash !== compatibility.dirtyWorktreeHash) {
6682
+ blockers.push('Dirty file content changed after the registered check-support compatibility boundary.');
6683
+ }
6684
+ if (!compatibility.scopeExtensions?.length) {
6685
+ blockers.push('The compatibility record must retain at least one machine-derived scope extension.');
6686
+ }
6687
+ if (step) {
6688
+ if (dirtyFiles.some((dirtyPath) => !pathAllowed(dirtyPath, step.allowedWrites))) {
6689
+ blockers.push('The current Step must explicitly own every preserved dirty file.');
6690
+ }
6691
+ if ((compatibility.scopeExtensions ?? []).some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
6692
+ || !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath))) {
6693
+ blockers.push('The current Step must explicitly own every check-support scope extension as allowedWrites and expectedOutputs.');
6694
+ }
6695
+ }
6696
+ let reboundPlanHash = compatibility.planHash;
6697
+ for (const rebind of task.knowledgeRebinds) {
6698
+ if (rebind.previousPlanHash === reboundPlanHash)
6699
+ reboundPlanHash = rebind.reboundPlanHash;
6700
+ }
6701
+ if (!task.planHash || reboundPlanHash !== task.planHash) {
6702
+ blockers.push('The current Plan must equal the compatibility Plan or its exact Knowledge-rebind descendant.');
6703
+ }
6704
+ const matchingDecisions = readCorrectiveDecisionEvents(this.store, task).filter((event) => {
6705
+ const evidence = event.planIntegrityEvidence;
6706
+ return event.stepId === compatibility.stepId
6707
+ && event.decision === 'continue-fix'
6708
+ && event.recoveryBasis === 'plan-integrity'
6709
+ && evidence?.kind === 'check-support-anchor-outside-allowed-writes'
6710
+ && evidence.conflictHash === compatibility.conflictHash
6711
+ && canonicalJsonStringify([...evidence.changedFiles].sort())
6712
+ === canonicalJsonStringify([...compatibility.dirtyFiles].sort())
6713
+ && canonicalJsonStringify(evidence.remediationEvents.map((binding) => binding.eventId))
6714
+ === canonicalJsonStringify(compatibility.remediationEventIds)
6715
+ && canonicalJsonStringify([...evidence.scopeExtensions].sort())
6716
+ === canonicalJsonStringify([...(compatibility.scopeExtensions ?? [])].sort());
6717
+ });
6718
+ if (matchingDecisions.length !== 1) {
6719
+ blockers.push('Exactly one validated historical check-support decision must bind the preserved dirty set and scope.');
6720
+ }
6721
+ try {
6722
+ validateTaskHistory(repositoryRoot, task, historyBoundary);
6723
+ }
6724
+ catch (error) {
6725
+ blockers.push(error instanceof Error ? error.message : String(error));
6726
+ }
6727
+ return {
6728
+ applicable: true,
6729
+ eligible: blockers.length === 0,
6730
+ compatibility: blockers.length === 0
6731
+ ? {
6732
+ ...structuredClone(compatibility),
6733
+ planHash: task.planHash,
6734
+ dirtyFiles,
6735
+ dirtyWorktreeHash: dirtyWorktreeHash,
6736
+ mode: 'post-rebind-dirty-carryover',
6737
+ }
6738
+ : null,
6739
+ dependencyRecoveryCommit: record.commitSha,
6740
+ blockers,
6741
+ };
6742
+ }
6743
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
5067
6744
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
5068
6745
  const now = this.now().getTime();
5069
6746
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
@@ -5083,7 +6760,20 @@ export class WorkflowService {
5083
6760
  currentBranch: versions.currentBranch,
5084
6761
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
5085
6762
  },
5086
- }, activeDownstreamProof, downstreamProofReplan);
6763
+ }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
6764
+ }
6765
+ inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
6766
+ const now = this.now().getTime();
6767
+ return inspectHistoricalStepProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
6768
+ activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
6769
+ staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
6770
+ pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
6771
+ + snapshot.observation.pendingMilestoneTransactions.length
6772
+ + snapshot.observation.pendingTaskTransactions.length,
6773
+ corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
6774
+ coreOperationCount: snapshot.observation.coreTaskOperations.length,
6775
+ verifiedHistoricalStepBindings: verifiedHistoricalStepReviewBindings(this.store, task),
6776
+ }, options);
5087
6777
  }
5088
6778
  withTaskNavigationContracts(task, next) {
5089
6779
  const action = typeof next.action === 'string' ? next.action : null;
@@ -5092,10 +6782,9 @@ export class WorkflowService {
5092
6782
  const policy = gitMutationPolicyForAction(task, action);
5093
6783
  const c1Posture = readTaskC1Posture(this.store, task);
5094
6784
  const claimedC1Posture = c1Posture.state === 'claimed';
5095
- const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
5096
- const activeLease = writerTokenContract
5097
- ? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
5098
- : null;
6785
+ const activeLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
6786
+ const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture, activeLease !== null);
6787
+ const activeLeaseUsable = activeLease ? !toPublicWriterLease(activeLease, this.now()).stale : false;
5099
6788
  const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
5100
6789
  const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
5101
6790
  ? {
@@ -5107,9 +6796,24 @@ export class WorkflowService {
5107
6796
  acquiresLeaseWhenAbsent: true,
5108
6797
  }
5109
6798
  : null;
6799
+ const credentialBlocksAction = Boolean(writerCredentialRecovery
6800
+ && writerTokenContract
6801
+ && (activeLease === null || !activeLeaseUsable));
5110
6802
  return {
5111
6803
  ...next,
5112
6804
  ...(policy ? { gitMutationPolicy: policy } : {}),
6805
+ ...(credentialBlocksAction
6806
+ ? {
6807
+ action: 'task writer-credential-replace',
6808
+ blockedAction: action,
6809
+ continuation: {
6810
+ kind: 'failed-step-pause',
6811
+ preservesTask: true,
6812
+ preservesWorktree: true,
6813
+ resumesWithFreshNext: true,
6814
+ },
6815
+ }
6816
+ : {}),
5113
6817
  ...(writerTokenContract ? { writerTokenContract } : {}),
5114
6818
  ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
5115
6819
  };
@@ -5521,16 +7225,20 @@ function assertTaskBranch(repositoryRoot, task) {
5521
7225
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected Task branch ${task.taskBranch}, found ${branch ?? 'detached HEAD'}.`);
5522
7226
  }
5523
7227
  }
5524
- function preserveCompletedStep(task, next) {
5525
- const previous = task.steps.find((candidate) => candidate.id === next.id && candidate.status === 'completed');
5526
- if (!previous)
5527
- return null;
5528
- const comparable = ({ status: _status, evidence: _evidence, ...definition }) => definition;
5529
- return JSON.stringify(comparable(previous)) === JSON.stringify(next) ? previous : null;
5530
- }
5531
7228
  function stripStepRuntime(step) {
5532
- const { status: _status, evidence: _evidence, ...definition } = step;
5533
- return definition;
7229
+ return normalizePlanStep(step);
7230
+ }
7231
+ function normalizePlanStep(step) {
7232
+ return normalizePlanStepDefinition(step);
7233
+ }
7234
+ function parseWorkflowBetaVersion(value) {
7235
+ const match = value?.match(/^2[.]0[.]0-beta[.]([0-9]+)(?:[.]([0-9]+))?$/);
7236
+ if (!match)
7237
+ return null;
7238
+ return {
7239
+ train: Number(match[1]),
7240
+ patch: match[2] === undefined ? null : Number(match[2]),
7241
+ };
5534
7242
  }
5535
7243
  function validateReviewInput(review) {
5536
7244
  if (!review.reviewer.trim())
@@ -5904,7 +7612,32 @@ function gitMutationPolicyForAction(task, action) {
5904
7612
  return null;
5905
7613
  }
5906
7614
  }
5907
- function writerTokenContractForAction(action, claimedC1Posture = false) {
7615
+ function taskWithEffectiveStepAllowedWrites(store, task, stepId) {
7616
+ const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
7617
+ return {
7618
+ ...task,
7619
+ steps: task.steps.map((step) => step.id === stepId ? { ...step, allowedWrites } : step),
7620
+ };
7621
+ }
7622
+ function reviewScopeAugmentations(store, task, stepId) {
7623
+ const step = requireStep(task, stepId);
7624
+ const paths = effectiveStepAllowedWrites(store, task, stepId)
7625
+ .filter((candidate) => !pathAllowed(candidate, step.allowedWrites));
7626
+ return paths.length === 0 ? [] : [{
7627
+ stepId,
7628
+ paths,
7629
+ authority: 'machine-derived-check-support',
7630
+ }];
7631
+ }
7632
+ function reviewCauseBindings(event) {
7633
+ return (event.remediationCauses ?? []).map((cause) => ({
7634
+ authority: 'external-reviewer',
7635
+ causeId: cause.causeId,
7636
+ causeFingerprint: cause.causeFingerprint,
7637
+ planConflict: cause.planConflict,
7638
+ }));
7639
+ }
7640
+ function writerTokenContractForAction(action, claimedC1Posture = false, existingLease = false) {
5908
7641
  const alwaysLeaseBound = [
5909
7642
  'task step-complete',
5910
7643
  'task downstream-proof-recover',
@@ -5930,16 +7663,19 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
5930
7663
  'task handback',
5931
7664
  'task handback-create',
5932
7665
  ];
5933
- if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
7666
+ if (!alwaysLeaseBound.includes(action)
7667
+ && !(claimedC1Posture && claimedC1LeaseBound.includes(action))
7668
+ && !(existingLease && ['task run', 'task step-review', 'task step-review-record'].includes(action))) {
5934
7669
  return null;
5935
7670
  }
5936
7671
  return {
5937
7672
  required: true,
5938
- option: '--writer-token',
5939
- source: 'active-writer-lease',
5940
- sensitive: true,
5941
- renewalDisposition: 'reuse-presented-token',
5942
- tokenReissuedOnRenewal: false,
7673
+ option: '--writer-credential-ref',
7674
+ source: 'local-credential-vault',
7675
+ transport: 'local-vault-v1',
7676
+ secretInArgv: false,
7677
+ secretInOutput: false,
7678
+ renewalDisposition: 'reuse-reference',
5943
7679
  };
5944
7680
  }
5945
7681
  function observationPreflightBlockers(observation) {