codex-workflow-v2 2.0.0-beta.12.9 → 2.0.0-beta.13.1

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 (117) hide show
  1. package/README.md +58 -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.d.ts +1 -0
  22. package/dist/src/alpha6/plan-risk.js +36 -7
  23. package/dist/src/alpha6/plan-risk.js.map +1 -1
  24. package/dist/src/alpha6/remediation-cause.d.ts +13 -0
  25. package/dist/src/alpha6/remediation-cause.js +77 -0
  26. package/dist/src/alpha6/remediation-cause.js.map +1 -0
  27. package/dist/src/alpha6/remediation.d.ts +15 -4
  28. package/dist/src/alpha6/remediation.js +430 -67
  29. package/dist/src/alpha6/remediation.js.map +1 -1
  30. package/dist/src/alpha6/review.js +34 -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 +216 -258
  38. package/dist/src/cli.js.map +1 -1
  39. package/dist/src/contracts.d.ts +168 -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 +221 -34
  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/index.d.ts +2 -0
  56. package/dist/src/index.js +2 -0
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
  59. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
  60. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
  61. package/dist/src/lifecycle/corrective-replan.js +1 -1
  62. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  63. package/dist/src/lifecycle/semantic-registry.js +1 -1
  64. package/dist/src/observation.js +1 -2
  65. package/dist/src/observation.js.map +1 -1
  66. package/dist/src/observed-routes.d.ts +114 -0
  67. package/dist/src/observed-routes.js +545 -0
  68. package/dist/src/observed-routes.js.map +1 -0
  69. package/dist/src/operational-contract.d.ts +26 -0
  70. package/dist/src/operational-contract.js +94 -0
  71. package/dist/src/operational-contract.js.map +1 -0
  72. package/dist/src/reviewer.d.ts +218 -8
  73. package/dist/src/reviewer.js +927 -25
  74. package/dist/src/reviewer.js.map +1 -1
  75. package/dist/src/state/corrective-replan-executor.js +21 -16
  76. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  77. package/dist/src/state/corrective-replan-public.js +35 -10
  78. package/dist/src/state/corrective-replan-public.js.map +1 -1
  79. package/dist/src/state/corrective-replan-transaction.js +12 -4
  80. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  81. package/dist/src/state/corrective-yield-transaction.js +11 -2
  82. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  83. package/dist/src/state/lock.d.ts +1 -0
  84. package/dist/src/state/lock.js +24 -0
  85. package/dist/src/state/lock.js.map +1 -1
  86. package/dist/src/version.d.ts +1 -1
  87. package/dist/src/version.js +1 -1
  88. package/dist/src/workflow.d.ts +65 -4
  89. package/dist/src/workflow.js +1710 -155
  90. package/dist/src/workflow.js.map +1 -1
  91. package/docs/autonomy-guardrails.md +41 -14
  92. package/docs/delegated-approval.md +6 -1
  93. package/docs/development-flow.md +42 -12
  94. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  95. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  96. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  97. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +189 -22
  98. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +154 -26
  99. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +333 -52
  100. package/docs/problem-briefs/01-pre-implementation-integrity.md +11 -7
  101. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +13 -6
  102. package/docs/problem-briefs/README.md +11 -11
  103. package/docs/release.md +92 -9
  104. package/docs/updating-existing-project.md +227 -0
  105. package/docs/validation-report.md +177 -99
  106. package/package.json +12 -3
  107. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +42 -1
  108. package/schemas/authorization-event.schema.json +10 -3
  109. package/schemas/corrective-decision-event.schema.json +164 -9
  110. package/schemas/corrective-replan-credentials.private.schema.json +4 -2
  111. package/schemas/milestone-scope-change-event.schema.json +25 -1
  112. package/schemas/plan-risk-audit-event.schema.json +1 -0
  113. package/schemas/remediation-event.schema.json +17 -1
  114. package/schemas/review-result.schema.json +1 -0
  115. package/schemas/step-review-event.schema.json +15 -0
  116. package/schemas/task.schema.json +61 -1
  117. package/schemas/transition-definition.schema.json +8 -0
@@ -2,23 +2,26 @@ 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
6
  import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
7
7
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
8
8
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
9
9
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
10
+ import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
10
11
  import { pathAllowed, validatePlan } from './domain/validation.js';
11
12
  import { WorkflowError } from './errors.js';
13
+ import { withObservedRouteMetadata } from './observed-routes.js';
12
14
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
13
15
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
14
16
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
15
17
  import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
16
18
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
17
19
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
18
- 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';
19
21
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
22
+ import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
20
23
  import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
21
- 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';
22
25
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
23
26
  import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
24
27
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
@@ -28,7 +31,7 @@ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
28
31
  import { ensureDirectory } from './fs-utils.js';
29
32
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
30
33
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
31
- import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
34
+ import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
32
35
  import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
33
36
  import { FileStateStore } from './state/store.js';
34
37
  import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
@@ -601,6 +604,353 @@ export class WorkflowService {
601
604
  const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
602
605
  return { discovery: savedDiscovery, task: savedTask, taskRoot };
603
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
+ }
604
954
  replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
605
955
  const { identity, locks } = this.#mutationContext(repository);
606
956
  const oldTask = this.store.readTask(identity.projectId, oldTaskId);
@@ -1266,11 +1616,14 @@ export class WorkflowService {
1266
1616
  || (event.decision === 'stop-escalate' && !overriddenStopDecisionEventIds.has(event.eventId)));
1267
1617
  const downstreamReplanRecovered = readDownstreamProofReplanRecoveries(this.store, task)
1268
1618
  .some((event) => event.resultTaskRevision === task.revision && event.reboundPlanHash === task.planHash);
1269
- 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)
1270
1623
  && !downstreamReplanRecovered
1271
1624
  ? readCurrentPlanRiskAudit(this.store, task)
1272
1625
  : null;
1273
- const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
1626
+ const classifiedCorrectivePosture = currentPlanRiskAudit
1274
1627
  ? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
1275
1628
  : null;
1276
1629
  const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
@@ -1300,7 +1653,10 @@ export class WorkflowService {
1300
1653
  : 'Use task corrective-replan-prepare, confirm, and execute.',
1301
1654
  });
1302
1655
  }
1303
- 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)) {
1304
1660
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
1305
1661
  }
1306
1662
  if (currentHardBlockingDecisions.length > 0) {
@@ -1370,7 +1726,8 @@ export class WorkflowService {
1370
1726
  throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
1371
1727
  }
1372
1728
  }
1373
- if (task.planHash && task.status === 'needs_fix') {
1729
+ if (task.planHash && (task.status === 'needs_fix'
1730
+ || correctiveReplanPosture?.posture === 'replan-required')) {
1374
1731
  if (task.planObjective !== null && plan.objective !== task.planObjective) {
1375
1732
  throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation may change the implementation Plan but not the Task objective.', {
1376
1733
  taskId: task.id,
@@ -1392,14 +1749,16 @@ export class WorkflowService {
1392
1749
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
1393
1750
  ? { ...authorization, decision: 'superseded' }
1394
1751
  : authorization);
1395
- const completedIds = task.steps
1396
- .filter((candidate) => candidate.status === 'completed')
1397
- .map((candidate) => candidate.id);
1398
- const plannedSteps = plan.steps.map((step) => preserveCompletedStep(task, step) ?? ({
1399
- ...step,
1400
- status: dependenciesComplete(step, completedIds) ? 'planned' : 'blocked',
1401
- evidence: null,
1402
- }));
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
+ });
1403
1762
  const invalidatedStepCommits = [...new Set([
1404
1763
  ...(task.invalidatedStepCommits ?? []),
1405
1764
  ...omittedStepEvidenceCommits(task, plannedSteps),
@@ -1410,15 +1769,62 @@ export class WorkflowService {
1410
1769
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
1411
1770
  }
1412
1771
  if (correctiveReplanPosture?.posture === 'replan-required') {
1413
- const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
1414
- if (!executionEvaluation.allowed) {
1415
- throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
1416
- transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1417
- evaluationPhase: executionEvaluation.phase,
1418
- blockingGuardIds: executionEvaluation.blockingGuardIds,
1419
- pendingGuardIds: executionEvaluation.pendingGuardIds,
1420
- guardResults: executionEvaluation.guardResults,
1421
- });
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
+ }
1422
1828
  }
1423
1829
  }
1424
1830
  if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
@@ -1441,6 +1847,8 @@ export class WorkflowService {
1441
1847
  knowledgeMapRevision: plan.knowledgeMapRevision,
1442
1848
  knowledgeMapHash: plan.knowledgeMapHash,
1443
1849
  graphUse: plan.graphUse,
1850
+ productionEdges: structuredClone(plan.productionEdges ?? []),
1851
+ requiresProductionEdges: plan.requiresProductionEdges === true,
1444
1852
  steps: plannedSteps,
1445
1853
  invalidatedStepCommits,
1446
1854
  authorizations,
@@ -1522,6 +1930,12 @@ export class WorkflowService {
1522
1930
  knowledgeMapHash,
1523
1931
  graphUse: task.graphUse,
1524
1932
  steps: task.steps.map(stripStepRuntime),
1933
+ ...(Object.hasOwn(task, 'productionEdges')
1934
+ ? { productionEdges: structuredClone(task.productionEdges ?? []) }
1935
+ : {}),
1936
+ ...(Object.hasOwn(task, 'requiresProductionEdges')
1937
+ ? { requiresProductionEdges: task.requiresProductionEdges === true }
1938
+ : {}),
1525
1939
  };
1526
1940
  validatePlan(reboundPlan);
1527
1941
  this.validateKnowledgeTargets(knowledgeMap, reboundPlan);
@@ -1678,7 +2092,12 @@ export class WorkflowService {
1678
2092
  requiredAction: 'task plan-set',
1679
2093
  });
1680
2094
  }
1681
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
2095
+ const correctiveAuthority = delegationGrantId === null
2096
+ ? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
2097
+ : null;
2098
+ const delegation = correctiveAuthority
2099
+ ? null
2100
+ : this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
1682
2101
  const event = {
1683
2102
  kind: 'execution',
1684
2103
  decision: 'approved',
@@ -1690,7 +2109,11 @@ export class WorkflowService {
1690
2109
  evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
1691
2110
  headCommit: mechanicalFeasibility.evaluatedHead,
1692
2111
  reason,
1693
- authorizationMode: delegation ? 'delegated' : 'human',
2112
+ authorizationMode: correctiveAuthority
2113
+ ? 'corrective-derived'
2114
+ : delegation
2115
+ ? 'delegated'
2116
+ : 'human',
1694
2117
  ...(delegation ? { delegation } : {}),
1695
2118
  mechanicalFeasibility,
1696
2119
  };
@@ -1875,7 +2298,7 @@ export class WorkflowService {
1875
2298
  const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1876
2299
  const action = String(navigation.action);
1877
2300
  const currentLease = context.locks.inspect(taskId);
1878
- const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
2301
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
1879
2302
  const requiredActor = writerTokenContract
1880
2303
  ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1881
2304
  : null;
@@ -1889,9 +2312,7 @@ export class WorkflowService {
1889
2312
  nextAction: action,
1890
2313
  });
1891
2314
  }
1892
- const lease = currentLease
1893
- ? context.locks.replaceCredential(taskId, normalizedActor)
1894
- : context.locks.acquire(taskId, normalizedActor);
2315
+ const lease = context.locks.replaceOrRecoverCredential(taskId, normalizedActor);
1895
2316
  if (posture.state === 'claimed') {
1896
2317
  appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1897
2318
  }
@@ -2010,6 +2431,7 @@ export class WorkflowService {
2010
2431
  }
2011
2432
  assertTaskBranch(context.identity.repositoryRoot, task);
2012
2433
  const step = requireStep(task, stepId);
2434
+ const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2013
2435
  const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
2014
2436
  if (!['planned', 'failed'].includes(step.status)) {
2015
2437
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
@@ -2044,18 +2466,25 @@ export class WorkflowService {
2044
2466
  const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
2045
2467
  ? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
2046
2468
  : null;
2469
+ const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2470
+ ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2471
+ : null;
2047
2472
  if (step.status === 'planned'
2048
2473
  && dirty.length > 0
2049
2474
  && !resumesCorrectiveReplan
2050
- && !downstreamProofCarryover?.eligible) {
2475
+ && !downstreamProofCarryover?.eligible
2476
+ && !planIntegrityDirtyCarryover?.eligible) {
2051
2477
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2052
2478
  changedFiles: dirty,
2053
2479
  ...(downstreamProofCarryover?.applicable
2054
2480
  ? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
2055
2481
  : {}),
2482
+ ...(planIntegrityDirtyCarryover?.applicable
2483
+ ? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
2484
+ : {}),
2056
2485
  });
2057
2486
  }
2058
- const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
2487
+ const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
2059
2488
  if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
2060
2489
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
2061
2490
  changedFiles: dirtyOutsideStep,
@@ -2102,7 +2531,9 @@ export class WorkflowService {
2102
2531
  return context.locks.acquire(taskId, actor.trim() || 'worker');
2103
2532
  })();
2104
2533
  }
2105
- const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
2534
+ const steps = task.steps.map((candidate) => candidate.id === stepId
2535
+ ? { ...candidate, status: 'in_progress', evidence: null, failurePause: null }
2536
+ : candidate);
2106
2537
  let saved;
2107
2538
  try {
2108
2539
  saved = this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
@@ -2126,7 +2557,7 @@ export class WorkflowService {
2126
2557
  path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
2127
2558
  ],
2128
2559
  allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
2129
- allowedWrites: step.allowedWrites,
2560
+ allowedWrites,
2130
2561
  commands: step.checks,
2131
2562
  forbiddenScope: [
2132
2563
  ...step.forbiddenScope,
@@ -2245,6 +2676,7 @@ export class WorkflowService {
2245
2676
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
2246
2677
  assertTaskBranch(context.identity.repositoryRoot, task);
2247
2678
  const step = requireStep(task, stepId);
2679
+ const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2248
2680
  if (task.status !== 'in_progress' || step.status !== 'in_progress') {
2249
2681
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
2250
2682
  }
@@ -2277,18 +2709,114 @@ export class WorkflowService {
2277
2709
  const checks = runChecks(context.identity.repositoryRoot, step.checks);
2278
2710
  const failed = checks.filter((check) => !check.passed);
2279
2711
  if (failed.length > 0) {
2280
- if (reviewRequired && planRiskAudit) {
2281
- appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
2712
+ const now = this.now();
2713
+ const normalizedFailures = normalizeMechanicalFailures(failed, context.identity.repositoryRoot);
2714
+ // Bind useful uncommitted work to this failure without treating it as
2715
+ // completed-Step authority.
2716
+ const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
2717
+ const failurePause = {
2718
+ recordedAt: now.toISOString(),
2719
+ dirtyFiles,
2720
+ dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
2721
+ checksHash: sha256Hex(canonicalJsonStringify(checks)),
2722
+ failedChecks: normalizedFailures,
2723
+ };
2724
+ const preparedRemediation = reviewRequired && planRiskAudit
2725
+ ? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
2282
2726
  completionCommit: headCommit(context.identity.repositoryRoot),
2283
2727
  checksHash: sha256Hex(canonicalJsonStringify(checks)),
2284
2728
  reviewEventHash: null,
2285
2729
  reviewerAttestationHash: null,
2286
- }, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
2287
- }
2288
- const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
2289
- return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
2730
+ executionTaskRevision: task.revision,
2731
+ dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
2732
+ }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
2733
+ : null;
2734
+ const steps = task.steps.map((candidate) => candidate.id === stepId
2735
+ ? { ...candidate, status: 'failed', evidence: null, failurePause }
2736
+ : candidate);
2737
+ const paused = {
2738
+ ...task,
2739
+ revision: task.revision + 1,
2740
+ updatedAt: now.toISOString(),
2741
+ status: 'needs_fix',
2742
+ steps,
2743
+ blockReason: `${stepId} checks failed.`,
2744
+ };
2745
+ const stepHistory = readRemediationEvents(this.store, task)
2746
+ .filter((event) => event.stepId === stepId);
2747
+ const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
2748
+ ? stepHistory
2749
+ : [...stepHistory, preparedRemediation.event];
2750
+ const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
2751
+ const c1Posture = readTaskC1Posture(this.store, task);
2752
+ const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
2753
+ ? prepareTaskCorrectiveYield(this.store, paused, {
2754
+ actor: c1Posture.claimant,
2755
+ writerToken: lease.token,
2756
+ reason: `Yield execution authority after repeated mechanical cause ${preparedRemediation.event.eventId}.`,
2757
+ eventId: `HOF-${sha256Hex([
2758
+ 'checks-failed-root-cause-replan-pause-v1',
2759
+ task.id,
2760
+ String(task.revision),
2761
+ preparedRemediation.event.eventHash,
2762
+ c1Posture.claimant,
2763
+ ].join('\0'))}`,
2764
+ }, now)
2765
+ : null;
2766
+ const projectRoot = this.store.projectRoot(task.projectId);
2767
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2768
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2769
+ const failureHead = headCommit(context.identity.repositoryRoot);
2770
+ applyProjectTransaction({
2771
+ projectRoot,
2772
+ projectId: task.projectId,
2773
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2774
+ kind: rollingCauseDecision === 'root-cause-replan'
2775
+ ? 'failed-step-root-cause-replan-pause'
2776
+ : 'failed-step-continuation',
2777
+ now,
2778
+ targets: [
2779
+ ...(preparedRemediation?.postimage
2780
+ ? [{
2781
+ path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
2782
+ content: preparedRemediation.postimage,
2783
+ }]
2784
+ : []),
2785
+ ...(preparedYield
2786
+ ? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
2787
+ : []),
2788
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
2789
+ { path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null },
2790
+ ],
2791
+ validate: () => {
2792
+ const saved = this.store.readTask(task.projectId, task.id);
2793
+ const savedStep = requireStep(saved, stepId);
2794
+ const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
2795
+ const remediationHash = preparedRemediation
2796
+ ? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
2797
+ : null;
2798
+ if (saved.revision !== paused.revision
2799
+ || saved.status !== 'needs_fix'
2800
+ || savedStep.status !== 'failed'
2801
+ || savedStep.failurePause?.dirtyWorktreeHash !== failurePause.dirtyWorktreeHash
2802
+ || savedStep.failurePause?.checksHash !== failurePause.checksHash
2803
+ || remediationHash !== (preparedRemediation?.event.eventHash ?? null)
2804
+ || context.locks.inspect(task.id) !== null
2805
+ || (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
2806
+ || headCommit(context.identity.repositoryRoot) !== failureHead
2807
+ || JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
2808
+ || hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== failurePause.dirtyWorktreeHash) {
2809
+ throw new WorkflowError('STATE_CORRUPT', 'Failed-Step continuation transaction readback failed.', {
2810
+ taskId,
2811
+ stepId,
2812
+ });
2813
+ }
2814
+ validateTaskHistory(context.identity.repositoryRoot, saved);
2815
+ },
2816
+ });
2817
+ return this.store.readTask(task.projectId, task.id);
2290
2818
  }
2291
- const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, step.allowedWrites);
2819
+ const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, allowedWrites);
2292
2820
  const evidence = {
2293
2821
  recordedAt: this.now().toISOString(),
2294
2822
  commitSha: committed.commitSha,
@@ -2327,7 +2855,7 @@ export class WorkflowService {
2327
2855
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
2328
2856
  const task = this.store.readTask(context.identity.projectId, taskId);
2329
2857
  assertExpectedRevision(task, expectedRevision);
2330
- this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2858
+ const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
2331
2859
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
2332
2860
  if (task.status !== 'in_progress' && task.status !== 'blocked') {
2333
2861
  throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
@@ -2398,18 +2926,14 @@ export class WorkflowService {
2398
2926
  resolution: currentCycle.resolution,
2399
2927
  });
2400
2928
  }
2401
- appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2402
- completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
2403
- checksHash: currentCycle.reviewRecordedEvent.checksHash,
2404
- reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
2405
- reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
2406
- }, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
2929
+ return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, currentCycle.reviewRecordedEvent, currentCycle.reviewerAttestationEvent.eventHash, planRiskAudit, undefined, reviewLease);
2407
2930
  }
2408
2931
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
2409
2932
  }
2410
2933
  ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2411
2934
  const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2412
- const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
2935
+ const reviewTask = taskWithEffectiveStepAllowedWrites(this.store, task, stepId);
2936
+ const launch = launchStrictStepReviewer(context.identity.repositoryRoot, reviewTask, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner, reviewScopeAugmentations(this.store, task, stepId));
2413
2937
  const recorded = recordStrictStepReview(this.store, task, stepId, {
2414
2938
  review: launch.review,
2415
2939
  reviewerOutputHash: launch.reviewerOutputHash,
@@ -2426,16 +2950,11 @@ export class WorkflowService {
2426
2950
  resolution: recorded.resolution,
2427
2951
  });
2428
2952
  }
2429
- appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2430
- completionCommit: reviewRecordedEvent.completionCommit,
2431
- checksHash: reviewRecordedEvent.checksHash,
2432
- reviewEventHash: reviewRecordedEvent.eventHash,
2433
- reviewerAttestationHash: reviewerAttestationEvent.eventHash,
2434
- }, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
2953
+ return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewRecordedEvent, reviewerAttestationEvent.eventHash, planRiskAudit, launch.review.summary, reviewLease);
2435
2954
  }
2436
2955
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
2437
2956
  }
2438
- prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2957
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision, reviewMode = 'ordinary') {
2439
2958
  const identity = resolveRepositoryIdentity(repository);
2440
2959
  const task = this.store.readTask(identity.projectId, taskId);
2441
2960
  assertExpectedRevision(task, expectedRevision);
@@ -2455,15 +2974,18 @@ export class WorkflowService {
2455
2974
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2456
2975
  }
2457
2976
  const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2458
- return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
2977
+ 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);
2459
2978
  }
2460
2979
  recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2461
2980
  const context = this.#mutationContext(repository);
2462
2981
  const task = this.store.readTask(context.identity.projectId, taskId);
2463
2982
  assertExpectedRevision(task, expectedRevision);
2464
- this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2983
+ const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
2465
2984
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2466
- const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2985
+ if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
2986
+ throw new WorkflowError('INVALID_ARGUMENT', 'External Step Review requires reviewMode ordinary or security.');
2987
+ }
2988
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision, input.reviewMode);
2467
2989
  let review;
2468
2990
  try {
2469
2991
  review = validateExternalStrictStepReviewInput(input, packet);
@@ -2498,23 +3020,131 @@ export class WorkflowService {
2498
3020
  isolationResult: 'verified',
2499
3021
  }, this.now());
2500
3022
  if (recorded.resolution === 'failed') {
2501
- appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2502
- completionCommit: recorded.reviewRecordedEvent.completionCommit,
2503
- checksHash: recorded.reviewRecordedEvent.checksHash,
2504
- reviewEventHash: recorded.reviewRecordedEvent.eventHash,
2505
- reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
2506
- }, readCurrentPlanRiskAudit(this.store, task), this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
3023
+ const saved = this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, recorded.reviewRecordedEvent, recorded.reviewerAttestationEvent.eventHash, readCurrentPlanRiskAudit(this.store, task), review.summary, reviewLease);
3024
+ return {
3025
+ mode: 'external-sealed',
3026
+ reviewMode: packet.reviewMode,
3027
+ packetHash: packet.packetHash,
3028
+ repositorySealHash: packet.repositorySealHash,
3029
+ reviewerThreadId: input.reviewerThreadId,
3030
+ task: saved,
3031
+ };
2507
3032
  }
2508
3033
  const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2509
3034
  return {
2510
3035
  mode: 'external-sealed',
3036
+ reviewMode: packet.reviewMode,
2511
3037
  packetHash: packet.packetHash,
2512
3038
  repositorySealHash: packet.repositorySealHash,
2513
3039
  reviewerThreadId: input.reviewerThreadId,
2514
3040
  task: saved,
2515
3041
  };
2516
3042
  }
2517
- prepareExternalTaskReview(repository, taskId, expectedRevision) {
3043
+ finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewEvent, reviewerAttestationHash, planRiskAudit, summary, reviewLease) {
3044
+ const now = this.now();
3045
+ const preparedRemediation = prepareRemediationEvent(this.store, task, stepId, 'review-failed', {
3046
+ completionCommit: reviewEvent.completionCommit,
3047
+ checksHash: reviewEvent.checksHash,
3048
+ reviewEventHash: reviewEvent.eventHash,
3049
+ reviewerAttestationHash,
3050
+ }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), reviewCauseBindings(reviewEvent));
3051
+ const stepHistory = readRemediationEvents(this.store, task)
3052
+ .filter((event) => event.stepId === stepId);
3053
+ const resultingHistory = preparedRemediation.postimage === null
3054
+ ? stepHistory
3055
+ : [...stepHistory, preparedRemediation.event];
3056
+ const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3057
+ const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
3058
+ const paused = {
3059
+ ...resolved,
3060
+ revision: task.revision + 1,
3061
+ updatedAt: now.toISOString(),
3062
+ };
3063
+ const projectRoot = this.store.projectRoot(task.projectId);
3064
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
3065
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
3066
+ const failureHead = headCommit(context.identity.repositoryRoot);
3067
+ const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3068
+ const dirtyWorktreeHash = hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles);
3069
+ const c1Posture = readTaskC1Posture(this.store, task);
3070
+ if (rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && !reviewLease) {
3071
+ throw new WorkflowError('STATE_CONFLICT', 'Root-cause replan pause requires the claimed writer authority it will release.', {
3072
+ taskId: task.id,
3073
+ stepId,
3074
+ });
3075
+ }
3076
+ const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && reviewLease
3077
+ ? prepareTaskCorrectiveYield(this.store, paused, {
3078
+ actor: c1Posture.claimant,
3079
+ writerToken: reviewLease.token,
3080
+ reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
3081
+ eventId: `HOF-${sha256Hex([
3082
+ 'strict-review-root-cause-replan-pause-v1',
3083
+ task.id,
3084
+ String(task.revision),
3085
+ preparedRemediation.event.eventHash,
3086
+ c1Posture.claimant,
3087
+ ].join('\0'))}`,
3088
+ }, now)
3089
+ : null;
3090
+ if (reviewLease) {
3091
+ const currentLease = context.locks.inspect(task.id);
3092
+ if (!currentLease || currentLease.token !== reviewLease.token || currentLease.owner !== reviewLease.owner) {
3093
+ throw new WorkflowError('STATE_CONFLICT', 'Strict Step Review writer authority changed before the failure pause could be recorded.', {
3094
+ taskId: task.id,
3095
+ stepId,
3096
+ });
3097
+ }
3098
+ }
3099
+ applyProjectTransaction({
3100
+ projectRoot,
3101
+ projectId: task.projectId,
3102
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
3103
+ kind: rollingCauseDecision === 'root-cause-replan'
3104
+ ? 'strict-review-root-cause-replan-pause'
3105
+ : 'strict-review-failed-step-pause',
3106
+ now,
3107
+ targets: [
3108
+ ...(preparedRemediation.postimage
3109
+ ? [{
3110
+ path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
3111
+ content: preparedRemediation.postimage,
3112
+ }]
3113
+ : []),
3114
+ ...(preparedYield
3115
+ ? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
3116
+ : []),
3117
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
3118
+ ...(reviewLease
3119
+ ? [{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null }]
3120
+ : []),
3121
+ ],
3122
+ validate: () => {
3123
+ const saved = this.store.readTask(task.projectId, task.id);
3124
+ const savedStep = requireStep(saved, stepId);
3125
+ const savedRemediation = readRemediationEvents(this.store, saved)
3126
+ .find((event) => event.eventId === preparedRemediation.event.eventId) ?? null;
3127
+ const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3128
+ if (saved.revision !== paused.revision
3129
+ || saved.status !== 'needs_fix'
3130
+ || savedStep.status !== 'failed'
3131
+ || savedRemediation?.eventHash !== preparedRemediation.event.eventHash
3132
+ || context.locks.inspect(task.id) !== null
3133
+ || (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
3134
+ || headCommit(context.identity.repositoryRoot) !== failureHead
3135
+ || JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
3136
+ || hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== dirtyWorktreeHash) {
3137
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review failure-pause transaction readback failed.', {
3138
+ taskId: task.id,
3139
+ stepId,
3140
+ });
3141
+ }
3142
+ validateTaskHistory(context.identity.repositoryRoot, saved);
3143
+ },
3144
+ });
3145
+ return this.store.readTask(task.projectId, task.id);
3146
+ }
3147
+ prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
2518
3148
  const identity = resolveRepositoryIdentity(repository);
2519
3149
  const task = this.store.readTask(identity.projectId, taskId);
2520
3150
  assertExpectedRevision(task, expectedRevision);
@@ -2531,7 +3161,7 @@ export class WorkflowService {
2531
3161
  if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2532
3162
  throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2533
3163
  }
2534
- return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
3164
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'), task.steps.flatMap((step) => reviewScopeAugmentations(this.store, task, step.id)), reviewMode);
2535
3165
  }
2536
3166
  recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2537
3167
  const context = this.#mutationContext(repository);
@@ -2539,7 +3169,10 @@ export class WorkflowService {
2539
3169
  assertExpectedRevision(task, expectedRevision);
2540
3170
  this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2541
3171
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2542
- const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
3172
+ if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
3173
+ throw new WorkflowError('INVALID_ARGUMENT', 'External Task Review requires reviewMode ordinary or security.');
3174
+ }
3175
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision, input.reviewMode);
2543
3176
  let review;
2544
3177
  try {
2545
3178
  review = validateExternalStrictTaskReviewInput(input, packet);
@@ -2553,6 +3186,7 @@ export class WorkflowService {
2553
3186
  const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2554
3187
  return {
2555
3188
  mode: 'external-sealed',
3189
+ reviewMode: packet.reviewMode,
2556
3190
  packetHash: packet.packetHash,
2557
3191
  repositorySealHash: packet.repositorySealHash,
2558
3192
  reviewerThreadId: input.reviewerThreadId,
@@ -2657,8 +3291,6 @@ export class WorkflowService {
2657
3291
  const task = this.store.readTask(context.identity.projectId, taskId);
2658
3292
  assertExpectedRevision(task, expectedRevision);
2659
3293
  this.requireExecutionAdoptionPosture(context.identity, taskId);
2660
- this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2661
- this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2662
3294
  assertTaskBranch(context.identity.repositoryRoot, task);
2663
3295
  this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2664
3296
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
@@ -2690,6 +3322,20 @@ export class WorkflowService {
2690
3322
  blockers: assessment.blockers,
2691
3323
  });
2692
3324
  }
3325
+ const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
3326
+ ? [assessment.evidence.remediationEventId]
3327
+ : assessment.evidence.remediationEvents.map((event) => event.eventId);
3328
+ const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
3329
+ && record.packageVersion === PACKAGE_VERSION
3330
+ && task.systemCommits.includes(record.commitSha)
3331
+ && record.planIntegrity?.stepId === stepId
3332
+ && record.planIntegrity.planHash === task.planHash
3333
+ && canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
3334
+ && record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
3335
+ if (!compatibility) {
3336
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
3337
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
3338
+ }
2693
3339
  const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
2694
3340
  return { assessment, event };
2695
3341
  }
@@ -2775,7 +3421,12 @@ export class WorkflowService {
2775
3421
  this.assertReviewFresh(identity.repositoryRoot, task);
2776
3422
  const resultHash = this.store.hashArtifact(root, 'result.md');
2777
3423
  const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
2778
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
3424
+ const correctiveAuthority = delegationGrantId === null
3425
+ ? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
3426
+ : null;
3427
+ const delegation = correctiveAuthority
3428
+ ? null
3429
+ : this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
2779
3430
  const event = {
2780
3431
  kind: 'final_acceptance',
2781
3432
  decision: 'approved',
@@ -2787,7 +3438,11 @@ export class WorkflowService {
2787
3438
  evidenceHash,
2788
3439
  headCommit: headCommit(identity.repositoryRoot),
2789
3440
  reason: '',
2790
- authorizationMode: delegation ? 'delegated' : 'human',
3441
+ authorizationMode: correctiveAuthority
3442
+ ? 'corrective-derived'
3443
+ : delegation
3444
+ ? 'delegated'
3445
+ : 'human',
2791
3446
  ...(delegation ? { delegation } : {}),
2792
3447
  };
2793
3448
  return this.store.writeTask({
@@ -2890,7 +3545,9 @@ export class WorkflowService {
2890
3545
  return saved;
2891
3546
  }
2892
3547
  status(repository) {
2893
- const snapshot = this.observationSnapshot(repository);
3548
+ return this.statusFromSnapshot(this.observationSnapshot(repository));
3549
+ }
3550
+ statusFromSnapshot(snapshot) {
2894
3551
  return {
2895
3552
  projectId: snapshot.identity.projectId,
2896
3553
  discoveries: snapshot.discoveries,
@@ -2904,6 +3561,73 @@ export class WorkflowService {
2904
3561
  observation: snapshot.observation,
2905
3562
  };
2906
3563
  }
3564
+ compactStatus(repository) {
3565
+ const snapshot = this.observationSnapshot(repository);
3566
+ const status = this.statusFromSnapshot(snapshot);
3567
+ const next = this.nextFromStatus(repository, null, status);
3568
+ const routedId = typeof next.id === 'string'
3569
+ ? next.id
3570
+ : typeof next.taskId === 'string'
3571
+ ? next.taskId
3572
+ : null;
3573
+ const activeTask = routedId === null
3574
+ ? null
3575
+ : snapshot.tasks.find((task) => task.id === routedId) ?? null;
3576
+ const routedStepId = typeof next.stepId === 'string' ? next.stepId : null;
3577
+ const activeStep = activeTask === null
3578
+ ? null
3579
+ : routedStepId === null
3580
+ ? activeTask.steps.find((step) => step.status === 'in_progress')
3581
+ ?? activeTask.steps.find((step) => step.status === 'failed')
3582
+ ?? null
3583
+ : activeTask.steps.find((step) => step.id === routedStepId) ?? null;
3584
+ const action = typeof next.action === 'string' ? next.action : 'blocked';
3585
+ const blockedAction = typeof next.blockedAction === 'string' ? next.blockedAction : null;
3586
+ const navigation = next.navigation && typeof next.navigation === 'object' && !Array.isArray(next.navigation)
3587
+ ? next.navigation
3588
+ : null;
3589
+ const route = {
3590
+ scope: typeof next.scope === 'string' ? next.scope : null,
3591
+ status: typeof next.status === 'string' ? next.status : null,
3592
+ entityId: routedId,
3593
+ navigationKind: navigation && typeof navigation.kind === 'string' ? navigation.kind : null,
3594
+ transitionId: navigation && typeof navigation.transitionId === 'string' ? navigation.transitionId : null,
3595
+ };
3596
+ const routeFingerprint = sha256Hex(canonicalJsonStringify({
3597
+ projectId: snapshot.identity.projectId,
3598
+ taskId: activeTask?.id ?? null,
3599
+ taskRevision: activeTask?.revision ?? null,
3600
+ stepId: activeStep?.id ?? routedStepId,
3601
+ action,
3602
+ blockedAction,
3603
+ route,
3604
+ observationKind: snapshot.observation.kind,
3605
+ observationReasonCodes: snapshot.observation.reasonCodes,
3606
+ }));
3607
+ return {
3608
+ projection: 'codex-workflow-compact-status-v1',
3609
+ bounded: true,
3610
+ projectId: snapshot.identity.projectId,
3611
+ counts: {
3612
+ discoveries: snapshot.discoveries.length,
3613
+ tasks: snapshot.tasks.length,
3614
+ milestones: snapshot.milestones.length,
3615
+ delegations: snapshot.delegations.length,
3616
+ },
3617
+ activeTask: activeTask
3618
+ ? { id: activeTask.id, revision: activeTask.revision, status: activeTask.status }
3619
+ : null,
3620
+ activeStep: activeStep ? { id: activeStep.id, status: activeStep.status } : null,
3621
+ action,
3622
+ blockedAction,
3623
+ routeFingerprint,
3624
+ route,
3625
+ observation: {
3626
+ kind: snapshot.observation.kind,
3627
+ reasonCodes: [...snapshot.observation.reasonCodes],
3628
+ },
3629
+ };
3630
+ }
2907
3631
  recoverMilestoneTransactions(repository) {
2908
3632
  const { identity } = this.#mutationContext(repository);
2909
3633
  const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
@@ -3239,12 +3963,179 @@ export class WorkflowService {
3239
3963
  dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
3240
3964
  requiredTransport: [
3241
3965
  'Commit only package.json and package-lock.json on the active Milestone base.',
3242
- 'Commit only the same dependency files on the Task branch while preserving all product bytes.',
3243
- 'Install the exact target package, then follow fresh next to dependency registration.',
3966
+ 'Commit only the same dependency files on the Task branch while preserving all product bytes.',
3967
+ 'Install the exact target package, then follow fresh next to dependency registration.',
3968
+ ],
3969
+ blockers,
3970
+ };
3971
+ }
3972
+ planIntegrityUpdatePreflight(repository, taskId) {
3973
+ const snapshot = this.observationSnapshot(repository);
3974
+ if (snapshot.observation.kind !== 'assessment') {
3975
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
3976
+ observation: snapshot.observation,
3977
+ });
3978
+ }
3979
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3980
+ if (!task)
3981
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3982
+ const assessments = task.steps
3983
+ .filter((step) => step.status === 'failed')
3984
+ .map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
3985
+ .filter((assessment) => assessment.applicable);
3986
+ const assessment = assessments.length === 1 ? assessments[0] : null;
3987
+ const adoptedCompatibility = assessment
3988
+ ? null
3989
+ : this.postRebindCheckSupportCompatibility(snapshot, task);
3990
+ const dirtyCarryoverAssessment = assessment || adoptedCompatibility
3991
+ ? null
3992
+ : this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
3993
+ const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
3994
+ && dirtyCarryoverAssessment.compatibility
3995
+ ? {
3996
+ ...dirtyCarryoverAssessment.compatibility,
3997
+ planHash: task.planHash,
3998
+ mode: 'post-rebind-dirty-carryover',
3999
+ }
4000
+ : null;
4001
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4002
+ const versionSurfaces = {
4003
+ declared: versions.declared,
4004
+ locked: versions.locked,
4005
+ installed: versions.installed,
4006
+ currentBranch: versions.currentBranch,
4007
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4008
+ };
4009
+ const sourceVersion = versionSurfaces.declared;
4010
+ const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
4011
+ const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
4012
+ const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
4013
+ && sourceBetaVersion.patch !== null
4014
+ && sourceBetaVersion.patch >= 11;
4015
+ const targetIsNewer = sourceBetaVersion !== null
4016
+ && targetBetaVersion !== null
4017
+ && (targetBetaVersion.train > sourceBetaVersion.train
4018
+ || (targetBetaVersion.train === sourceBetaVersion.train
4019
+ && targetBetaVersion.patch !== null
4020
+ && sourceBetaVersion.patch !== null
4021
+ && targetBetaVersion.patch > sourceBetaVersion.patch));
4022
+ const blockers = [
4023
+ ...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
4024
+ ...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
4025
+ ? []
4026
+ : (dirtyCarryoverAssessment?.blockers.length
4027
+ ? dirtyCarryoverAssessment.blockers
4028
+ : ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
4029
+ ...(!sourceIsEligibleBeta12 || !targetIsNewer
4030
+ ? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
4031
+ : []),
4032
+ ...Object.entries(versionSurfaces)
4033
+ .filter(([, version]) => version !== sourceVersion)
4034
+ .map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
4035
+ ...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
4036
+ ...observationPreflightBlockers(snapshot.observation),
4037
+ ];
4038
+ return {
4039
+ readOnly: true,
4040
+ eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
4041
+ action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
4042
+ ? 'update plan-integrity-transport'
4043
+ : null,
4044
+ projectId: task.projectId,
4045
+ taskId: task.id,
4046
+ taskRevision: task.revision,
4047
+ stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
4048
+ sourceVersion,
4049
+ targetVersion: PACKAGE_VERSION,
4050
+ taskBranch: task.taskBranch,
4051
+ baseBranch: task.baseBranch,
4052
+ dirtyFiles: assessment?.evidence?.changedFiles
4053
+ ?? adoptedCompatibility?.dirtyFiles
4054
+ ?? dirtyCarryoverCompatibility?.dirtyFiles
4055
+ ?? [],
4056
+ conflictHash: assessment?.evidence?.conflictHash
4057
+ ?? adoptedCompatibility?.conflictHash
4058
+ ?? dirtyCarryoverCompatibility?.conflictHash
4059
+ ?? null,
4060
+ compatibilityMode: adoptedCompatibility?.mode
4061
+ ?? dirtyCarryoverCompatibility?.mode
4062
+ ?? 'failed-step-conflict',
4063
+ requiredTransport: [
4064
+ 'Commit only package.json and package-lock.json on the active Milestone base.',
4065
+ 'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
4066
+ 'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
3244
4067
  ],
3245
4068
  blockers,
3246
4069
  };
3247
4070
  }
4071
+ planIntegrityDependencyRecoveryPreflight(repository, taskId) {
4072
+ const snapshot = this.observationSnapshot(repository);
4073
+ if (snapshot.observation.kind !== 'assessment') {
4074
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
4075
+ observation: snapshot.observation,
4076
+ });
4077
+ }
4078
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4079
+ if (!task)
4080
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4081
+ const assessmentBlockers = [];
4082
+ const assessments = task.steps
4083
+ .filter((step) => step.status === 'failed')
4084
+ .flatMap((step) => {
4085
+ try {
4086
+ return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
4087
+ }
4088
+ catch (error) {
4089
+ if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
4090
+ assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
4091
+ return [];
4092
+ }
4093
+ throw error;
4094
+ }
4095
+ })
4096
+ .filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
4097
+ const assessment = assessments.length === 1 ? assessments[0] : null;
4098
+ const adoptedCompatibility = assessment?.evidence
4099
+ ? null
4100
+ : this.postRebindCheckSupportCompatibility(snapshot, task);
4101
+ const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
4102
+ ? null
4103
+ : this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
4104
+ const carriedCompatibility = !assessment?.evidence && task.planHash
4105
+ ? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
4106
+ : null;
4107
+ if ((!assessment?.evidence
4108
+ && !carriedCompatibility
4109
+ && !adoptedCompatibility
4110
+ && !dirtyCarryoverCompatibility) || !task.planHash) {
4111
+ const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
4112
+ return {
4113
+ ...ordinary,
4114
+ eligible: false,
4115
+ action: null,
4116
+ planIntegrity: null,
4117
+ blockers: [...new Set([
4118
+ ...ordinary.blockers,
4119
+ ...assessmentBlockers,
4120
+ 'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
4121
+ ])],
4122
+ };
4123
+ }
4124
+ const compatibility = carriedCompatibility
4125
+ ?? adoptedCompatibility
4126
+ ?? dirtyCarryoverCompatibility
4127
+ ?? {
4128
+ stepId: assessment.stepId,
4129
+ planHash: task.planHash,
4130
+ conflictHash: assessment.evidence.conflictHash,
4131
+ dirtyFiles: [...assessment.evidence.changedFiles],
4132
+ dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
4133
+ remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
4134
+ ? [assessment.evidence.remediationEventId]
4135
+ : assessment.evidence.remediationEvents.map((event) => event.eventId),
4136
+ };
4137
+ return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
4138
+ }
3248
4139
  downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
3249
4140
  const snapshot = this.observationSnapshot(repository);
3250
4141
  if (snapshot.observation.kind !== 'assessment') {
@@ -3299,6 +4190,9 @@ export class WorkflowService {
3299
4190
  recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
3300
4191
  return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
3301
4192
  }
4193
+ recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4194
+ return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
4195
+ }
3302
4196
  recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
3303
4197
  const normalizedActor = actor.trim();
3304
4198
  const normalizedReason = reason.trim();
@@ -3315,7 +4209,9 @@ export class WorkflowService {
3315
4209
  ? Boolean(record.downstreamProofReplan)
3316
4210
  : recoveryMode === 'historical-step'
3317
4211
  ? Boolean(record.historicalStepProvenance)
3318
- : Boolean(record.activeDownstreamProof) === recoveryMode));
4212
+ : recoveryMode === 'plan-integrity'
4213
+ ? Boolean(record.planIntegrity)
4214
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
3319
4215
  if (prior && current.systemCommits.includes(currentHead))
3320
4216
  return current;
3321
4217
  assertExpectedRevision(current, expectedRevision);
@@ -3327,42 +4223,113 @@ export class WorkflowService {
3327
4223
  ? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
3328
4224
  : recoveryMode === 'historical-step'
3329
4225
  ? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
3330
- : recoveryMode
3331
- ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
3332
- : this.inspectDependencyProvenanceCandidate(snapshot, task);
4226
+ : recoveryMode === 'plan-integrity'
4227
+ ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
4228
+ : recoveryMode
4229
+ ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4230
+ : this.inspectDependencyProvenanceCandidate(snapshot, task);
3333
4231
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
3334
4232
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
3335
4233
  taskId,
3336
4234
  blockers: preflight.blockers,
3337
4235
  });
3338
4236
  }
3339
- return context.store.writeTask({
4237
+ const now = this.now();
4238
+ const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
4239
+ const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
4240
+ return {
4241
+ commitSha: dependencyCommit.commitSha,
4242
+ parentCommitSha: dependencyCommit.parentCommitSha,
4243
+ packageName: PACKAGE_NAME,
4244
+ packageVersion: dependencyCommit.packageVersion,
4245
+ files: ['package-lock.json', 'package.json'],
4246
+ actor: normalizedActor,
4247
+ reason: normalizedReason,
4248
+ recordedAt: now.toISOString(),
4249
+ retainedHistoricalCommits: isTerminalCommit ? preflight.retainedHistoricalCommits : [],
4250
+ ...(isTerminalCommit && preflight.activeDownstreamProof
4251
+ ? { activeDownstreamProof: preflight.activeDownstreamProof }
4252
+ : {}),
4253
+ ...(isTerminalCommit && preflight.downstreamProofReplan
4254
+ ? { downstreamProofReplan: preflight.downstreamProofReplan }
4255
+ : {}),
4256
+ ...(isTerminalCommit && preflight.historicalStepProvenance
4257
+ ? { historicalStepProvenance: preflight.historicalStepProvenance }
4258
+ : {}),
4259
+ ...(isTerminalCommit && preflight.planIntegrity
4260
+ ? { planIntegrity: preflight.planIntegrity }
4261
+ : {}),
4262
+ };
4263
+ });
4264
+ const nextTask = {
3340
4265
  ...task,
3341
- systemCommits: [...task.systemCommits, preflight.commitSha],
4266
+ revision: task.revision + 1,
4267
+ updatedAt: now.toISOString(),
4268
+ systemCommits: [
4269
+ ...task.systemCommits,
4270
+ ...preflight.dependencyCommits.map((dependencyCommit) => dependencyCommit.commitSha),
4271
+ ],
3342
4272
  dependencyProvenanceRecoveries: [
3343
4273
  ...(task.dependencyProvenanceRecoveries ?? []),
4274
+ ...recoveredRecords,
4275
+ ],
4276
+ };
4277
+ if (recoveryMode !== 'plan-integrity' || !preflight.planIntegrity) {
4278
+ return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
4279
+ }
4280
+ const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
4281
+ const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
4282
+ const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
4283
+ let refreshedKnowledgeMap = null;
4284
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
4285
+ const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
4286
+ if (!knowledgeRefresh.eligible) {
4287
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
4288
+ taskId,
4289
+ dependencyKnowledgeRefresh: knowledgeRefresh,
4290
+ });
4291
+ }
4292
+ const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
4293
+ const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
4294
+ if (!approved.approval) {
4295
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
4296
+ }
4297
+ refreshedKnowledgeMap = {
4298
+ ...approved,
4299
+ revision: currentKnowledgeMap.revision + 2,
4300
+ createdAt: currentKnowledgeMap.createdAt,
4301
+ updatedAt: now.toISOString(),
4302
+ approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
4303
+ };
4304
+ }
4305
+ const projectRoot = this.store.projectRoot(task.projectId);
4306
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
4307
+ applyProjectTransaction({
4308
+ projectRoot,
4309
+ projectId: task.projectId,
4310
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
4311
+ kind: 'plan-integrity-dependency-recovery',
4312
+ now,
4313
+ targets: [
4314
+ ...(refreshedKnowledgeMap
4315
+ ? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
4316
+ : []),
3344
4317
  {
3345
- commitSha: preflight.commitSha,
3346
- parentCommitSha: preflight.parentCommitSha,
3347
- packageName: PACKAGE_NAME,
3348
- packageVersion: PACKAGE_VERSION,
3349
- files: ['package-lock.json', 'package.json'],
3350
- actor: normalizedActor,
3351
- reason: normalizedReason,
3352
- recordedAt: this.now().toISOString(),
3353
- retainedHistoricalCommits: preflight.retainedHistoricalCommits,
3354
- ...(preflight.activeDownstreamProof
3355
- ? { activeDownstreamProof: preflight.activeDownstreamProof }
3356
- : {}),
3357
- ...(preflight.downstreamProofReplan
3358
- ? { downstreamProofReplan: preflight.downstreamProofReplan }
3359
- : {}),
3360
- ...(preflight.historicalStepProvenance
3361
- ? { historicalStepProvenance: preflight.historicalStepProvenance }
3362
- : {}),
4318
+ path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
4319
+ content: `${JSON.stringify(nextTask, null, 2)}\n`,
3363
4320
  },
3364
4321
  ],
3365
- }, expectedRevision);
4322
+ validate: () => {
4323
+ const saved = this.store.readTask(task.projectId, task.id);
4324
+ if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
4325
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
4326
+ }
4327
+ if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
4328
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
4329
+ }
4330
+ },
4331
+ });
4332
+ return this.store.readTask(task.projectId, task.id);
3366
4333
  }
3367
4334
  milestoneProgress(repository, milestoneId) {
3368
4335
  const snapshot = this.observationSnapshot(repository);
@@ -3619,7 +4586,9 @@ export class WorkflowService {
3619
4586
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
3620
4587
  }
3621
4588
  next(repository, taskId = null) {
3622
- const status = this.status(repository);
4589
+ return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
4590
+ }
4591
+ nextFromStatus(repository, taskId, status) {
3623
4592
  if (status.observation.kind === 'blocked') {
3624
4593
  return {
3625
4594
  scope: 'project',
@@ -3759,7 +4728,24 @@ export class WorkflowService {
3759
4728
  ...(dependency ? { dependency } : {}),
3760
4729
  });
3761
4730
  }
4731
+ const prioritizedRemediationTask = status.milestones.flatMap((milestone) => {
4732
+ if (milestone.status !== 'active')
4733
+ return [];
4734
+ const event = [...readMilestoneScopeChangeEvents(this.store, status.projectId, milestone.id)]
4735
+ .reverse()
4736
+ .find((candidate) => candidate.authorizationMode === 'audited-remediation'
4737
+ && candidate.candidatePlanHash === milestone.planHash
4738
+ && candidate.remediationEvidence?.remediationTaskId);
4739
+ if (!event?.remediationEvidence)
4740
+ return [];
4741
+ const task = tasksById.get(event.remediationEvidence.remediationTaskId);
4742
+ return task
4743
+ && taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
4744
+ ? [task]
4745
+ : [];
4746
+ }).at(0) ?? null;
3762
4747
  const activeTask = requestedTask
4748
+ ?? prioritizedRemediationTask
3763
4749
  ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
3764
4750
  if (activeTask) {
3765
4751
  const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
@@ -3789,6 +4775,22 @@ export class WorkflowService {
3789
4775
  },
3790
4776
  });
3791
4777
  }
4778
+ const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
4779
+ if (planIntegrityDependencyRecovery.eligible) {
4780
+ return withAdoption({
4781
+ scope: 'task',
4782
+ id: activeTask.id,
4783
+ status: activeTask.status,
4784
+ action: 'update plan-integrity-dependency-recover',
4785
+ revision: activeTask.revision,
4786
+ dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
4787
+ compatibilityBoundary: {
4788
+ preservesWorktree: true,
4789
+ prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
4790
+ continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
4791
+ },
4792
+ });
4793
+ }
3792
4794
  const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3793
4795
  if (dependencyRecovery.eligible) {
3794
4796
  return withAdoption({
@@ -3915,41 +4917,132 @@ export class WorkflowService {
3915
4917
  if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
3916
4918
  throw error;
3917
4919
  const blockingDecision = error.details.decision;
3918
- planRiskAuditRecovery = blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
4920
+ const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
4921
+ const existingRemediationEvent = blockingAudit && activeTask.milestoneId
4922
+ ? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
4923
+ .reverse()
4924
+ .find((event) => event.authorizationMode === 'audited-remediation'
4925
+ && event.remediationEvidence?.planRiskAuditEventHash === blockingAudit.eventHash) ?? null
4926
+ : null;
4927
+ const existingRemediationTask = existingRemediationEvent?.remediationEvidence
4928
+ ? tasksById.get(existingRemediationEvent.remediationEvidence.remediationTaskId) ?? null
4929
+ : null;
4930
+ const remediationDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status)) ?? null;
4931
+ const remediationMilestone = activeTask.milestoneId
4932
+ ? milestonesById.get(activeTask.milestoneId) ?? null
4933
+ : null;
4934
+ const remediationMemberships = remediationMilestone
4935
+ ? new Map(remediationMilestone.memberships.map((membership) => [membership.taskId, membership]))
4936
+ : new Map();
4937
+ const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
4938
+ const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
4939
+ && remediationMilestone?.status === 'active'
4940
+ && remediationMembership?.disposition === 'required';
4941
+ const remediationAncestorIds = new Set();
4942
+ const visitRemediationAncestors = (candidateTaskId) => {
4943
+ for (const predecessorTaskId of remediationMemberships.get(candidateTaskId)?.dependsOnTaskIds ?? []) {
4944
+ if (remediationAncestorIds.has(predecessorTaskId))
4945
+ continue;
4946
+ remediationAncestorIds.add(predecessorTaskId);
4947
+ visitRemediationAncestors(predecessorTaskId);
4948
+ }
4949
+ };
4950
+ visitRemediationAncestors(activeTask.id);
4951
+ const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
4952
+ .filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
4953
+ .sort();
4954
+ planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
3919
4955
  ? {
3920
4956
  scope: 'task',
3921
4957
  id: activeTask.id,
3922
4958
  status: activeTask.status,
3923
- action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
4959
+ action: existingRemediationTask?.status === 'merged'
4960
+ ? 'task plan-set'
4961
+ : existingRemediationTask
4962
+ ? 'doctor'
4963
+ : remediationDiscovery?.status === 'ready_to_materialize'
4964
+ ? 'milestone remediation-materialize'
4965
+ : remediationDiscovery
4966
+ ? 'discovery update'
4967
+ : 'discovery start',
3924
4968
  blockedAction: 'task authorize',
3925
4969
  revision: activeTask.revision,
3926
4970
  planRiskAuditDecision: {
3927
4971
  state: 'recorded-blocking',
3928
4972
  decision: blockingDecision,
3929
4973
  reason: error.message,
3930
- requiredAction: blockingDecision === 'stop-escalate'
3931
- ? 'Replace the rejected Step Plan before requesting fresh authorization.'
3932
- : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
4974
+ requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
3933
4975
  },
3934
- ...(blockingDecision === 'stop-escalate'
3935
- ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
3936
- : {}),
3937
- }
3938
- : {
3939
- scope: 'task',
3940
- id: activeTask.id,
3941
- status: activeTask.status,
3942
- action: 'task plan-risk-audit',
3943
- revision: activeTask.revision,
3944
- planRiskAuditRecovery: {
3945
- state: 'required',
3946
- reason: error.message,
3947
- currentBriefHash: activeTask.briefHash,
3948
- currentPlanHash: activeTask.planHash,
3949
- knowledgeMapRevision: activeTask.knowledgeMapRevision,
3950
- knowledgeMapHash: activeTask.knowledgeMapHash,
4976
+ auditedRemediation: {
4977
+ state: existingRemediationTask?.status === 'merged'
4978
+ ? 'merged-replan-required'
4979
+ : existingRemediationTask
4980
+ ? 'materialized-not-merged'
4981
+ : remediationDiscovery?.status === 'ready_to_materialize'
4982
+ ? 'ready-to-materialize'
4983
+ : 'discovery-required',
4984
+ humanApprovalRequired: false,
4985
+ blockedTaskId: activeTask.id,
4986
+ blockedTaskRevision: activeTask.revision,
4987
+ milestoneId: remediationMilestone.id,
4988
+ milestoneRevision: remediationMilestone.revision,
4989
+ planRiskAuditEventId: blockingAudit?.eventId ?? null,
4990
+ planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
4991
+ discoveryId: remediationDiscovery?.id ?? null,
4992
+ discoveryRevision: remediationDiscovery?.revision ?? null,
4993
+ remediationTaskId: existingRemediationTask?.id ?? null,
4994
+ remediationTaskStatus: existingRemediationTask?.status ?? null,
4995
+ requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
4996
+ eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
4997
+ command: existingRemediationTask?.status === 'merged'
4998
+ ? 'task plan-set'
4999
+ : existingRemediationTask
5000
+ ? 'doctor'
5001
+ : remediationDiscovery?.status === 'ready_to_materialize'
5002
+ ? 'milestone remediation-materialize'
5003
+ : remediationDiscovery
5004
+ ? 'discovery update'
5005
+ : 'discovery start',
5006
+ boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5007
+ preservesMilestoneSemantics: true,
3951
5008
  },
3952
- };
5009
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5010
+ }
5011
+ : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5012
+ ? {
5013
+ scope: 'task',
5014
+ id: activeTask.id,
5015
+ status: activeTask.status,
5016
+ action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5017
+ blockedAction: 'task authorize',
5018
+ revision: activeTask.revision,
5019
+ planRiskAuditDecision: {
5020
+ state: 'recorded-blocking',
5021
+ decision: blockingDecision,
5022
+ reason: error.message,
5023
+ requiredAction: blockingDecision === 'stop-escalate'
5024
+ ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5025
+ : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5026
+ },
5027
+ ...(blockingDecision === 'stop-escalate'
5028
+ ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5029
+ : {}),
5030
+ }
5031
+ : {
5032
+ scope: 'task',
5033
+ id: activeTask.id,
5034
+ status: activeTask.status,
5035
+ action: 'task plan-risk-audit',
5036
+ revision: activeTask.revision,
5037
+ planRiskAuditRecovery: {
5038
+ state: 'required',
5039
+ reason: error.message,
5040
+ currentBriefHash: activeTask.briefHash,
5041
+ currentPlanHash: activeTask.planHash,
5042
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
5043
+ knowledgeMapHash: activeTask.knowledgeMapHash,
5044
+ },
5045
+ };
3953
5046
  }
3954
5047
  }
3955
5048
  const taskNext = activeTask.status === 'planning'
@@ -3958,6 +5051,37 @@ export class WorkflowService {
3958
5051
  ? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
3959
5052
  : planRiskAuditRecovery
3960
5053
  ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
5054
+ const auditedRemediationNavigation = taskNext?.auditedRemediation;
5055
+ let auditedDependencyKnowledgeRefreshBlocked = null;
5056
+ if (auditedRemediationNavigation
5057
+ && activeTaskMilestone
5058
+ && ['discovery start', 'discovery update', 'milestone remediation-materialize'].includes(String(taskNext?.action))) {
5059
+ try {
5060
+ const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
5061
+ const inspectedKnowledgeMap = inspectCurrentKnowledgeMap();
5062
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
5063
+ const dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(repositoryRoot, activeTaskMilestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
5064
+ if (dependencyKnowledgeRefresh.eligible) {
5065
+ return withAdoption(withMilestoneCancellation({
5066
+ ...taskNext,
5067
+ auditedRemediation: {
5068
+ ...auditedRemediationNavigation,
5069
+ dependencyKnowledgeRefresh: {
5070
+ ...dependencyKnowledgeRefresh,
5071
+ humanApprovalRequired: false,
5072
+ application: 'atomic-with-remediation-materialization',
5073
+ },
5074
+ },
5075
+ }));
5076
+ }
5077
+ auditedDependencyKnowledgeRefreshBlocked = dependencyKnowledgeRefresh;
5078
+ }
5079
+ }
5080
+ catch (error) {
5081
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
5082
+ throw error;
5083
+ }
5084
+ }
3961
5085
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
3962
5086
  // strict review are executable while context refresh correctly remains forbidden.
3963
5087
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -3967,8 +5091,12 @@ export class WorkflowService {
3967
5091
  || taskNext?.action === 'task step-review'
3968
5092
  || taskNext?.action === 'task downstream-proof-recover'
3969
5093
  || taskNext?.action === 'task remediation-mode-recover'
5094
+ || (taskNext?.action === 'task writer-credential-replace'
5095
+ && taskNext?.blockedAction === 'task remediation-mode-recover')
3970
5096
  || taskNext?.action === 'task plan-integrity-recover'
3971
5097
  || taskNext?.blockedAction === 'task plan-integrity-recover'
5098
+ || taskNext?.action === 'task corrective-yield'
5099
+ || String(taskNext?.action ?? '').startsWith('task corrective-replan')
3972
5100
  || hardStop
3973
5101
  || terminalCorrectiveDecision === 'split-required'
3974
5102
  || terminalCorrectiveDecision === 'stop-escalate') {
@@ -4005,8 +5133,10 @@ export class WorkflowService {
4005
5133
  ? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
4006
5134
  : null)
4007
5135
  : null;
5136
+ const rollingRootCausePlanSet = taskNext?.action === 'task plan-set'
5137
+ && taskNext.rollingWave?.state === 'root-cause-replan-required';
4008
5138
  const contextRefreshOptions = action === 'project-memory reconcile'
4009
- && taskNext?.action !== 'task plan-set'
5139
+ && (taskNext?.action !== 'task plan-set' || rollingRootCausePlanSet)
4010
5140
  && refreshAssessment?.eligible
4011
5141
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
4012
5142
  : [];
@@ -4052,6 +5182,15 @@ export class WorkflowService {
4052
5182
  },
4053
5183
  }
4054
5184
  : {}),
5185
+ ...(auditedDependencyKnowledgeRefreshBlocked
5186
+ ? {
5187
+ auditedDependencyKnowledgeRefreshBlocked: {
5188
+ ...auditedDependencyKnowledgeRefreshBlocked,
5189
+ humanApprovalRequired: true,
5190
+ requiredAction: 'Resolve or explicitly approve the non-dependency Project Knowledge drift.',
5191
+ },
5192
+ }
5193
+ : {}),
4055
5194
  }));
4056
5195
  }
4057
5196
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
@@ -4358,6 +5497,32 @@ export class WorkflowService {
4358
5497
  milestonePlanHash: milestone.planHash,
4359
5498
  };
4360
5499
  }
5500
+ currentCorrectiveDerivedTaskAuthority(projectId, task, actor) {
5501
+ if (!task.milestoneId || actor.trim() !== `agent:milestone-remediation:${task.milestoneId}`)
5502
+ return null;
5503
+ const milestone = this.readMilestoneCurrent(projectId, task.milestoneId);
5504
+ if (milestone.status !== 'active' || !milestone.planHash)
5505
+ return null;
5506
+ const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
5507
+ if (membership?.disposition !== 'required')
5508
+ return null;
5509
+ const authorization = [...milestone.authorizations]
5510
+ .reverse()
5511
+ .find((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved');
5512
+ if (authorization?.authorizationMode !== 'corrective-derived'
5513
+ || authorization.planHash !== milestone.planHash
5514
+ || authorization.actor !== actor.trim())
5515
+ return null;
5516
+ const scopeChange = [...readMilestoneScopeChangeEvents(this.store, projectId, milestone.id)]
5517
+ .reverse()
5518
+ .find((candidate) => candidate.authorizationMode === 'audited-remediation'
5519
+ && candidate.candidatePlanHash === milestone.planHash
5520
+ && candidate.remediationEvidence !== null
5521
+ && candidate.remediationEvidence !== undefined);
5522
+ if (!scopeChange)
5523
+ return null;
5524
+ return { milestone, authorization, scopeChange };
5525
+ }
4361
5526
  nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
4362
5527
  const transition = task.status === 'awaiting_execution_authorization'
4363
5528
  ? 'task.execution_authorize'
@@ -4367,6 +5532,12 @@ export class WorkflowService {
4367
5532
  const delegatedApprovalOptions = transition
4368
5533
  ? this.delegatedApprovalOptions(projectId, transition, task)
4369
5534
  : [];
5535
+ const correctiveDerivedActor = task.milestoneId
5536
+ ? `agent:milestone-remediation:${task.milestoneId}`
5537
+ : null;
5538
+ const correctiveDerivedAuthority = transition && correctiveDerivedActor
5539
+ ? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
5540
+ : null;
4370
5541
  let remediationModeRecovery = null;
4371
5542
  let guardedCorrectivePosture = null;
4372
5543
  if (planRiskAudit) {
@@ -4374,7 +5545,7 @@ export class WorkflowService {
4374
5545
  guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
4375
5546
  }
4376
5547
  catch (error) {
4377
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
5548
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
4378
5549
  if (!remediationModeRecovery)
4379
5550
  throw error;
4380
5551
  }
@@ -4387,7 +5558,7 @@ export class WorkflowService {
4387
5558
  catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
4388
5559
  }
4389
5560
  catch (error) {
4390
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
5561
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
4391
5562
  if (!remediationModeRecovery)
4392
5563
  throw error;
4393
5564
  }
@@ -4399,6 +5570,11 @@ export class WorkflowService {
4399
5570
  && changedFiles(repositoryRoot).length > 0
4400
5571
  ? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
4401
5572
  : null;
5573
+ const planIntegrityDirtyCarryover = next.action === 'task run'
5574
+ && nextStepId
5575
+ && changedFiles(repositoryRoot).length > 0
5576
+ ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
5577
+ : null;
4402
5578
  let mechanicalFeasibilityOverlay = {};
4403
5579
  if (task.status === 'awaiting_execution_authorization' && task.planHash) {
4404
5580
  try {
@@ -4449,6 +5625,9 @@ export class WorkflowService {
4449
5625
  }
4450
5626
  const handoffPosture = readTaskC1Posture(this.store, task);
4451
5627
  const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
5628
+ const failedStepRemediations = failedStep && !remediationModeRecovery
5629
+ ? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
5630
+ : [];
4452
5631
  const failedStepReviewRouting = failedStep
4453
5632
  ? latestFailedStepReviewRouting(this.store, task, failedStep.id)
4454
5633
  : null;
@@ -4579,11 +5758,21 @@ export class WorkflowService {
4579
5758
  stepId: planIntegrityAssessment.stepId,
4580
5759
  planIntegrityRecovery: {
4581
5760
  state: 'eligible',
4582
- reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
5761
+ reasonCode: planIntegrityAssessment.evidence?.kind === 'missing-npm-script-outside-allowed-writes'
5762
+ ? 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES'
5763
+ : planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
5764
+ ? 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES'
5765
+ : planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
5766
+ ? 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES'
5767
+ : 'NON_EXECUTABLE_NPM_CHECK_OUTSIDE_ALLOWED_WRITES',
4583
5768
  expectedRevision: task.revision,
4584
5769
  requiredActor,
4585
5770
  evidence: planIntegrityAssessment.evidence,
4586
- boundedEffect: 'record-replan-required-decision',
5771
+ boundedEffect: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
5772
+ ? 'record-machine-derived-check-support-scope'
5773
+ : planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
5774
+ ? 'record-machine-derived-component-owner-scope'
5775
+ : 'record-replan-required-decision',
4587
5776
  preservesWorktree: true,
4588
5777
  },
4589
5778
  }
@@ -4716,7 +5905,48 @@ export class WorkflowService {
4716
5905
  };
4717
5906
  break;
4718
5907
  case 'replan-required':
4719
- guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
5908
+ guardedCorrectiveOverlay = guardedCorrectivePosture.correctiveDecision
5909
+ ? correctiveReplanOverlay(guardedCorrectivePosture)
5910
+ : (() => {
5911
+ const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
5912
+ const rollingC1Posture = readTaskC1Posture(this.store, task);
5913
+ const blockers = [
5914
+ ...(!['needs_fix', 'ready'].includes(task.status)
5915
+ ? [`Task must be needs_fix or context-refreshed ready, not ${task.status}.`]
5916
+ : []),
5917
+ ...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may remain in progress.'] : []),
5918
+ ...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
5919
+ ? [`${guardedCorrectivePosture.stepId} must remain failed.`]
5920
+ : []),
5921
+ ...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
5922
+ ...(rollingC1Posture.state !== 'none'
5923
+ ? ['No C1 claim may remain after the root-cause failure pause.']
5924
+ : []),
5925
+ ];
5926
+ return blockers.length > 0
5927
+ ? {
5928
+ action: 'doctor',
5929
+ blockedAction: 'task plan-set',
5930
+ stepId: guardedCorrectivePosture.stepId,
5931
+ rollingWave: {
5932
+ state: 'root-cause-replan-blocked',
5933
+ attemptCount: guardedCorrectivePosture.attemptCount,
5934
+ humanApprovalRequired: false,
5935
+ blockers,
5936
+ },
5937
+ }
5938
+ : {
5939
+ action: 'task plan-set',
5940
+ blockedAction: 'task run',
5941
+ stepId: guardedCorrectivePosture.stepId,
5942
+ rollingWave: {
5943
+ state: 'root-cause-replan-required',
5944
+ attemptCount: guardedCorrectivePosture.attemptCount,
5945
+ humanApprovalRequired: false,
5946
+ boundary: 'Task objective, requirements, acceptance, and security semantics remain unchanged.',
5947
+ },
5948
+ };
5949
+ })();
4720
5950
  break;
4721
5951
  case 'split-required':
4722
5952
  {
@@ -4874,6 +6104,11 @@ export class WorkflowService {
4874
6104
  externalSealedReview: {
4875
6105
  packetCommand: 'task step-review-packet',
4876
6106
  recordCommand: 'task step-review-record',
6107
+ reviewMode: 'ordinary',
6108
+ allowedReviewModes: ['ordinary', 'security'],
6109
+ packetReviewModeOption: '--review-mode',
6110
+ implicitSecurityScan: false,
6111
+ reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${strictStepReviewStep.id}:${strictStepReviewStep.evidence.commitSha}:ordinary`),
4877
6112
  reviewerMustUseSeparateChat: true,
4878
6113
  repositorySealRequired: true,
4879
6114
  },
@@ -4889,6 +6124,11 @@ export class WorkflowService {
4889
6124
  externalSealedReview: {
4890
6125
  packetCommand: 'task review-packet',
4891
6126
  recordCommand: 'task review-sealed-record',
6127
+ reviewMode: 'ordinary',
6128
+ allowedReviewModes: ['ordinary', 'security'],
6129
+ packetReviewModeOption: '--review-mode',
6130
+ implicitSecurityScan: false,
6131
+ reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${headCommit(repositoryRoot)}:ordinary`),
4892
6132
  reviewerMustUseSeparateChat: true,
4893
6133
  repositorySealRequired: true,
4894
6134
  },
@@ -4907,7 +6147,11 @@ export class WorkflowService {
4907
6147
  : {}),
4908
6148
  ...guardedCorrectiveOverlay,
4909
6149
  ...downstreamProofOverlay,
4910
- ...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
6150
+ ...(failedStepReviewRouting?.route === 'replan'
6151
+ && next.action === 'task run'
6152
+ && (!failedStep
6153
+ || deriveRollingCauseDecision(failedStepRemediations) === null
6154
+ || failedStepRemediations.at(-1)?.causeBindings?.length === 0)
4911
6155
  ? {
4912
6156
  action: 'task plan-set',
4913
6157
  blockedAction: 'task run',
@@ -4944,7 +6188,44 @@ export class WorkflowService {
4944
6188
  },
4945
6189
  }
4946
6190
  : {}),
6191
+ ...(planIntegrityDirtyCarryover?.applicable
6192
+ ? planIntegrityDirtyCarryover.eligible
6193
+ ? {
6194
+ planIntegrityDirtyCarryover: {
6195
+ state: 'validated',
6196
+ stepId: nextStepId,
6197
+ dependencyRecoveryCommit: planIntegrityDirtyCarryover.dependencyRecoveryCommit,
6198
+ dirtyFiles: planIntegrityDirtyCarryover.compatibility?.dirtyFiles ?? [],
6199
+ dirtyWorktreeHash: planIntegrityDirtyCarryover.compatibility?.dirtyWorktreeHash ?? null,
6200
+ scopeExtensions: planIntegrityDirtyCarryover.compatibility?.scopeExtensions ?? [],
6201
+ meaning: 'The current Step may adopt exactly these hash-bound check-support bytes on task run.',
6202
+ },
6203
+ }
6204
+ : {
6205
+ action: 'doctor',
6206
+ blockedAction: 'task run',
6207
+ planIntegrityDirtyCarryover: {
6208
+ state: 'blocked',
6209
+ stepId: nextStepId,
6210
+ blockers: planIntegrityDirtyCarryover.blockers,
6211
+ },
6212
+ }
6213
+ : {}),
4947
6214
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6215
+ ...(correctiveDerivedAuthority && correctiveDerivedActor
6216
+ ? {
6217
+ correctiveDerivedApproval: {
6218
+ action: transition === 'task.execution_authorize' ? 'task authorize' : 'task accept',
6219
+ actor: correctiveDerivedActor,
6220
+ humanApprovalRequired: false,
6221
+ milestoneId: correctiveDerivedAuthority.milestone.id,
6222
+ milestoneRevision: correctiveDerivedAuthority.milestone.revision,
6223
+ milestonePlanHash: correctiveDerivedAuthority.milestone.planHash,
6224
+ planRiskAuditEventId: correctiveDerivedAuthority.scopeChange.remediationEvidence?.planRiskAuditEventId ?? null,
6225
+ authority: 'current-corrective-derived-milestone-execution-authorization',
6226
+ },
6227
+ }
6228
+ : {}),
4948
6229
  };
4949
6230
  if (handoffPosture.state === 'pending') {
4950
6231
  return this.withTaskNavigationContracts(task, {
@@ -5003,7 +6284,20 @@ export class WorkflowService {
5003
6284
  return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
5004
6285
  }
5005
6286
  taskHasTerminalCorrectiveDecision(task) {
5006
- return this.taskTerminalCorrectivePosture(task) !== null;
6287
+ try {
6288
+ return this.taskTerminalCorrectivePosture(task) !== null;
6289
+ }
6290
+ catch (error) {
6291
+ if (error instanceof WorkflowError
6292
+ && error.code === 'TRANSITION_BLOCKED'
6293
+ && (error.details.decision === 'stop-escalate' || error.details.decision === 'split-required')) {
6294
+ // A blocking Plan Risk Audit is not a guarded-Step terminal decision, but it
6295
+ // equally cannot own an atomic delegated context refresh. Treat it as
6296
+ // ineligible here so standalone Knowledge reconciliation remains executable.
6297
+ return true;
6298
+ }
6299
+ throw error;
6300
+ }
5007
6301
  }
5008
6302
  taskTerminalCorrectivePosture(task) {
5009
6303
  if (!task.planHash)
@@ -5166,6 +6460,10 @@ export class WorkflowService {
5166
6460
  statePlanHash: task.planHash,
5167
6461
  });
5168
6462
  }
6463
+ if (authorization.authorizationMode === 'corrective-derived'
6464
+ && !this.currentCorrectiveDerivedTaskAuthority(task.projectId, task, authorization.actor)) {
6465
+ 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 });
6466
+ }
5169
6467
  const mechanicalFeasibility = authorization.mechanicalFeasibility;
5170
6468
  if (!mechanicalFeasibility)
5171
6469
  return authorization;
@@ -5237,7 +6535,218 @@ export class WorkflowService {
5237
6535
  },
5238
6536
  };
5239
6537
  }
5240
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null) {
6538
+ carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
6539
+ let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
6540
+ let dependencyCommitCount = 0;
6541
+ while (!task.systemCommits.includes(boundaryCommit)) {
6542
+ const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
6543
+ if (!dependencyCommit)
6544
+ return null;
6545
+ boundaryCommit = dependencyCommit.parentCommitSha;
6546
+ dependencyCommitCount += 1;
6547
+ }
6548
+ if (dependencyCommitCount === 0)
6549
+ return null;
6550
+ const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
6551
+ const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
6552
+ const compatibility = record.planIntegrity;
6553
+ if (!compatibility
6554
+ || record.commitSha !== boundaryCommit
6555
+ || !task.systemCommits.includes(record.commitSha)
6556
+ || compatibility.planHash !== task.planHash
6557
+ || canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
6558
+ || compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
6559
+ return false;
6560
+ const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
6561
+ if (!step || step.status !== 'failed')
6562
+ return false;
6563
+ const remediationEventIds = readRemediationEvents(this.store, task)
6564
+ .filter((event) => event.stepId === compatibility.stepId)
6565
+ .map((event) => event.eventId);
6566
+ if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
6567
+ return false;
6568
+ return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
6569
+ && event.stepId === compatibility.stepId
6570
+ && event.decision === 'replan-required')
6571
+ && readTaskC1Posture(this.store, task).state === 'none';
6572
+ });
6573
+ return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
6574
+ }
6575
+ postRebindCheckSupportCompatibility(snapshot, task) {
6576
+ if (task.status !== 'awaiting_execution_authorization'
6577
+ || !task.planHash
6578
+ || task.steps.some((step) => step.status === 'in_progress')
6579
+ || readTaskC1Posture(this.store, task).state !== 'none')
6580
+ return null;
6581
+ const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
6582
+ const candidates = readCorrectiveDecisionEvents(this.store, task).flatMap((event) => {
6583
+ const evidence = event.planIntegrityEvidence;
6584
+ if (event.planHash === task.planHash
6585
+ || event.decision !== 'continue-fix'
6586
+ || event.recoveryBasis !== 'plan-integrity'
6587
+ || evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
6588
+ || !task.systemCommits.includes(evidence.gitHead)
6589
+ || canonicalJsonStringify([...evidence.changedFiles].sort()) !== canonicalJsonStringify(dirtyFiles)) {
6590
+ return [];
6591
+ }
6592
+ const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
6593
+ if (!step
6594
+ || step.status !== 'planned'
6595
+ || evidence.scopeExtensions.some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
6596
+ || !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath)))
6597
+ return [];
6598
+ return [{
6599
+ stepId: step.id,
6600
+ planHash: task.planHash,
6601
+ conflictHash: evidence.conflictHash,
6602
+ dirtyFiles: [...dirtyFiles],
6603
+ dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, dirtyFiles),
6604
+ remediationEventIds: evidence.remediationEvents.map((binding) => binding.eventId),
6605
+ mode: 'post-rebind-scope-adoption',
6606
+ scopeExtensions: [...evidence.scopeExtensions],
6607
+ }];
6608
+ });
6609
+ return candidates.length === 1 ? candidates[0] : null;
6610
+ }
6611
+ carriedPostRebindCheckSupportDirtyCarryover(snapshot, task) {
6612
+ let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
6613
+ let dependencyCommitCount = 0;
6614
+ while (!task.systemCommits.includes(boundaryCommit)) {
6615
+ const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
6616
+ if (!dependencyCommit)
6617
+ return null;
6618
+ boundaryCommit = dependencyCommit.parentCommitSha;
6619
+ dependencyCommitCount += 1;
6620
+ }
6621
+ if (dependencyCommitCount === 0)
6622
+ return null;
6623
+ const assessment = this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, boundaryCommit);
6624
+ if (!assessment.eligible || !assessment.compatibility || !task.planHash)
6625
+ return null;
6626
+ return {
6627
+ ...assessment.compatibility,
6628
+ planHash: task.planHash,
6629
+ mode: 'post-rebind-dirty-carryover',
6630
+ };
6631
+ }
6632
+ assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, stepId, historyBoundary = headCommit(repositoryRoot)) {
6633
+ const relevantRecords = [...(task.dependencyProvenanceRecoveries ?? [])]
6634
+ .reverse()
6635
+ .filter((record) => {
6636
+ const compatibility = record.planIntegrity;
6637
+ return compatibility
6638
+ && ['post-rebind-scope-adoption', 'post-rebind-dirty-carryover'].includes(compatibility.mode ?? '')
6639
+ && (!stepId || compatibility.stepId === stepId);
6640
+ });
6641
+ if (relevantRecords.length === 0) {
6642
+ return {
6643
+ applicable: false,
6644
+ eligible: false,
6645
+ compatibility: null,
6646
+ dependencyRecoveryCommit: null,
6647
+ blockers: [],
6648
+ };
6649
+ }
6650
+ const blockers = [];
6651
+ const record = relevantRecords[0];
6652
+ const compatibility = record.planIntegrity;
6653
+ const step = task.steps.find((candidate) => candidate.id === compatibility.stepId) ?? null;
6654
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
6655
+ let dirtyWorktreeHash = null;
6656
+ try {
6657
+ dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
6658
+ }
6659
+ catch (error) {
6660
+ blockers.push(error instanceof Error ? error.message : String(error));
6661
+ }
6662
+ if (record.commitSha !== historyBoundary || !task.systemCommits.includes(record.commitSha)) {
6663
+ blockers.push('The latest check-support compatibility record must bind the exact registered history boundary.');
6664
+ }
6665
+ if (task.workspaceOwner !== 'local') {
6666
+ blockers.push('Check-support dirty carryover requires the recorded local Task workspace.');
6667
+ }
6668
+ if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
6669
+ blockers.push('Current branch must equal the recorded Task branch.');
6670
+ }
6671
+ if (!['ready', 'in_progress'].includes(task.status)) {
6672
+ blockers.push('The Task must retain an execution-authorized posture before dirty carryover.');
6673
+ }
6674
+ if (!task.planHash) {
6675
+ blockers.push('The Task must retain a current Plan hash.');
6676
+ }
6677
+ if (!step || step.status !== 'planned' || step.evidence !== null) {
6678
+ blockers.push(`The bound Step ${compatibility.stepId} must remain planned with no evidence.`);
6679
+ }
6680
+ if (task.steps.some((candidate) => candidate.status === 'in_progress')) {
6681
+ blockers.push('No Step may already be in progress before dirty carryover.');
6682
+ }
6683
+ if (canonicalJsonStringify(dirtyFiles)
6684
+ !== canonicalJsonStringify([...compatibility.dirtyFiles].sort())) {
6685
+ blockers.push('Dirty files changed after the registered check-support compatibility boundary.');
6686
+ }
6687
+ if (dirtyWorktreeHash !== compatibility.dirtyWorktreeHash) {
6688
+ blockers.push('Dirty file content changed after the registered check-support compatibility boundary.');
6689
+ }
6690
+ if (!compatibility.scopeExtensions?.length) {
6691
+ blockers.push('The compatibility record must retain at least one machine-derived scope extension.');
6692
+ }
6693
+ if (step) {
6694
+ if (dirtyFiles.some((dirtyPath) => !pathAllowed(dirtyPath, step.allowedWrites))) {
6695
+ blockers.push('The current Step must explicitly own every preserved dirty file.');
6696
+ }
6697
+ if ((compatibility.scopeExtensions ?? []).some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
6698
+ || !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath))) {
6699
+ blockers.push('The current Step must explicitly own every check-support scope extension as allowedWrites and expectedOutputs.');
6700
+ }
6701
+ }
6702
+ let reboundPlanHash = compatibility.planHash;
6703
+ for (const rebind of task.knowledgeRebinds) {
6704
+ if (rebind.previousPlanHash === reboundPlanHash)
6705
+ reboundPlanHash = rebind.reboundPlanHash;
6706
+ }
6707
+ if (!task.planHash || reboundPlanHash !== task.planHash) {
6708
+ blockers.push('The current Plan must equal the compatibility Plan or its exact Knowledge-rebind descendant.');
6709
+ }
6710
+ const matchingDecisions = readCorrectiveDecisionEvents(this.store, task).filter((event) => {
6711
+ const evidence = event.planIntegrityEvidence;
6712
+ return event.stepId === compatibility.stepId
6713
+ && event.decision === 'continue-fix'
6714
+ && event.recoveryBasis === 'plan-integrity'
6715
+ && evidence?.kind === 'check-support-anchor-outside-allowed-writes'
6716
+ && evidence.conflictHash === compatibility.conflictHash
6717
+ && canonicalJsonStringify([...evidence.changedFiles].sort())
6718
+ === canonicalJsonStringify([...compatibility.dirtyFiles].sort())
6719
+ && canonicalJsonStringify(evidence.remediationEvents.map((binding) => binding.eventId))
6720
+ === canonicalJsonStringify(compatibility.remediationEventIds)
6721
+ && canonicalJsonStringify([...evidence.scopeExtensions].sort())
6722
+ === canonicalJsonStringify([...(compatibility.scopeExtensions ?? [])].sort());
6723
+ });
6724
+ if (matchingDecisions.length !== 1) {
6725
+ blockers.push('Exactly one validated historical check-support decision must bind the preserved dirty set and scope.');
6726
+ }
6727
+ try {
6728
+ validateTaskHistory(repositoryRoot, task, historyBoundary);
6729
+ }
6730
+ catch (error) {
6731
+ blockers.push(error instanceof Error ? error.message : String(error));
6732
+ }
6733
+ return {
6734
+ applicable: true,
6735
+ eligible: blockers.length === 0,
6736
+ compatibility: blockers.length === 0
6737
+ ? {
6738
+ ...structuredClone(compatibility),
6739
+ planHash: task.planHash,
6740
+ dirtyFiles,
6741
+ dirtyWorktreeHash: dirtyWorktreeHash,
6742
+ mode: 'post-rebind-dirty-carryover',
6743
+ }
6744
+ : null,
6745
+ dependencyRecoveryCommit: record.commitSha,
6746
+ blockers,
6747
+ };
6748
+ }
6749
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
5241
6750
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
5242
6751
  const now = this.now().getTime();
5243
6752
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
@@ -5257,7 +6766,7 @@ export class WorkflowService {
5257
6766
  currentBranch: versions.currentBranch,
5258
6767
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
5259
6768
  },
5260
- }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance);
6769
+ }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
5261
6770
  }
5262
6771
  inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
5263
6772
  const now = this.now().getTime();
@@ -5279,10 +6788,9 @@ export class WorkflowService {
5279
6788
  const policy = gitMutationPolicyForAction(task, action);
5280
6789
  const c1Posture = readTaskC1Posture(this.store, task);
5281
6790
  const claimedC1Posture = c1Posture.state === 'claimed';
5282
- const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
5283
- const activeLease = writerTokenContract
5284
- ? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
5285
- : null;
6791
+ const activeLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
6792
+ const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture, activeLease !== null);
6793
+ const activeLeaseUsable = activeLease ? !toPublicWriterLease(activeLease, this.now()).stale : false;
5286
6794
  const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
5287
6795
  const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
5288
6796
  ? {
@@ -5294,9 +6802,24 @@ export class WorkflowService {
5294
6802
  acquiresLeaseWhenAbsent: true,
5295
6803
  }
5296
6804
  : null;
6805
+ const credentialBlocksAction = Boolean(writerCredentialRecovery
6806
+ && writerTokenContract
6807
+ && (activeLease === null || !activeLeaseUsable));
5297
6808
  return {
5298
6809
  ...next,
5299
6810
  ...(policy ? { gitMutationPolicy: policy } : {}),
6811
+ ...(credentialBlocksAction
6812
+ ? {
6813
+ action: 'task writer-credential-replace',
6814
+ blockedAction: action,
6815
+ continuation: {
6816
+ kind: 'failed-step-pause',
6817
+ preservesTask: true,
6818
+ preservesWorktree: true,
6819
+ resumesWithFreshNext: true,
6820
+ },
6821
+ }
6822
+ : {}),
5300
6823
  ...(writerTokenContract ? { writerTokenContract } : {}),
5301
6824
  ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
5302
6825
  };
@@ -5708,16 +7231,20 @@ function assertTaskBranch(repositoryRoot, task) {
5708
7231
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected Task branch ${task.taskBranch}, found ${branch ?? 'detached HEAD'}.`);
5709
7232
  }
5710
7233
  }
5711
- function preserveCompletedStep(task, next) {
5712
- const previous = task.steps.find((candidate) => candidate.id === next.id && candidate.status === 'completed');
5713
- if (!previous)
5714
- return null;
5715
- const comparable = ({ status: _status, evidence: _evidence, ...definition }) => definition;
5716
- return JSON.stringify(comparable(previous)) === JSON.stringify(next) ? previous : null;
5717
- }
5718
7234
  function stripStepRuntime(step) {
5719
- const { status: _status, evidence: _evidence, ...definition } = step;
5720
- return definition;
7235
+ return normalizePlanStep(step);
7236
+ }
7237
+ function normalizePlanStep(step) {
7238
+ return normalizePlanStepDefinition(step);
7239
+ }
7240
+ function parseWorkflowBetaVersion(value) {
7241
+ const match = value?.match(/^2[.]0[.]0-beta[.]([0-9]+)(?:[.]([0-9]+))?$/);
7242
+ if (!match)
7243
+ return null;
7244
+ return {
7245
+ train: Number(match[1]),
7246
+ patch: match[2] === undefined ? null : Number(match[2]),
7247
+ };
5721
7248
  }
5722
7249
  function validateReviewInput(review) {
5723
7250
  if (!review.reviewer.trim())
@@ -6091,7 +7618,32 @@ function gitMutationPolicyForAction(task, action) {
6091
7618
  return null;
6092
7619
  }
6093
7620
  }
6094
- function writerTokenContractForAction(action, claimedC1Posture = false) {
7621
+ function taskWithEffectiveStepAllowedWrites(store, task, stepId) {
7622
+ const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
7623
+ return {
7624
+ ...task,
7625
+ steps: task.steps.map((step) => step.id === stepId ? { ...step, allowedWrites } : step),
7626
+ };
7627
+ }
7628
+ function reviewScopeAugmentations(store, task, stepId) {
7629
+ const step = requireStep(task, stepId);
7630
+ const paths = effectiveStepAllowedWrites(store, task, stepId)
7631
+ .filter((candidate) => !pathAllowed(candidate, step.allowedWrites));
7632
+ return paths.length === 0 ? [] : [{
7633
+ stepId,
7634
+ paths,
7635
+ authority: 'machine-derived-check-support',
7636
+ }];
7637
+ }
7638
+ function reviewCauseBindings(event) {
7639
+ return (event.remediationCauses ?? []).map((cause) => ({
7640
+ authority: 'external-reviewer',
7641
+ causeId: cause.causeId,
7642
+ causeFingerprint: cause.causeFingerprint,
7643
+ planConflict: cause.planConflict,
7644
+ }));
7645
+ }
7646
+ function writerTokenContractForAction(action, claimedC1Posture = false, existingLease = false) {
6095
7647
  const alwaysLeaseBound = [
6096
7648
  'task step-complete',
6097
7649
  'task downstream-proof-recover',
@@ -6117,16 +7669,19 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
6117
7669
  'task handback',
6118
7670
  'task handback-create',
6119
7671
  ];
6120
- if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
7672
+ if (!alwaysLeaseBound.includes(action)
7673
+ && !(claimedC1Posture && claimedC1LeaseBound.includes(action))
7674
+ && !(existingLease && ['task run', 'task step-review', 'task step-review-record'].includes(action))) {
6121
7675
  return null;
6122
7676
  }
6123
7677
  return {
6124
7678
  required: true,
6125
- option: '--writer-token',
6126
- source: 'active-writer-lease',
6127
- sensitive: true,
6128
- renewalDisposition: 'reuse-presented-token',
6129
- tokenReissuedOnRenewal: false,
7679
+ option: '--writer-credential-ref',
7680
+ source: 'local-credential-vault',
7681
+ transport: 'local-vault-v1',
7682
+ secretInArgv: false,
7683
+ secretInOutput: false,
7684
+ renewalDisposition: 'reuse-reference',
6130
7685
  };
6131
7686
  }
6132
7687
  function observationPreflightBlockers(observation) {