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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) 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.js +3 -1
  22. package/dist/src/alpha6/plan-risk.js.map +1 -1
  23. package/dist/src/alpha6/remediation-cause.d.ts +13 -0
  24. package/dist/src/alpha6/remediation-cause.js +77 -0
  25. package/dist/src/alpha6/remediation-cause.js.map +1 -0
  26. package/dist/src/alpha6/remediation.d.ts +15 -4
  27. package/dist/src/alpha6/remediation.js +430 -67
  28. package/dist/src/alpha6/remediation.js.map +1 -1
  29. package/dist/src/alpha6/review.js +34 -4
  30. package/dist/src/alpha6/review.js.map +1 -1
  31. package/dist/src/artifacts.js +6 -0
  32. package/dist/src/artifacts.js.map +1 -1
  33. package/dist/src/cli-actions.d.ts +25 -0
  34. package/dist/src/cli-actions.js +109 -0
  35. package/dist/src/cli-actions.js.map +1 -0
  36. package/dist/src/cli.js +216 -258
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/contracts.d.ts +166 -5
  39. package/dist/src/credential-transport.d.ts +38 -0
  40. package/dist/src/credential-transport.js +156 -0
  41. package/dist/src/credential-transport.js.map +1 -0
  42. package/dist/src/dependency-provenance.d.ts +23 -2
  43. package/dist/src/dependency-provenance.js +221 -34
  44. package/dist/src/dependency-provenance.js.map +1 -1
  45. package/dist/src/domain/completed-step-carryover.d.ts +13 -0
  46. package/dist/src/domain/completed-step-carryover.js +71 -0
  47. package/dist/src/domain/completed-step-carryover.js.map +1 -0
  48. package/dist/src/domain/validation.d.ts +9 -1
  49. package/dist/src/domain/validation.js +86 -0
  50. package/dist/src/domain/validation.js.map +1 -1
  51. package/dist/src/gateway-handshake.d.ts +11 -0
  52. package/dist/src/gateway-handshake.js +92 -0
  53. package/dist/src/gateway-handshake.js.map +1 -0
  54. package/dist/src/index.d.ts +2 -0
  55. package/dist/src/index.js +2 -0
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
  58. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
  59. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
  60. package/dist/src/lifecycle/corrective-replan.js +1 -1
  61. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  62. package/dist/src/lifecycle/semantic-registry.js +1 -1
  63. package/dist/src/observation.js +1 -2
  64. package/dist/src/observation.js.map +1 -1
  65. package/dist/src/observed-routes.d.ts +114 -0
  66. package/dist/src/observed-routes.js +545 -0
  67. package/dist/src/observed-routes.js.map +1 -0
  68. package/dist/src/operational-contract.d.ts +26 -0
  69. package/dist/src/operational-contract.js +94 -0
  70. package/dist/src/operational-contract.js.map +1 -0
  71. package/dist/src/reviewer.d.ts +218 -8
  72. package/dist/src/reviewer.js +927 -25
  73. package/dist/src/reviewer.js.map +1 -1
  74. package/dist/src/state/corrective-replan-executor.js +18 -15
  75. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  76. package/dist/src/state/corrective-replan-public.js +35 -10
  77. package/dist/src/state/corrective-replan-public.js.map +1 -1
  78. package/dist/src/state/corrective-replan-transaction.js +12 -4
  79. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  80. package/dist/src/state/corrective-yield-transaction.js +11 -2
  81. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  82. package/dist/src/state/lock.d.ts +1 -0
  83. package/dist/src/state/lock.js +24 -0
  84. package/dist/src/state/lock.js.map +1 -1
  85. package/dist/src/version.d.ts +1 -1
  86. package/dist/src/version.js +1 -1
  87. package/dist/src/version.js.map +1 -1
  88. package/dist/src/workflow.d.ts +65 -4
  89. package/dist/src/workflow.js +1704 -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 +181 -22
  98. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +148 -26
  99. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +326 -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 +83 -9
  104. package/docs/updating-existing-project.md +205 -0
  105. package/docs/validation-report.md +156 -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/remediation-event.schema.json +17 -1
  113. package/schemas/review-result.schema.json +1 -0
  114. package/schemas/step-review-event.schema.json +15 -0
  115. package/schemas/task.schema.json +61 -1
  116. 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,
@@ -1678,7 +2086,12 @@ export class WorkflowService {
1678
2086
  requiredAction: 'task plan-set',
1679
2087
  });
1680
2088
  }
1681
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
2089
+ const correctiveAuthority = delegationGrantId === null
2090
+ ? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
2091
+ : null;
2092
+ const delegation = correctiveAuthority
2093
+ ? null
2094
+ : this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
1682
2095
  const event = {
1683
2096
  kind: 'execution',
1684
2097
  decision: 'approved',
@@ -1690,7 +2103,11 @@ export class WorkflowService {
1690
2103
  evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
1691
2104
  headCommit: mechanicalFeasibility.evaluatedHead,
1692
2105
  reason,
1693
- authorizationMode: delegation ? 'delegated' : 'human',
2106
+ authorizationMode: correctiveAuthority
2107
+ ? 'corrective-derived'
2108
+ : delegation
2109
+ ? 'delegated'
2110
+ : 'human',
1694
2111
  ...(delegation ? { delegation } : {}),
1695
2112
  mechanicalFeasibility,
1696
2113
  };
@@ -1875,7 +2292,7 @@ export class WorkflowService {
1875
2292
  const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1876
2293
  const action = String(navigation.action);
1877
2294
  const currentLease = context.locks.inspect(taskId);
1878
- const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
2295
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
1879
2296
  const requiredActor = writerTokenContract
1880
2297
  ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1881
2298
  : null;
@@ -1889,9 +2306,7 @@ export class WorkflowService {
1889
2306
  nextAction: action,
1890
2307
  });
1891
2308
  }
1892
- const lease = currentLease
1893
- ? context.locks.replaceCredential(taskId, normalizedActor)
1894
- : context.locks.acquire(taskId, normalizedActor);
2309
+ const lease = context.locks.replaceOrRecoverCredential(taskId, normalizedActor);
1895
2310
  if (posture.state === 'claimed') {
1896
2311
  appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1897
2312
  }
@@ -2010,6 +2425,7 @@ export class WorkflowService {
2010
2425
  }
2011
2426
  assertTaskBranch(context.identity.repositoryRoot, task);
2012
2427
  const step = requireStep(task, stepId);
2428
+ const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2013
2429
  const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
2014
2430
  if (!['planned', 'failed'].includes(step.status)) {
2015
2431
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
@@ -2044,18 +2460,25 @@ export class WorkflowService {
2044
2460
  const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
2045
2461
  ? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
2046
2462
  : null;
2463
+ const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2464
+ ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2465
+ : null;
2047
2466
  if (step.status === 'planned'
2048
2467
  && dirty.length > 0
2049
2468
  && !resumesCorrectiveReplan
2050
- && !downstreamProofCarryover?.eligible) {
2469
+ && !downstreamProofCarryover?.eligible
2470
+ && !planIntegrityDirtyCarryover?.eligible) {
2051
2471
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2052
2472
  changedFiles: dirty,
2053
2473
  ...(downstreamProofCarryover?.applicable
2054
2474
  ? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
2055
2475
  : {}),
2476
+ ...(planIntegrityDirtyCarryover?.applicable
2477
+ ? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
2478
+ : {}),
2056
2479
  });
2057
2480
  }
2058
- const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
2481
+ const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
2059
2482
  if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
2060
2483
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
2061
2484
  changedFiles: dirtyOutsideStep,
@@ -2102,7 +2525,9 @@ export class WorkflowService {
2102
2525
  return context.locks.acquire(taskId, actor.trim() || 'worker');
2103
2526
  })();
2104
2527
  }
2105
- const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
2528
+ const steps = task.steps.map((candidate) => candidate.id === stepId
2529
+ ? { ...candidate, status: 'in_progress', evidence: null, failurePause: null }
2530
+ : candidate);
2106
2531
  let saved;
2107
2532
  try {
2108
2533
  saved = this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
@@ -2126,7 +2551,7 @@ export class WorkflowService {
2126
2551
  path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
2127
2552
  ],
2128
2553
  allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
2129
- allowedWrites: step.allowedWrites,
2554
+ allowedWrites,
2130
2555
  commands: step.checks,
2131
2556
  forbiddenScope: [
2132
2557
  ...step.forbiddenScope,
@@ -2245,6 +2670,7 @@ export class WorkflowService {
2245
2670
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
2246
2671
  assertTaskBranch(context.identity.repositoryRoot, task);
2247
2672
  const step = requireStep(task, stepId);
2673
+ const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
2248
2674
  if (task.status !== 'in_progress' || step.status !== 'in_progress') {
2249
2675
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
2250
2676
  }
@@ -2277,18 +2703,114 @@ export class WorkflowService {
2277
2703
  const checks = runChecks(context.identity.repositoryRoot, step.checks);
2278
2704
  const failed = checks.filter((check) => !check.passed);
2279
2705
  if (failed.length > 0) {
2280
- if (reviewRequired && planRiskAudit) {
2281
- appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
2706
+ const now = this.now();
2707
+ const normalizedFailures = normalizeMechanicalFailures(failed, context.identity.repositoryRoot);
2708
+ // Bind useful uncommitted work to this failure without treating it as
2709
+ // completed-Step authority.
2710
+ const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
2711
+ const failurePause = {
2712
+ recordedAt: now.toISOString(),
2713
+ dirtyFiles,
2714
+ dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
2715
+ checksHash: sha256Hex(canonicalJsonStringify(checks)),
2716
+ failedChecks: normalizedFailures,
2717
+ };
2718
+ const preparedRemediation = reviewRequired && planRiskAudit
2719
+ ? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
2282
2720
  completionCommit: headCommit(context.identity.repositoryRoot),
2283
2721
  checksHash: sha256Hex(canonicalJsonStringify(checks)),
2284
2722
  reviewEventHash: null,
2285
2723
  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);
2724
+ executionTaskRevision: task.revision,
2725
+ dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
2726
+ }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
2727
+ : null;
2728
+ const steps = task.steps.map((candidate) => candidate.id === stepId
2729
+ ? { ...candidate, status: 'failed', evidence: null, failurePause }
2730
+ : candidate);
2731
+ const paused = {
2732
+ ...task,
2733
+ revision: task.revision + 1,
2734
+ updatedAt: now.toISOString(),
2735
+ status: 'needs_fix',
2736
+ steps,
2737
+ blockReason: `${stepId} checks failed.`,
2738
+ };
2739
+ const stepHistory = readRemediationEvents(this.store, task)
2740
+ .filter((event) => event.stepId === stepId);
2741
+ const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
2742
+ ? stepHistory
2743
+ : [...stepHistory, preparedRemediation.event];
2744
+ const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
2745
+ const c1Posture = readTaskC1Posture(this.store, task);
2746
+ const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
2747
+ ? prepareTaskCorrectiveYield(this.store, paused, {
2748
+ actor: c1Posture.claimant,
2749
+ writerToken: lease.token,
2750
+ reason: `Yield execution authority after repeated mechanical cause ${preparedRemediation.event.eventId}.`,
2751
+ eventId: `HOF-${sha256Hex([
2752
+ 'checks-failed-root-cause-replan-pause-v1',
2753
+ task.id,
2754
+ String(task.revision),
2755
+ preparedRemediation.event.eventHash,
2756
+ c1Posture.claimant,
2757
+ ].join('\0'))}`,
2758
+ }, now)
2759
+ : null;
2760
+ const projectRoot = this.store.projectRoot(task.projectId);
2761
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
2762
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2763
+ const failureHead = headCommit(context.identity.repositoryRoot);
2764
+ applyProjectTransaction({
2765
+ projectRoot,
2766
+ projectId: task.projectId,
2767
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
2768
+ kind: rollingCauseDecision === 'root-cause-replan'
2769
+ ? 'failed-step-root-cause-replan-pause'
2770
+ : 'failed-step-continuation',
2771
+ now,
2772
+ targets: [
2773
+ ...(preparedRemediation?.postimage
2774
+ ? [{
2775
+ path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
2776
+ content: preparedRemediation.postimage,
2777
+ }]
2778
+ : []),
2779
+ ...(preparedYield
2780
+ ? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
2781
+ : []),
2782
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
2783
+ { path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null },
2784
+ ],
2785
+ validate: () => {
2786
+ const saved = this.store.readTask(task.projectId, task.id);
2787
+ const savedStep = requireStep(saved, stepId);
2788
+ const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
2789
+ const remediationHash = preparedRemediation
2790
+ ? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
2791
+ : null;
2792
+ if (saved.revision !== paused.revision
2793
+ || saved.status !== 'needs_fix'
2794
+ || savedStep.status !== 'failed'
2795
+ || savedStep.failurePause?.dirtyWorktreeHash !== failurePause.dirtyWorktreeHash
2796
+ || savedStep.failurePause?.checksHash !== failurePause.checksHash
2797
+ || remediationHash !== (preparedRemediation?.event.eventHash ?? null)
2798
+ || context.locks.inspect(task.id) !== null
2799
+ || (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
2800
+ || headCommit(context.identity.repositoryRoot) !== failureHead
2801
+ || JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
2802
+ || hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== failurePause.dirtyWorktreeHash) {
2803
+ throw new WorkflowError('STATE_CORRUPT', 'Failed-Step continuation transaction readback failed.', {
2804
+ taskId,
2805
+ stepId,
2806
+ });
2807
+ }
2808
+ validateTaskHistory(context.identity.repositoryRoot, saved);
2809
+ },
2810
+ });
2811
+ return this.store.readTask(task.projectId, task.id);
2290
2812
  }
2291
- const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, step.allowedWrites);
2813
+ const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, allowedWrites);
2292
2814
  const evidence = {
2293
2815
  recordedAt: this.now().toISOString(),
2294
2816
  commitSha: committed.commitSha,
@@ -2327,7 +2849,7 @@ export class WorkflowService {
2327
2849
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
2328
2850
  const task = this.store.readTask(context.identity.projectId, taskId);
2329
2851
  assertExpectedRevision(task, expectedRevision);
2330
- this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2852
+ const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
2331
2853
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
2332
2854
  if (task.status !== 'in_progress' && task.status !== 'blocked') {
2333
2855
  throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
@@ -2398,18 +2920,14 @@ export class WorkflowService {
2398
2920
  resolution: currentCycle.resolution,
2399
2921
  });
2400
2922
  }
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));
2923
+ return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, currentCycle.reviewRecordedEvent, currentCycle.reviewerAttestationEvent.eventHash, planRiskAudit, undefined, reviewLease);
2407
2924
  }
2408
2925
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
2409
2926
  }
2410
2927
  ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2411
2928
  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);
2929
+ const reviewTask = taskWithEffectiveStepAllowedWrites(this.store, task, stepId);
2930
+ const launch = launchStrictStepReviewer(context.identity.repositoryRoot, reviewTask, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner, reviewScopeAugmentations(this.store, task, stepId));
2413
2931
  const recorded = recordStrictStepReview(this.store, task, stepId, {
2414
2932
  review: launch.review,
2415
2933
  reviewerOutputHash: launch.reviewerOutputHash,
@@ -2426,16 +2944,11 @@ export class WorkflowService {
2426
2944
  resolution: recorded.resolution,
2427
2945
  });
2428
2946
  }
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));
2947
+ return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewRecordedEvent, reviewerAttestationEvent.eventHash, planRiskAudit, launch.review.summary, reviewLease);
2435
2948
  }
2436
2949
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
2437
2950
  }
2438
- prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2951
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision, reviewMode = 'ordinary') {
2439
2952
  const identity = resolveRepositoryIdentity(repository);
2440
2953
  const task = this.store.readTask(identity.projectId, taskId);
2441
2954
  assertExpectedRevision(task, expectedRevision);
@@ -2455,15 +2968,18 @@ export class WorkflowService {
2455
2968
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2456
2969
  }
2457
2970
  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);
2971
+ return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, taskWithEffectiveStepAllowedWrites(this.store, task, stepId), this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, reviewScopeAugmentations(this.store, task, stepId), reviewMode);
2459
2972
  }
2460
2973
  recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2461
2974
  const context = this.#mutationContext(repository);
2462
2975
  const task = this.store.readTask(context.identity.projectId, taskId);
2463
2976
  assertExpectedRevision(task, expectedRevision);
2464
- this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2977
+ const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
2465
2978
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2466
- const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2979
+ if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
2980
+ throw new WorkflowError('INVALID_ARGUMENT', 'External Step Review requires reviewMode ordinary or security.');
2981
+ }
2982
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision, input.reviewMode);
2467
2983
  let review;
2468
2984
  try {
2469
2985
  review = validateExternalStrictStepReviewInput(input, packet);
@@ -2498,23 +3014,131 @@ export class WorkflowService {
2498
3014
  isolationResult: 'verified',
2499
3015
  }, this.now());
2500
3016
  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));
3017
+ const saved = this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, recorded.reviewRecordedEvent, recorded.reviewerAttestationEvent.eventHash, readCurrentPlanRiskAudit(this.store, task), review.summary, reviewLease);
3018
+ return {
3019
+ mode: 'external-sealed',
3020
+ reviewMode: packet.reviewMode,
3021
+ packetHash: packet.packetHash,
3022
+ repositorySealHash: packet.repositorySealHash,
3023
+ reviewerThreadId: input.reviewerThreadId,
3024
+ task: saved,
3025
+ };
2507
3026
  }
2508
3027
  const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2509
3028
  return {
2510
3029
  mode: 'external-sealed',
3030
+ reviewMode: packet.reviewMode,
2511
3031
  packetHash: packet.packetHash,
2512
3032
  repositorySealHash: packet.repositorySealHash,
2513
3033
  reviewerThreadId: input.reviewerThreadId,
2514
3034
  task: saved,
2515
3035
  };
2516
3036
  }
2517
- prepareExternalTaskReview(repository, taskId, expectedRevision) {
3037
+ finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewEvent, reviewerAttestationHash, planRiskAudit, summary, reviewLease) {
3038
+ const now = this.now();
3039
+ const preparedRemediation = prepareRemediationEvent(this.store, task, stepId, 'review-failed', {
3040
+ completionCommit: reviewEvent.completionCommit,
3041
+ checksHash: reviewEvent.checksHash,
3042
+ reviewEventHash: reviewEvent.eventHash,
3043
+ reviewerAttestationHash,
3044
+ }, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), reviewCauseBindings(reviewEvent));
3045
+ const stepHistory = readRemediationEvents(this.store, task)
3046
+ .filter((event) => event.stepId === stepId);
3047
+ const resultingHistory = preparedRemediation.postimage === null
3048
+ ? stepHistory
3049
+ : [...stepHistory, preparedRemediation.event];
3050
+ const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
3051
+ const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
3052
+ const paused = {
3053
+ ...resolved,
3054
+ revision: task.revision + 1,
3055
+ updatedAt: now.toISOString(),
3056
+ };
3057
+ const projectRoot = this.store.projectRoot(task.projectId);
3058
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
3059
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
3060
+ const failureHead = headCommit(context.identity.repositoryRoot);
3061
+ const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3062
+ const dirtyWorktreeHash = hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles);
3063
+ const c1Posture = readTaskC1Posture(this.store, task);
3064
+ if (rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && !reviewLease) {
3065
+ throw new WorkflowError('STATE_CONFLICT', 'Root-cause replan pause requires the claimed writer authority it will release.', {
3066
+ taskId: task.id,
3067
+ stepId,
3068
+ });
3069
+ }
3070
+ const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && reviewLease
3071
+ ? prepareTaskCorrectiveYield(this.store, paused, {
3072
+ actor: c1Posture.claimant,
3073
+ writerToken: reviewLease.token,
3074
+ reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
3075
+ eventId: `HOF-${sha256Hex([
3076
+ 'strict-review-root-cause-replan-pause-v1',
3077
+ task.id,
3078
+ String(task.revision),
3079
+ preparedRemediation.event.eventHash,
3080
+ c1Posture.claimant,
3081
+ ].join('\0'))}`,
3082
+ }, now)
3083
+ : null;
3084
+ if (reviewLease) {
3085
+ const currentLease = context.locks.inspect(task.id);
3086
+ if (!currentLease || currentLease.token !== reviewLease.token || currentLease.owner !== reviewLease.owner) {
3087
+ throw new WorkflowError('STATE_CONFLICT', 'Strict Step Review writer authority changed before the failure pause could be recorded.', {
3088
+ taskId: task.id,
3089
+ stepId,
3090
+ });
3091
+ }
3092
+ }
3093
+ applyProjectTransaction({
3094
+ projectRoot,
3095
+ projectId: task.projectId,
3096
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
3097
+ kind: rollingCauseDecision === 'root-cause-replan'
3098
+ ? 'strict-review-root-cause-replan-pause'
3099
+ : 'strict-review-failed-step-pause',
3100
+ now,
3101
+ targets: [
3102
+ ...(preparedRemediation.postimage
3103
+ ? [{
3104
+ path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
3105
+ content: preparedRemediation.postimage,
3106
+ }]
3107
+ : []),
3108
+ ...(preparedYield
3109
+ ? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
3110
+ : []),
3111
+ { path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
3112
+ ...(reviewLease
3113
+ ? [{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null }]
3114
+ : []),
3115
+ ],
3116
+ validate: () => {
3117
+ const saved = this.store.readTask(task.projectId, task.id);
3118
+ const savedStep = requireStep(saved, stepId);
3119
+ const savedRemediation = readRemediationEvents(this.store, saved)
3120
+ .find((event) => event.eventId === preparedRemediation.event.eventId) ?? null;
3121
+ const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
3122
+ if (saved.revision !== paused.revision
3123
+ || saved.status !== 'needs_fix'
3124
+ || savedStep.status !== 'failed'
3125
+ || savedRemediation?.eventHash !== preparedRemediation.event.eventHash
3126
+ || context.locks.inspect(task.id) !== null
3127
+ || (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
3128
+ || headCommit(context.identity.repositoryRoot) !== failureHead
3129
+ || JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
3130
+ || hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== dirtyWorktreeHash) {
3131
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review failure-pause transaction readback failed.', {
3132
+ taskId: task.id,
3133
+ stepId,
3134
+ });
3135
+ }
3136
+ validateTaskHistory(context.identity.repositoryRoot, saved);
3137
+ },
3138
+ });
3139
+ return this.store.readTask(task.projectId, task.id);
3140
+ }
3141
+ prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
2518
3142
  const identity = resolveRepositoryIdentity(repository);
2519
3143
  const task = this.store.readTask(identity.projectId, taskId);
2520
3144
  assertExpectedRevision(task, expectedRevision);
@@ -2531,7 +3155,7 @@ export class WorkflowService {
2531
3155
  if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2532
3156
  throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2533
3157
  }
2534
- return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
3158
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'), task.steps.flatMap((step) => reviewScopeAugmentations(this.store, task, step.id)), reviewMode);
2535
3159
  }
2536
3160
  recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2537
3161
  const context = this.#mutationContext(repository);
@@ -2539,7 +3163,10 @@ export class WorkflowService {
2539
3163
  assertExpectedRevision(task, expectedRevision);
2540
3164
  this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2541
3165
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2542
- const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
3166
+ if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
3167
+ throw new WorkflowError('INVALID_ARGUMENT', 'External Task Review requires reviewMode ordinary or security.');
3168
+ }
3169
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision, input.reviewMode);
2543
3170
  let review;
2544
3171
  try {
2545
3172
  review = validateExternalStrictTaskReviewInput(input, packet);
@@ -2553,6 +3180,7 @@ export class WorkflowService {
2553
3180
  const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2554
3181
  return {
2555
3182
  mode: 'external-sealed',
3183
+ reviewMode: packet.reviewMode,
2556
3184
  packetHash: packet.packetHash,
2557
3185
  repositorySealHash: packet.repositorySealHash,
2558
3186
  reviewerThreadId: input.reviewerThreadId,
@@ -2657,8 +3285,6 @@ export class WorkflowService {
2657
3285
  const task = this.store.readTask(context.identity.projectId, taskId);
2658
3286
  assertExpectedRevision(task, expectedRevision);
2659
3287
  this.requireExecutionAdoptionPosture(context.identity, taskId);
2660
- this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2661
- this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2662
3288
  assertTaskBranch(context.identity.repositoryRoot, task);
2663
3289
  this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2664
3290
  this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
@@ -2690,6 +3316,20 @@ export class WorkflowService {
2690
3316
  blockers: assessment.blockers,
2691
3317
  });
2692
3318
  }
3319
+ const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
3320
+ ? [assessment.evidence.remediationEventId]
3321
+ : assessment.evidence.remediationEvents.map((event) => event.eventId);
3322
+ const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
3323
+ && record.packageVersion === PACKAGE_VERSION
3324
+ && task.systemCommits.includes(record.commitSha)
3325
+ && record.planIntegrity?.stepId === stepId
3326
+ && record.planIntegrity.planHash === task.planHash
3327
+ && canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
3328
+ && record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
3329
+ if (!compatibility) {
3330
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
3331
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
3332
+ }
2693
3333
  const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
2694
3334
  return { assessment, event };
2695
3335
  }
@@ -2775,7 +3415,12 @@ export class WorkflowService {
2775
3415
  this.assertReviewFresh(identity.repositoryRoot, task);
2776
3416
  const resultHash = this.store.hashArtifact(root, 'result.md');
2777
3417
  const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
2778
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
3418
+ const correctiveAuthority = delegationGrantId === null
3419
+ ? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
3420
+ : null;
3421
+ const delegation = correctiveAuthority
3422
+ ? null
3423
+ : this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
2779
3424
  const event = {
2780
3425
  kind: 'final_acceptance',
2781
3426
  decision: 'approved',
@@ -2787,7 +3432,11 @@ export class WorkflowService {
2787
3432
  evidenceHash,
2788
3433
  headCommit: headCommit(identity.repositoryRoot),
2789
3434
  reason: '',
2790
- authorizationMode: delegation ? 'delegated' : 'human',
3435
+ authorizationMode: correctiveAuthority
3436
+ ? 'corrective-derived'
3437
+ : delegation
3438
+ ? 'delegated'
3439
+ : 'human',
2791
3440
  ...(delegation ? { delegation } : {}),
2792
3441
  };
2793
3442
  return this.store.writeTask({
@@ -2890,7 +3539,9 @@ export class WorkflowService {
2890
3539
  return saved;
2891
3540
  }
2892
3541
  status(repository) {
2893
- const snapshot = this.observationSnapshot(repository);
3542
+ return this.statusFromSnapshot(this.observationSnapshot(repository));
3543
+ }
3544
+ statusFromSnapshot(snapshot) {
2894
3545
  return {
2895
3546
  projectId: snapshot.identity.projectId,
2896
3547
  discoveries: snapshot.discoveries,
@@ -2904,6 +3555,73 @@ export class WorkflowService {
2904
3555
  observation: snapshot.observation,
2905
3556
  };
2906
3557
  }
3558
+ compactStatus(repository) {
3559
+ const snapshot = this.observationSnapshot(repository);
3560
+ const status = this.statusFromSnapshot(snapshot);
3561
+ const next = this.nextFromStatus(repository, null, status);
3562
+ const routedId = typeof next.id === 'string'
3563
+ ? next.id
3564
+ : typeof next.taskId === 'string'
3565
+ ? next.taskId
3566
+ : null;
3567
+ const activeTask = routedId === null
3568
+ ? null
3569
+ : snapshot.tasks.find((task) => task.id === routedId) ?? null;
3570
+ const routedStepId = typeof next.stepId === 'string' ? next.stepId : null;
3571
+ const activeStep = activeTask === null
3572
+ ? null
3573
+ : routedStepId === null
3574
+ ? activeTask.steps.find((step) => step.status === 'in_progress')
3575
+ ?? activeTask.steps.find((step) => step.status === 'failed')
3576
+ ?? null
3577
+ : activeTask.steps.find((step) => step.id === routedStepId) ?? null;
3578
+ const action = typeof next.action === 'string' ? next.action : 'blocked';
3579
+ const blockedAction = typeof next.blockedAction === 'string' ? next.blockedAction : null;
3580
+ const navigation = next.navigation && typeof next.navigation === 'object' && !Array.isArray(next.navigation)
3581
+ ? next.navigation
3582
+ : null;
3583
+ const route = {
3584
+ scope: typeof next.scope === 'string' ? next.scope : null,
3585
+ status: typeof next.status === 'string' ? next.status : null,
3586
+ entityId: routedId,
3587
+ navigationKind: navigation && typeof navigation.kind === 'string' ? navigation.kind : null,
3588
+ transitionId: navigation && typeof navigation.transitionId === 'string' ? navigation.transitionId : null,
3589
+ };
3590
+ const routeFingerprint = sha256Hex(canonicalJsonStringify({
3591
+ projectId: snapshot.identity.projectId,
3592
+ taskId: activeTask?.id ?? null,
3593
+ taskRevision: activeTask?.revision ?? null,
3594
+ stepId: activeStep?.id ?? routedStepId,
3595
+ action,
3596
+ blockedAction,
3597
+ route,
3598
+ observationKind: snapshot.observation.kind,
3599
+ observationReasonCodes: snapshot.observation.reasonCodes,
3600
+ }));
3601
+ return {
3602
+ projection: 'codex-workflow-compact-status-v1',
3603
+ bounded: true,
3604
+ projectId: snapshot.identity.projectId,
3605
+ counts: {
3606
+ discoveries: snapshot.discoveries.length,
3607
+ tasks: snapshot.tasks.length,
3608
+ milestones: snapshot.milestones.length,
3609
+ delegations: snapshot.delegations.length,
3610
+ },
3611
+ activeTask: activeTask
3612
+ ? { id: activeTask.id, revision: activeTask.revision, status: activeTask.status }
3613
+ : null,
3614
+ activeStep: activeStep ? { id: activeStep.id, status: activeStep.status } : null,
3615
+ action,
3616
+ blockedAction,
3617
+ routeFingerprint,
3618
+ route,
3619
+ observation: {
3620
+ kind: snapshot.observation.kind,
3621
+ reasonCodes: [...snapshot.observation.reasonCodes],
3622
+ },
3623
+ };
3624
+ }
2907
3625
  recoverMilestoneTransactions(repository) {
2908
3626
  const { identity } = this.#mutationContext(repository);
2909
3627
  const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
@@ -3239,12 +3957,179 @@ export class WorkflowService {
3239
3957
  dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
3240
3958
  requiredTransport: [
3241
3959
  '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.',
3960
+ 'Commit only the same dependency files on the Task branch while preserving all product bytes.',
3961
+ 'Install the exact target package, then follow fresh next to dependency registration.',
3962
+ ],
3963
+ blockers,
3964
+ };
3965
+ }
3966
+ planIntegrityUpdatePreflight(repository, taskId) {
3967
+ const snapshot = this.observationSnapshot(repository);
3968
+ if (snapshot.observation.kind !== 'assessment') {
3969
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
3970
+ observation: snapshot.observation,
3971
+ });
3972
+ }
3973
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
3974
+ if (!task)
3975
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3976
+ const assessments = task.steps
3977
+ .filter((step) => step.status === 'failed')
3978
+ .map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
3979
+ .filter((assessment) => assessment.applicable);
3980
+ const assessment = assessments.length === 1 ? assessments[0] : null;
3981
+ const adoptedCompatibility = assessment
3982
+ ? null
3983
+ : this.postRebindCheckSupportCompatibility(snapshot, task);
3984
+ const dirtyCarryoverAssessment = assessment || adoptedCompatibility
3985
+ ? null
3986
+ : this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
3987
+ const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
3988
+ && dirtyCarryoverAssessment.compatibility
3989
+ ? {
3990
+ ...dirtyCarryoverAssessment.compatibility,
3991
+ planHash: task.planHash,
3992
+ mode: 'post-rebind-dirty-carryover',
3993
+ }
3994
+ : null;
3995
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
3996
+ const versionSurfaces = {
3997
+ declared: versions.declared,
3998
+ locked: versions.locked,
3999
+ installed: versions.installed,
4000
+ currentBranch: versions.currentBranch,
4001
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4002
+ };
4003
+ const sourceVersion = versionSurfaces.declared;
4004
+ const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
4005
+ const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
4006
+ const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
4007
+ && sourceBetaVersion.patch !== null
4008
+ && sourceBetaVersion.patch >= 11;
4009
+ const targetIsNewer = sourceBetaVersion !== null
4010
+ && targetBetaVersion !== null
4011
+ && (targetBetaVersion.train > sourceBetaVersion.train
4012
+ || (targetBetaVersion.train === sourceBetaVersion.train
4013
+ && targetBetaVersion.patch !== null
4014
+ && sourceBetaVersion.patch !== null
4015
+ && targetBetaVersion.patch > sourceBetaVersion.patch));
4016
+ const blockers = [
4017
+ ...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
4018
+ ...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
4019
+ ? []
4020
+ : (dirtyCarryoverAssessment?.blockers.length
4021
+ ? dirtyCarryoverAssessment.blockers
4022
+ : ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
4023
+ ...(!sourceIsEligibleBeta12 || !targetIsNewer
4024
+ ? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
4025
+ : []),
4026
+ ...Object.entries(versionSurfaces)
4027
+ .filter(([, version]) => version !== sourceVersion)
4028
+ .map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
4029
+ ...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
4030
+ ...observationPreflightBlockers(snapshot.observation),
4031
+ ];
4032
+ return {
4033
+ readOnly: true,
4034
+ eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
4035
+ action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
4036
+ ? 'update plan-integrity-transport'
4037
+ : null,
4038
+ projectId: task.projectId,
4039
+ taskId: task.id,
4040
+ taskRevision: task.revision,
4041
+ stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
4042
+ sourceVersion,
4043
+ targetVersion: PACKAGE_VERSION,
4044
+ taskBranch: task.taskBranch,
4045
+ baseBranch: task.baseBranch,
4046
+ dirtyFiles: assessment?.evidence?.changedFiles
4047
+ ?? adoptedCompatibility?.dirtyFiles
4048
+ ?? dirtyCarryoverCompatibility?.dirtyFiles
4049
+ ?? [],
4050
+ conflictHash: assessment?.evidence?.conflictHash
4051
+ ?? adoptedCompatibility?.conflictHash
4052
+ ?? dirtyCarryoverCompatibility?.conflictHash
4053
+ ?? null,
4054
+ compatibilityMode: adoptedCompatibility?.mode
4055
+ ?? dirtyCarryoverCompatibility?.mode
4056
+ ?? 'failed-step-conflict',
4057
+ requiredTransport: [
4058
+ 'Commit only package.json and package-lock.json on the active Milestone base.',
4059
+ 'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
4060
+ 'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
3244
4061
  ],
3245
4062
  blockers,
3246
4063
  };
3247
4064
  }
4065
+ planIntegrityDependencyRecoveryPreflight(repository, taskId) {
4066
+ const snapshot = this.observationSnapshot(repository);
4067
+ if (snapshot.observation.kind !== 'assessment') {
4068
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
4069
+ observation: snapshot.observation,
4070
+ });
4071
+ }
4072
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4073
+ if (!task)
4074
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4075
+ const assessmentBlockers = [];
4076
+ const assessments = task.steps
4077
+ .filter((step) => step.status === 'failed')
4078
+ .flatMap((step) => {
4079
+ try {
4080
+ return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
4081
+ }
4082
+ catch (error) {
4083
+ if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
4084
+ assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
4085
+ return [];
4086
+ }
4087
+ throw error;
4088
+ }
4089
+ })
4090
+ .filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
4091
+ const assessment = assessments.length === 1 ? assessments[0] : null;
4092
+ const adoptedCompatibility = assessment?.evidence
4093
+ ? null
4094
+ : this.postRebindCheckSupportCompatibility(snapshot, task);
4095
+ const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
4096
+ ? null
4097
+ : this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
4098
+ const carriedCompatibility = !assessment?.evidence && task.planHash
4099
+ ? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
4100
+ : null;
4101
+ if ((!assessment?.evidence
4102
+ && !carriedCompatibility
4103
+ && !adoptedCompatibility
4104
+ && !dirtyCarryoverCompatibility) || !task.planHash) {
4105
+ const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
4106
+ return {
4107
+ ...ordinary,
4108
+ eligible: false,
4109
+ action: null,
4110
+ planIntegrity: null,
4111
+ blockers: [...new Set([
4112
+ ...ordinary.blockers,
4113
+ ...assessmentBlockers,
4114
+ 'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
4115
+ ])],
4116
+ };
4117
+ }
4118
+ const compatibility = carriedCompatibility
4119
+ ?? adoptedCompatibility
4120
+ ?? dirtyCarryoverCompatibility
4121
+ ?? {
4122
+ stepId: assessment.stepId,
4123
+ planHash: task.planHash,
4124
+ conflictHash: assessment.evidence.conflictHash,
4125
+ dirtyFiles: [...assessment.evidence.changedFiles],
4126
+ dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
4127
+ remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
4128
+ ? [assessment.evidence.remediationEventId]
4129
+ : assessment.evidence.remediationEvents.map((event) => event.eventId),
4130
+ };
4131
+ return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
4132
+ }
3248
4133
  downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
3249
4134
  const snapshot = this.observationSnapshot(repository);
3250
4135
  if (snapshot.observation.kind !== 'assessment') {
@@ -3299,6 +4184,9 @@ export class WorkflowService {
3299
4184
  recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
3300
4185
  return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
3301
4186
  }
4187
+ recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
4188
+ return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
4189
+ }
3302
4190
  recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
3303
4191
  const normalizedActor = actor.trim();
3304
4192
  const normalizedReason = reason.trim();
@@ -3315,7 +4203,9 @@ export class WorkflowService {
3315
4203
  ? Boolean(record.downstreamProofReplan)
3316
4204
  : recoveryMode === 'historical-step'
3317
4205
  ? Boolean(record.historicalStepProvenance)
3318
- : Boolean(record.activeDownstreamProof) === recoveryMode));
4206
+ : recoveryMode === 'plan-integrity'
4207
+ ? Boolean(record.planIntegrity)
4208
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
3319
4209
  if (prior && current.systemCommits.includes(currentHead))
3320
4210
  return current;
3321
4211
  assertExpectedRevision(current, expectedRevision);
@@ -3327,42 +4217,113 @@ export class WorkflowService {
3327
4217
  ? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
3328
4218
  : recoveryMode === 'historical-step'
3329
4219
  ? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
3330
- : recoveryMode
3331
- ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
3332
- : this.inspectDependencyProvenanceCandidate(snapshot, task);
4220
+ : recoveryMode === 'plan-integrity'
4221
+ ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
4222
+ : recoveryMode
4223
+ ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4224
+ : this.inspectDependencyProvenanceCandidate(snapshot, task);
3333
4225
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
3334
4226
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
3335
4227
  taskId,
3336
4228
  blockers: preflight.blockers,
3337
4229
  });
3338
4230
  }
3339
- return context.store.writeTask({
4231
+ const now = this.now();
4232
+ const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
4233
+ const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
4234
+ return {
4235
+ commitSha: dependencyCommit.commitSha,
4236
+ parentCommitSha: dependencyCommit.parentCommitSha,
4237
+ packageName: PACKAGE_NAME,
4238
+ packageVersion: dependencyCommit.packageVersion,
4239
+ files: ['package-lock.json', 'package.json'],
4240
+ actor: normalizedActor,
4241
+ reason: normalizedReason,
4242
+ recordedAt: now.toISOString(),
4243
+ retainedHistoricalCommits: isTerminalCommit ? preflight.retainedHistoricalCommits : [],
4244
+ ...(isTerminalCommit && preflight.activeDownstreamProof
4245
+ ? { activeDownstreamProof: preflight.activeDownstreamProof }
4246
+ : {}),
4247
+ ...(isTerminalCommit && preflight.downstreamProofReplan
4248
+ ? { downstreamProofReplan: preflight.downstreamProofReplan }
4249
+ : {}),
4250
+ ...(isTerminalCommit && preflight.historicalStepProvenance
4251
+ ? { historicalStepProvenance: preflight.historicalStepProvenance }
4252
+ : {}),
4253
+ ...(isTerminalCommit && preflight.planIntegrity
4254
+ ? { planIntegrity: preflight.planIntegrity }
4255
+ : {}),
4256
+ };
4257
+ });
4258
+ const nextTask = {
3340
4259
  ...task,
3341
- systemCommits: [...task.systemCommits, preflight.commitSha],
4260
+ revision: task.revision + 1,
4261
+ updatedAt: now.toISOString(),
4262
+ systemCommits: [
4263
+ ...task.systemCommits,
4264
+ ...preflight.dependencyCommits.map((dependencyCommit) => dependencyCommit.commitSha),
4265
+ ],
3342
4266
  dependencyProvenanceRecoveries: [
3343
4267
  ...(task.dependencyProvenanceRecoveries ?? []),
4268
+ ...recoveredRecords,
4269
+ ],
4270
+ };
4271
+ if (recoveryMode !== 'plan-integrity' || !preflight.planIntegrity) {
4272
+ return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
4273
+ }
4274
+ const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
4275
+ const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
4276
+ const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
4277
+ let refreshedKnowledgeMap = null;
4278
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
4279
+ const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
4280
+ if (!knowledgeRefresh.eligible) {
4281
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
4282
+ taskId,
4283
+ dependencyKnowledgeRefresh: knowledgeRefresh,
4284
+ });
4285
+ }
4286
+ const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
4287
+ const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
4288
+ if (!approved.approval) {
4289
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
4290
+ }
4291
+ refreshedKnowledgeMap = {
4292
+ ...approved,
4293
+ revision: currentKnowledgeMap.revision + 2,
4294
+ createdAt: currentKnowledgeMap.createdAt,
4295
+ updatedAt: now.toISOString(),
4296
+ approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
4297
+ };
4298
+ }
4299
+ const projectRoot = this.store.projectRoot(task.projectId);
4300
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
4301
+ applyProjectTransaction({
4302
+ projectRoot,
4303
+ projectId: task.projectId,
4304
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
4305
+ kind: 'plan-integrity-dependency-recovery',
4306
+ now,
4307
+ targets: [
4308
+ ...(refreshedKnowledgeMap
4309
+ ? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
4310
+ : []),
3344
4311
  {
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
- : {}),
4312
+ path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
4313
+ content: `${JSON.stringify(nextTask, null, 2)}\n`,
3363
4314
  },
3364
4315
  ],
3365
- }, expectedRevision);
4316
+ validate: () => {
4317
+ const saved = this.store.readTask(task.projectId, task.id);
4318
+ if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
4319
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
4320
+ }
4321
+ if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
4322
+ throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
4323
+ }
4324
+ },
4325
+ });
4326
+ return this.store.readTask(task.projectId, task.id);
3366
4327
  }
3367
4328
  milestoneProgress(repository, milestoneId) {
3368
4329
  const snapshot = this.observationSnapshot(repository);
@@ -3619,7 +4580,9 @@ export class WorkflowService {
3619
4580
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
3620
4581
  }
3621
4582
  next(repository, taskId = null) {
3622
- const status = this.status(repository);
4583
+ return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
4584
+ }
4585
+ nextFromStatus(repository, taskId, status) {
3623
4586
  if (status.observation.kind === 'blocked') {
3624
4587
  return {
3625
4588
  scope: 'project',
@@ -3759,7 +4722,24 @@ export class WorkflowService {
3759
4722
  ...(dependency ? { dependency } : {}),
3760
4723
  });
3761
4724
  }
4725
+ const prioritizedRemediationTask = status.milestones.flatMap((milestone) => {
4726
+ if (milestone.status !== 'active')
4727
+ return [];
4728
+ const event = [...readMilestoneScopeChangeEvents(this.store, status.projectId, milestone.id)]
4729
+ .reverse()
4730
+ .find((candidate) => candidate.authorizationMode === 'audited-remediation'
4731
+ && candidate.candidatePlanHash === milestone.planHash
4732
+ && candidate.remediationEvidence?.remediationTaskId);
4733
+ if (!event?.remediationEvidence)
4734
+ return [];
4735
+ const task = tasksById.get(event.remediationEvidence.remediationTaskId);
4736
+ return task
4737
+ && taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
4738
+ ? [task]
4739
+ : [];
4740
+ }).at(0) ?? null;
3762
4741
  const activeTask = requestedTask
4742
+ ?? prioritizedRemediationTask
3763
4743
  ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
3764
4744
  if (activeTask) {
3765
4745
  const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
@@ -3789,6 +4769,22 @@ export class WorkflowService {
3789
4769
  },
3790
4770
  });
3791
4771
  }
4772
+ const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
4773
+ if (planIntegrityDependencyRecovery.eligible) {
4774
+ return withAdoption({
4775
+ scope: 'task',
4776
+ id: activeTask.id,
4777
+ status: activeTask.status,
4778
+ action: 'update plan-integrity-dependency-recover',
4779
+ revision: activeTask.revision,
4780
+ dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
4781
+ compatibilityBoundary: {
4782
+ preservesWorktree: true,
4783
+ prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
4784
+ continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
4785
+ },
4786
+ });
4787
+ }
3792
4788
  const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3793
4789
  if (dependencyRecovery.eligible) {
3794
4790
  return withAdoption({
@@ -3915,41 +4911,132 @@ export class WorkflowService {
3915
4911
  if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
3916
4912
  throw error;
3917
4913
  const blockingDecision = error.details.decision;
3918
- planRiskAuditRecovery = blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
4914
+ const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
4915
+ const existingRemediationEvent = blockingAudit && activeTask.milestoneId
4916
+ ? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
4917
+ .reverse()
4918
+ .find((event) => event.authorizationMode === 'audited-remediation'
4919
+ && event.remediationEvidence?.planRiskAuditEventHash === blockingAudit.eventHash) ?? null
4920
+ : null;
4921
+ const existingRemediationTask = existingRemediationEvent?.remediationEvidence
4922
+ ? tasksById.get(existingRemediationEvent.remediationEvidence.remediationTaskId) ?? null
4923
+ : null;
4924
+ const remediationDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status)) ?? null;
4925
+ const remediationMilestone = activeTask.milestoneId
4926
+ ? milestonesById.get(activeTask.milestoneId) ?? null
4927
+ : null;
4928
+ const remediationMemberships = remediationMilestone
4929
+ ? new Map(remediationMilestone.memberships.map((membership) => [membership.taskId, membership]))
4930
+ : new Map();
4931
+ const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
4932
+ const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
4933
+ && remediationMilestone?.status === 'active'
4934
+ && remediationMembership?.disposition === 'required';
4935
+ const remediationAncestorIds = new Set();
4936
+ const visitRemediationAncestors = (candidateTaskId) => {
4937
+ for (const predecessorTaskId of remediationMemberships.get(candidateTaskId)?.dependsOnTaskIds ?? []) {
4938
+ if (remediationAncestorIds.has(predecessorTaskId))
4939
+ continue;
4940
+ remediationAncestorIds.add(predecessorTaskId);
4941
+ visitRemediationAncestors(predecessorTaskId);
4942
+ }
4943
+ };
4944
+ visitRemediationAncestors(activeTask.id);
4945
+ const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
4946
+ .filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
4947
+ .sort();
4948
+ planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
3919
4949
  ? {
3920
4950
  scope: 'task',
3921
4951
  id: activeTask.id,
3922
4952
  status: activeTask.status,
3923
- action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
4953
+ action: existingRemediationTask?.status === 'merged'
4954
+ ? 'task plan-set'
4955
+ : existingRemediationTask
4956
+ ? 'doctor'
4957
+ : remediationDiscovery?.status === 'ready_to_materialize'
4958
+ ? 'milestone remediation-materialize'
4959
+ : remediationDiscovery
4960
+ ? 'discovery update'
4961
+ : 'discovery start',
3924
4962
  blockedAction: 'task authorize',
3925
4963
  revision: activeTask.revision,
3926
4964
  planRiskAuditDecision: {
3927
4965
  state: 'recorded-blocking',
3928
4966
  decision: blockingDecision,
3929
4967
  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.',
4968
+ requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
3933
4969
  },
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,
4970
+ auditedRemediation: {
4971
+ state: existingRemediationTask?.status === 'merged'
4972
+ ? 'merged-replan-required'
4973
+ : existingRemediationTask
4974
+ ? 'materialized-not-merged'
4975
+ : remediationDiscovery?.status === 'ready_to_materialize'
4976
+ ? 'ready-to-materialize'
4977
+ : 'discovery-required',
4978
+ humanApprovalRequired: false,
4979
+ blockedTaskId: activeTask.id,
4980
+ blockedTaskRevision: activeTask.revision,
4981
+ milestoneId: remediationMilestone.id,
4982
+ milestoneRevision: remediationMilestone.revision,
4983
+ planRiskAuditEventId: blockingAudit?.eventId ?? null,
4984
+ planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
4985
+ discoveryId: remediationDiscovery?.id ?? null,
4986
+ discoveryRevision: remediationDiscovery?.revision ?? null,
4987
+ remediationTaskId: existingRemediationTask?.id ?? null,
4988
+ remediationTaskStatus: existingRemediationTask?.status ?? null,
4989
+ requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
4990
+ eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
4991
+ command: existingRemediationTask?.status === 'merged'
4992
+ ? 'task plan-set'
4993
+ : existingRemediationTask
4994
+ ? 'doctor'
4995
+ : remediationDiscovery?.status === 'ready_to_materialize'
4996
+ ? 'milestone remediation-materialize'
4997
+ : remediationDiscovery
4998
+ ? 'discovery update'
4999
+ : 'discovery start',
5000
+ boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
5001
+ preservesMilestoneSemantics: true,
3951
5002
  },
3952
- };
5003
+ taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
5004
+ }
5005
+ : blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
5006
+ ? {
5007
+ scope: 'task',
5008
+ id: activeTask.id,
5009
+ status: activeTask.status,
5010
+ action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
5011
+ blockedAction: 'task authorize',
5012
+ revision: activeTask.revision,
5013
+ planRiskAuditDecision: {
5014
+ state: 'recorded-blocking',
5015
+ decision: blockingDecision,
5016
+ reason: error.message,
5017
+ requiredAction: blockingDecision === 'stop-escalate'
5018
+ ? 'Replace the rejected Step Plan before requesting fresh authorization.'
5019
+ : 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
5020
+ },
5021
+ ...(blockingDecision === 'stop-escalate'
5022
+ ? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
5023
+ : {}),
5024
+ }
5025
+ : {
5026
+ scope: 'task',
5027
+ id: activeTask.id,
5028
+ status: activeTask.status,
5029
+ action: 'task plan-risk-audit',
5030
+ revision: activeTask.revision,
5031
+ planRiskAuditRecovery: {
5032
+ state: 'required',
5033
+ reason: error.message,
5034
+ currentBriefHash: activeTask.briefHash,
5035
+ currentPlanHash: activeTask.planHash,
5036
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
5037
+ knowledgeMapHash: activeTask.knowledgeMapHash,
5038
+ },
5039
+ };
3953
5040
  }
3954
5041
  }
3955
5042
  const taskNext = activeTask.status === 'planning'
@@ -3958,6 +5045,37 @@ export class WorkflowService {
3958
5045
  ? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
3959
5046
  : planRiskAuditRecovery
3960
5047
  ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
5048
+ const auditedRemediationNavigation = taskNext?.auditedRemediation;
5049
+ let auditedDependencyKnowledgeRefreshBlocked = null;
5050
+ if (auditedRemediationNavigation
5051
+ && activeTaskMilestone
5052
+ && ['discovery start', 'discovery update', 'milestone remediation-materialize'].includes(String(taskNext?.action))) {
5053
+ try {
5054
+ const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
5055
+ const inspectedKnowledgeMap = inspectCurrentKnowledgeMap();
5056
+ if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
5057
+ const dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(repositoryRoot, activeTaskMilestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
5058
+ if (dependencyKnowledgeRefresh.eligible) {
5059
+ return withAdoption(withMilestoneCancellation({
5060
+ ...taskNext,
5061
+ auditedRemediation: {
5062
+ ...auditedRemediationNavigation,
5063
+ dependencyKnowledgeRefresh: {
5064
+ ...dependencyKnowledgeRefresh,
5065
+ humanApprovalRequired: false,
5066
+ application: 'atomic-with-remediation-materialization',
5067
+ },
5068
+ },
5069
+ }));
5070
+ }
5071
+ auditedDependencyKnowledgeRefreshBlocked = dependencyKnowledgeRefresh;
5072
+ }
5073
+ }
5074
+ catch (error) {
5075
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
5076
+ throw error;
5077
+ }
5078
+ }
3961
5079
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
3962
5080
  // strict review are executable while context refresh correctly remains forbidden.
3963
5081
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -3967,8 +5085,12 @@ export class WorkflowService {
3967
5085
  || taskNext?.action === 'task step-review'
3968
5086
  || taskNext?.action === 'task downstream-proof-recover'
3969
5087
  || taskNext?.action === 'task remediation-mode-recover'
5088
+ || (taskNext?.action === 'task writer-credential-replace'
5089
+ && taskNext?.blockedAction === 'task remediation-mode-recover')
3970
5090
  || taskNext?.action === 'task plan-integrity-recover'
3971
5091
  || taskNext?.blockedAction === 'task plan-integrity-recover'
5092
+ || taskNext?.action === 'task corrective-yield'
5093
+ || String(taskNext?.action ?? '').startsWith('task corrective-replan')
3972
5094
  || hardStop
3973
5095
  || terminalCorrectiveDecision === 'split-required'
3974
5096
  || terminalCorrectiveDecision === 'stop-escalate') {
@@ -4005,8 +5127,10 @@ export class WorkflowService {
4005
5127
  ? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
4006
5128
  : null)
4007
5129
  : null;
5130
+ const rollingRootCausePlanSet = taskNext?.action === 'task plan-set'
5131
+ && taskNext.rollingWave?.state === 'root-cause-replan-required';
4008
5132
  const contextRefreshOptions = action === 'project-memory reconcile'
4009
- && taskNext?.action !== 'task plan-set'
5133
+ && (taskNext?.action !== 'task plan-set' || rollingRootCausePlanSet)
4010
5134
  && refreshAssessment?.eligible
4011
5135
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
4012
5136
  : [];
@@ -4052,6 +5176,15 @@ export class WorkflowService {
4052
5176
  },
4053
5177
  }
4054
5178
  : {}),
5179
+ ...(auditedDependencyKnowledgeRefreshBlocked
5180
+ ? {
5181
+ auditedDependencyKnowledgeRefreshBlocked: {
5182
+ ...auditedDependencyKnowledgeRefreshBlocked,
5183
+ humanApprovalRequired: true,
5184
+ requiredAction: 'Resolve or explicitly approve the non-dependency Project Knowledge drift.',
5185
+ },
5186
+ }
5187
+ : {}),
4055
5188
  }));
4056
5189
  }
4057
5190
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
@@ -4358,6 +5491,32 @@ export class WorkflowService {
4358
5491
  milestonePlanHash: milestone.planHash,
4359
5492
  };
4360
5493
  }
5494
+ currentCorrectiveDerivedTaskAuthority(projectId, task, actor) {
5495
+ if (!task.milestoneId || actor.trim() !== `agent:milestone-remediation:${task.milestoneId}`)
5496
+ return null;
5497
+ const milestone = this.readMilestoneCurrent(projectId, task.milestoneId);
5498
+ if (milestone.status !== 'active' || !milestone.planHash)
5499
+ return null;
5500
+ const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
5501
+ if (membership?.disposition !== 'required')
5502
+ return null;
5503
+ const authorization = [...milestone.authorizations]
5504
+ .reverse()
5505
+ .find((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved');
5506
+ if (authorization?.authorizationMode !== 'corrective-derived'
5507
+ || authorization.planHash !== milestone.planHash
5508
+ || authorization.actor !== actor.trim())
5509
+ return null;
5510
+ const scopeChange = [...readMilestoneScopeChangeEvents(this.store, projectId, milestone.id)]
5511
+ .reverse()
5512
+ .find((candidate) => candidate.authorizationMode === 'audited-remediation'
5513
+ && candidate.candidatePlanHash === milestone.planHash
5514
+ && candidate.remediationEvidence !== null
5515
+ && candidate.remediationEvidence !== undefined);
5516
+ if (!scopeChange)
5517
+ return null;
5518
+ return { milestone, authorization, scopeChange };
5519
+ }
4361
5520
  nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
4362
5521
  const transition = task.status === 'awaiting_execution_authorization'
4363
5522
  ? 'task.execution_authorize'
@@ -4367,6 +5526,12 @@ export class WorkflowService {
4367
5526
  const delegatedApprovalOptions = transition
4368
5527
  ? this.delegatedApprovalOptions(projectId, transition, task)
4369
5528
  : [];
5529
+ const correctiveDerivedActor = task.milestoneId
5530
+ ? `agent:milestone-remediation:${task.milestoneId}`
5531
+ : null;
5532
+ const correctiveDerivedAuthority = transition && correctiveDerivedActor
5533
+ ? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
5534
+ : null;
4370
5535
  let remediationModeRecovery = null;
4371
5536
  let guardedCorrectivePosture = null;
4372
5537
  if (planRiskAudit) {
@@ -4374,7 +5539,7 @@ export class WorkflowService {
4374
5539
  guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
4375
5540
  }
4376
5541
  catch (error) {
4377
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
5542
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
4378
5543
  if (!remediationModeRecovery)
4379
5544
  throw error;
4380
5545
  }
@@ -4387,7 +5552,7 @@ export class WorkflowService {
4387
5552
  catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
4388
5553
  }
4389
5554
  catch (error) {
4390
- remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
5555
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
4391
5556
  if (!remediationModeRecovery)
4392
5557
  throw error;
4393
5558
  }
@@ -4399,6 +5564,11 @@ export class WorkflowService {
4399
5564
  && changedFiles(repositoryRoot).length > 0
4400
5565
  ? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
4401
5566
  : null;
5567
+ const planIntegrityDirtyCarryover = next.action === 'task run'
5568
+ && nextStepId
5569
+ && changedFiles(repositoryRoot).length > 0
5570
+ ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
5571
+ : null;
4402
5572
  let mechanicalFeasibilityOverlay = {};
4403
5573
  if (task.status === 'awaiting_execution_authorization' && task.planHash) {
4404
5574
  try {
@@ -4449,6 +5619,9 @@ export class WorkflowService {
4449
5619
  }
4450
5620
  const handoffPosture = readTaskC1Posture(this.store, task);
4451
5621
  const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
5622
+ const failedStepRemediations = failedStep && !remediationModeRecovery
5623
+ ? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
5624
+ : [];
4452
5625
  const failedStepReviewRouting = failedStep
4453
5626
  ? latestFailedStepReviewRouting(this.store, task, failedStep.id)
4454
5627
  : null;
@@ -4579,11 +5752,21 @@ export class WorkflowService {
4579
5752
  stepId: planIntegrityAssessment.stepId,
4580
5753
  planIntegrityRecovery: {
4581
5754
  state: 'eligible',
4582
- reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
5755
+ reasonCode: planIntegrityAssessment.evidence?.kind === 'missing-npm-script-outside-allowed-writes'
5756
+ ? 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES'
5757
+ : planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
5758
+ ? 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES'
5759
+ : planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
5760
+ ? 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES'
5761
+ : 'NON_EXECUTABLE_NPM_CHECK_OUTSIDE_ALLOWED_WRITES',
4583
5762
  expectedRevision: task.revision,
4584
5763
  requiredActor,
4585
5764
  evidence: planIntegrityAssessment.evidence,
4586
- boundedEffect: 'record-replan-required-decision',
5765
+ boundedEffect: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
5766
+ ? 'record-machine-derived-check-support-scope'
5767
+ : planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
5768
+ ? 'record-machine-derived-component-owner-scope'
5769
+ : 'record-replan-required-decision',
4587
5770
  preservesWorktree: true,
4588
5771
  },
4589
5772
  }
@@ -4716,7 +5899,48 @@ export class WorkflowService {
4716
5899
  };
4717
5900
  break;
4718
5901
  case 'replan-required':
4719
- guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
5902
+ guardedCorrectiveOverlay = guardedCorrectivePosture.correctiveDecision
5903
+ ? correctiveReplanOverlay(guardedCorrectivePosture)
5904
+ : (() => {
5905
+ const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
5906
+ const rollingC1Posture = readTaskC1Posture(this.store, task);
5907
+ const blockers = [
5908
+ ...(!['needs_fix', 'ready'].includes(task.status)
5909
+ ? [`Task must be needs_fix or context-refreshed ready, not ${task.status}.`]
5910
+ : []),
5911
+ ...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may remain in progress.'] : []),
5912
+ ...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
5913
+ ? [`${guardedCorrectivePosture.stepId} must remain failed.`]
5914
+ : []),
5915
+ ...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
5916
+ ...(rollingC1Posture.state !== 'none'
5917
+ ? ['No C1 claim may remain after the root-cause failure pause.']
5918
+ : []),
5919
+ ];
5920
+ return blockers.length > 0
5921
+ ? {
5922
+ action: 'doctor',
5923
+ blockedAction: 'task plan-set',
5924
+ stepId: guardedCorrectivePosture.stepId,
5925
+ rollingWave: {
5926
+ state: 'root-cause-replan-blocked',
5927
+ attemptCount: guardedCorrectivePosture.attemptCount,
5928
+ humanApprovalRequired: false,
5929
+ blockers,
5930
+ },
5931
+ }
5932
+ : {
5933
+ action: 'task plan-set',
5934
+ blockedAction: 'task run',
5935
+ stepId: guardedCorrectivePosture.stepId,
5936
+ rollingWave: {
5937
+ state: 'root-cause-replan-required',
5938
+ attemptCount: guardedCorrectivePosture.attemptCount,
5939
+ humanApprovalRequired: false,
5940
+ boundary: 'Task objective, requirements, acceptance, and security semantics remain unchanged.',
5941
+ },
5942
+ };
5943
+ })();
4720
5944
  break;
4721
5945
  case 'split-required':
4722
5946
  {
@@ -4874,6 +6098,11 @@ export class WorkflowService {
4874
6098
  externalSealedReview: {
4875
6099
  packetCommand: 'task step-review-packet',
4876
6100
  recordCommand: 'task step-review-record',
6101
+ reviewMode: 'ordinary',
6102
+ allowedReviewModes: ['ordinary', 'security'],
6103
+ packetReviewModeOption: '--review-mode',
6104
+ implicitSecurityScan: false,
6105
+ reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${strictStepReviewStep.id}:${strictStepReviewStep.evidence.commitSha}:ordinary`),
4877
6106
  reviewerMustUseSeparateChat: true,
4878
6107
  repositorySealRequired: true,
4879
6108
  },
@@ -4889,6 +6118,11 @@ export class WorkflowService {
4889
6118
  externalSealedReview: {
4890
6119
  packetCommand: 'task review-packet',
4891
6120
  recordCommand: 'task review-sealed-record',
6121
+ reviewMode: 'ordinary',
6122
+ allowedReviewModes: ['ordinary', 'security'],
6123
+ packetReviewModeOption: '--review-mode',
6124
+ implicitSecurityScan: false,
6125
+ reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${headCommit(repositoryRoot)}:ordinary`),
4892
6126
  reviewerMustUseSeparateChat: true,
4893
6127
  repositorySealRequired: true,
4894
6128
  },
@@ -4907,7 +6141,11 @@ export class WorkflowService {
4907
6141
  : {}),
4908
6142
  ...guardedCorrectiveOverlay,
4909
6143
  ...downstreamProofOverlay,
4910
- ...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
6144
+ ...(failedStepReviewRouting?.route === 'replan'
6145
+ && next.action === 'task run'
6146
+ && (!failedStep
6147
+ || deriveRollingCauseDecision(failedStepRemediations) === null
6148
+ || failedStepRemediations.at(-1)?.causeBindings?.length === 0)
4911
6149
  ? {
4912
6150
  action: 'task plan-set',
4913
6151
  blockedAction: 'task run',
@@ -4944,7 +6182,44 @@ export class WorkflowService {
4944
6182
  },
4945
6183
  }
4946
6184
  : {}),
6185
+ ...(planIntegrityDirtyCarryover?.applicable
6186
+ ? planIntegrityDirtyCarryover.eligible
6187
+ ? {
6188
+ planIntegrityDirtyCarryover: {
6189
+ state: 'validated',
6190
+ stepId: nextStepId,
6191
+ dependencyRecoveryCommit: planIntegrityDirtyCarryover.dependencyRecoveryCommit,
6192
+ dirtyFiles: planIntegrityDirtyCarryover.compatibility?.dirtyFiles ?? [],
6193
+ dirtyWorktreeHash: planIntegrityDirtyCarryover.compatibility?.dirtyWorktreeHash ?? null,
6194
+ scopeExtensions: planIntegrityDirtyCarryover.compatibility?.scopeExtensions ?? [],
6195
+ meaning: 'The current Step may adopt exactly these hash-bound check-support bytes on task run.',
6196
+ },
6197
+ }
6198
+ : {
6199
+ action: 'doctor',
6200
+ blockedAction: 'task run',
6201
+ planIntegrityDirtyCarryover: {
6202
+ state: 'blocked',
6203
+ stepId: nextStepId,
6204
+ blockers: planIntegrityDirtyCarryover.blockers,
6205
+ },
6206
+ }
6207
+ : {}),
4947
6208
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6209
+ ...(correctiveDerivedAuthority && correctiveDerivedActor
6210
+ ? {
6211
+ correctiveDerivedApproval: {
6212
+ action: transition === 'task.execution_authorize' ? 'task authorize' : 'task accept',
6213
+ actor: correctiveDerivedActor,
6214
+ humanApprovalRequired: false,
6215
+ milestoneId: correctiveDerivedAuthority.milestone.id,
6216
+ milestoneRevision: correctiveDerivedAuthority.milestone.revision,
6217
+ milestonePlanHash: correctiveDerivedAuthority.milestone.planHash,
6218
+ planRiskAuditEventId: correctiveDerivedAuthority.scopeChange.remediationEvidence?.planRiskAuditEventId ?? null,
6219
+ authority: 'current-corrective-derived-milestone-execution-authorization',
6220
+ },
6221
+ }
6222
+ : {}),
4948
6223
  };
4949
6224
  if (handoffPosture.state === 'pending') {
4950
6225
  return this.withTaskNavigationContracts(task, {
@@ -5003,7 +6278,20 @@ export class WorkflowService {
5003
6278
  return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
5004
6279
  }
5005
6280
  taskHasTerminalCorrectiveDecision(task) {
5006
- return this.taskTerminalCorrectivePosture(task) !== null;
6281
+ try {
6282
+ return this.taskTerminalCorrectivePosture(task) !== null;
6283
+ }
6284
+ catch (error) {
6285
+ if (error instanceof WorkflowError
6286
+ && error.code === 'TRANSITION_BLOCKED'
6287
+ && (error.details.decision === 'stop-escalate' || error.details.decision === 'split-required')) {
6288
+ // A blocking Plan Risk Audit is not a guarded-Step terminal decision, but it
6289
+ // equally cannot own an atomic delegated context refresh. Treat it as
6290
+ // ineligible here so standalone Knowledge reconciliation remains executable.
6291
+ return true;
6292
+ }
6293
+ throw error;
6294
+ }
5007
6295
  }
5008
6296
  taskTerminalCorrectivePosture(task) {
5009
6297
  if (!task.planHash)
@@ -5166,6 +6454,10 @@ export class WorkflowService {
5166
6454
  statePlanHash: task.planHash,
5167
6455
  });
5168
6456
  }
6457
+ if (authorization.authorizationMode === 'corrective-derived'
6458
+ && !this.currentCorrectiveDerivedTaskAuthority(task.projectId, task, authorization.actor)) {
6459
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective-derived Task authorization is no longer backed by the current Milestone authority.', { taskId: task.id, milestoneId: task.milestoneId, actor: authorization.actor });
6460
+ }
5169
6461
  const mechanicalFeasibility = authorization.mechanicalFeasibility;
5170
6462
  if (!mechanicalFeasibility)
5171
6463
  return authorization;
@@ -5237,7 +6529,218 @@ export class WorkflowService {
5237
6529
  },
5238
6530
  };
5239
6531
  }
5240
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null) {
6532
+ carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
6533
+ let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
6534
+ let dependencyCommitCount = 0;
6535
+ while (!task.systemCommits.includes(boundaryCommit)) {
6536
+ const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
6537
+ if (!dependencyCommit)
6538
+ return null;
6539
+ boundaryCommit = dependencyCommit.parentCommitSha;
6540
+ dependencyCommitCount += 1;
6541
+ }
6542
+ if (dependencyCommitCount === 0)
6543
+ return null;
6544
+ const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
6545
+ const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
6546
+ const compatibility = record.planIntegrity;
6547
+ if (!compatibility
6548
+ || record.commitSha !== boundaryCommit
6549
+ || !task.systemCommits.includes(record.commitSha)
6550
+ || compatibility.planHash !== task.planHash
6551
+ || canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
6552
+ || compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
6553
+ return false;
6554
+ const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
6555
+ if (!step || step.status !== 'failed')
6556
+ return false;
6557
+ const remediationEventIds = readRemediationEvents(this.store, task)
6558
+ .filter((event) => event.stepId === compatibility.stepId)
6559
+ .map((event) => event.eventId);
6560
+ if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
6561
+ return false;
6562
+ return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
6563
+ && event.stepId === compatibility.stepId
6564
+ && event.decision === 'replan-required')
6565
+ && readTaskC1Posture(this.store, task).state === 'none';
6566
+ });
6567
+ return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
6568
+ }
6569
+ postRebindCheckSupportCompatibility(snapshot, task) {
6570
+ if (task.status !== 'awaiting_execution_authorization'
6571
+ || !task.planHash
6572
+ || task.steps.some((step) => step.status === 'in_progress')
6573
+ || readTaskC1Posture(this.store, task).state !== 'none')
6574
+ return null;
6575
+ const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
6576
+ const candidates = readCorrectiveDecisionEvents(this.store, task).flatMap((event) => {
6577
+ const evidence = event.planIntegrityEvidence;
6578
+ if (event.planHash === task.planHash
6579
+ || event.decision !== 'continue-fix'
6580
+ || event.recoveryBasis !== 'plan-integrity'
6581
+ || evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
6582
+ || !task.systemCommits.includes(evidence.gitHead)
6583
+ || canonicalJsonStringify([...evidence.changedFiles].sort()) !== canonicalJsonStringify(dirtyFiles)) {
6584
+ return [];
6585
+ }
6586
+ const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
6587
+ if (!step
6588
+ || step.status !== 'planned'
6589
+ || evidence.scopeExtensions.some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
6590
+ || !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath)))
6591
+ return [];
6592
+ return [{
6593
+ stepId: step.id,
6594
+ planHash: task.planHash,
6595
+ conflictHash: evidence.conflictHash,
6596
+ dirtyFiles: [...dirtyFiles],
6597
+ dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, dirtyFiles),
6598
+ remediationEventIds: evidence.remediationEvents.map((binding) => binding.eventId),
6599
+ mode: 'post-rebind-scope-adoption',
6600
+ scopeExtensions: [...evidence.scopeExtensions],
6601
+ }];
6602
+ });
6603
+ return candidates.length === 1 ? candidates[0] : null;
6604
+ }
6605
+ carriedPostRebindCheckSupportDirtyCarryover(snapshot, task) {
6606
+ let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
6607
+ let dependencyCommitCount = 0;
6608
+ while (!task.systemCommits.includes(boundaryCommit)) {
6609
+ const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
6610
+ if (!dependencyCommit)
6611
+ return null;
6612
+ boundaryCommit = dependencyCommit.parentCommitSha;
6613
+ dependencyCommitCount += 1;
6614
+ }
6615
+ if (dependencyCommitCount === 0)
6616
+ return null;
6617
+ const assessment = this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, boundaryCommit);
6618
+ if (!assessment.eligible || !assessment.compatibility || !task.planHash)
6619
+ return null;
6620
+ return {
6621
+ ...assessment.compatibility,
6622
+ planHash: task.planHash,
6623
+ mode: 'post-rebind-dirty-carryover',
6624
+ };
6625
+ }
6626
+ assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, stepId, historyBoundary = headCommit(repositoryRoot)) {
6627
+ const relevantRecords = [...(task.dependencyProvenanceRecoveries ?? [])]
6628
+ .reverse()
6629
+ .filter((record) => {
6630
+ const compatibility = record.planIntegrity;
6631
+ return compatibility
6632
+ && ['post-rebind-scope-adoption', 'post-rebind-dirty-carryover'].includes(compatibility.mode ?? '')
6633
+ && (!stepId || compatibility.stepId === stepId);
6634
+ });
6635
+ if (relevantRecords.length === 0) {
6636
+ return {
6637
+ applicable: false,
6638
+ eligible: false,
6639
+ compatibility: null,
6640
+ dependencyRecoveryCommit: null,
6641
+ blockers: [],
6642
+ };
6643
+ }
6644
+ const blockers = [];
6645
+ const record = relevantRecords[0];
6646
+ const compatibility = record.planIntegrity;
6647
+ const step = task.steps.find((candidate) => candidate.id === compatibility.stepId) ?? null;
6648
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
6649
+ let dirtyWorktreeHash = null;
6650
+ try {
6651
+ dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
6652
+ }
6653
+ catch (error) {
6654
+ blockers.push(error instanceof Error ? error.message : String(error));
6655
+ }
6656
+ if (record.commitSha !== historyBoundary || !task.systemCommits.includes(record.commitSha)) {
6657
+ blockers.push('The latest check-support compatibility record must bind the exact registered history boundary.');
6658
+ }
6659
+ if (task.workspaceOwner !== 'local') {
6660
+ blockers.push('Check-support dirty carryover requires the recorded local Task workspace.');
6661
+ }
6662
+ if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
6663
+ blockers.push('Current branch must equal the recorded Task branch.');
6664
+ }
6665
+ if (!['ready', 'in_progress'].includes(task.status)) {
6666
+ blockers.push('The Task must retain an execution-authorized posture before dirty carryover.');
6667
+ }
6668
+ if (!task.planHash) {
6669
+ blockers.push('The Task must retain a current Plan hash.');
6670
+ }
6671
+ if (!step || step.status !== 'planned' || step.evidence !== null) {
6672
+ blockers.push(`The bound Step ${compatibility.stepId} must remain planned with no evidence.`);
6673
+ }
6674
+ if (task.steps.some((candidate) => candidate.status === 'in_progress')) {
6675
+ blockers.push('No Step may already be in progress before dirty carryover.');
6676
+ }
6677
+ if (canonicalJsonStringify(dirtyFiles)
6678
+ !== canonicalJsonStringify([...compatibility.dirtyFiles].sort())) {
6679
+ blockers.push('Dirty files changed after the registered check-support compatibility boundary.');
6680
+ }
6681
+ if (dirtyWorktreeHash !== compatibility.dirtyWorktreeHash) {
6682
+ blockers.push('Dirty file content changed after the registered check-support compatibility boundary.');
6683
+ }
6684
+ if (!compatibility.scopeExtensions?.length) {
6685
+ blockers.push('The compatibility record must retain at least one machine-derived scope extension.');
6686
+ }
6687
+ if (step) {
6688
+ if (dirtyFiles.some((dirtyPath) => !pathAllowed(dirtyPath, step.allowedWrites))) {
6689
+ blockers.push('The current Step must explicitly own every preserved dirty file.');
6690
+ }
6691
+ if ((compatibility.scopeExtensions ?? []).some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
6692
+ || !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath))) {
6693
+ blockers.push('The current Step must explicitly own every check-support scope extension as allowedWrites and expectedOutputs.');
6694
+ }
6695
+ }
6696
+ let reboundPlanHash = compatibility.planHash;
6697
+ for (const rebind of task.knowledgeRebinds) {
6698
+ if (rebind.previousPlanHash === reboundPlanHash)
6699
+ reboundPlanHash = rebind.reboundPlanHash;
6700
+ }
6701
+ if (!task.planHash || reboundPlanHash !== task.planHash) {
6702
+ blockers.push('The current Plan must equal the compatibility Plan or its exact Knowledge-rebind descendant.');
6703
+ }
6704
+ const matchingDecisions = readCorrectiveDecisionEvents(this.store, task).filter((event) => {
6705
+ const evidence = event.planIntegrityEvidence;
6706
+ return event.stepId === compatibility.stepId
6707
+ && event.decision === 'continue-fix'
6708
+ && event.recoveryBasis === 'plan-integrity'
6709
+ && evidence?.kind === 'check-support-anchor-outside-allowed-writes'
6710
+ && evidence.conflictHash === compatibility.conflictHash
6711
+ && canonicalJsonStringify([...evidence.changedFiles].sort())
6712
+ === canonicalJsonStringify([...compatibility.dirtyFiles].sort())
6713
+ && canonicalJsonStringify(evidence.remediationEvents.map((binding) => binding.eventId))
6714
+ === canonicalJsonStringify(compatibility.remediationEventIds)
6715
+ && canonicalJsonStringify([...evidence.scopeExtensions].sort())
6716
+ === canonicalJsonStringify([...(compatibility.scopeExtensions ?? [])].sort());
6717
+ });
6718
+ if (matchingDecisions.length !== 1) {
6719
+ blockers.push('Exactly one validated historical check-support decision must bind the preserved dirty set and scope.');
6720
+ }
6721
+ try {
6722
+ validateTaskHistory(repositoryRoot, task, historyBoundary);
6723
+ }
6724
+ catch (error) {
6725
+ blockers.push(error instanceof Error ? error.message : String(error));
6726
+ }
6727
+ return {
6728
+ applicable: true,
6729
+ eligible: blockers.length === 0,
6730
+ compatibility: blockers.length === 0
6731
+ ? {
6732
+ ...structuredClone(compatibility),
6733
+ planHash: task.planHash,
6734
+ dirtyFiles,
6735
+ dirtyWorktreeHash: dirtyWorktreeHash,
6736
+ mode: 'post-rebind-dirty-carryover',
6737
+ }
6738
+ : null,
6739
+ dependencyRecoveryCommit: record.commitSha,
6740
+ blockers,
6741
+ };
6742
+ }
6743
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
5241
6744
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
5242
6745
  const now = this.now().getTime();
5243
6746
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
@@ -5257,7 +6760,7 @@ export class WorkflowService {
5257
6760
  currentBranch: versions.currentBranch,
5258
6761
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
5259
6762
  },
5260
- }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance);
6763
+ }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
5261
6764
  }
5262
6765
  inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
5263
6766
  const now = this.now().getTime();
@@ -5279,10 +6782,9 @@ export class WorkflowService {
5279
6782
  const policy = gitMutationPolicyForAction(task, action);
5280
6783
  const c1Posture = readTaskC1Posture(this.store, task);
5281
6784
  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;
6785
+ const activeLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
6786
+ const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture, activeLease !== null);
6787
+ const activeLeaseUsable = activeLease ? !toPublicWriterLease(activeLease, this.now()).stale : false;
5286
6788
  const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
5287
6789
  const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
5288
6790
  ? {
@@ -5294,9 +6796,24 @@ export class WorkflowService {
5294
6796
  acquiresLeaseWhenAbsent: true,
5295
6797
  }
5296
6798
  : null;
6799
+ const credentialBlocksAction = Boolean(writerCredentialRecovery
6800
+ && writerTokenContract
6801
+ && (activeLease === null || !activeLeaseUsable));
5297
6802
  return {
5298
6803
  ...next,
5299
6804
  ...(policy ? { gitMutationPolicy: policy } : {}),
6805
+ ...(credentialBlocksAction
6806
+ ? {
6807
+ action: 'task writer-credential-replace',
6808
+ blockedAction: action,
6809
+ continuation: {
6810
+ kind: 'failed-step-pause',
6811
+ preservesTask: true,
6812
+ preservesWorktree: true,
6813
+ resumesWithFreshNext: true,
6814
+ },
6815
+ }
6816
+ : {}),
5300
6817
  ...(writerTokenContract ? { writerTokenContract } : {}),
5301
6818
  ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
5302
6819
  };
@@ -5708,16 +7225,20 @@ function assertTaskBranch(repositoryRoot, task) {
5708
7225
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected Task branch ${task.taskBranch}, found ${branch ?? 'detached HEAD'}.`);
5709
7226
  }
5710
7227
  }
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
7228
  function stripStepRuntime(step) {
5719
- const { status: _status, evidence: _evidence, ...definition } = step;
5720
- return definition;
7229
+ return normalizePlanStep(step);
7230
+ }
7231
+ function normalizePlanStep(step) {
7232
+ return normalizePlanStepDefinition(step);
7233
+ }
7234
+ function parseWorkflowBetaVersion(value) {
7235
+ const match = value?.match(/^2[.]0[.]0-beta[.]([0-9]+)(?:[.]([0-9]+))?$/);
7236
+ if (!match)
7237
+ return null;
7238
+ return {
7239
+ train: Number(match[1]),
7240
+ patch: match[2] === undefined ? null : Number(match[2]),
7241
+ };
5721
7242
  }
5722
7243
  function validateReviewInput(review) {
5723
7244
  if (!review.reviewer.trim())
@@ -6091,7 +7612,32 @@ function gitMutationPolicyForAction(task, action) {
6091
7612
  return null;
6092
7613
  }
6093
7614
  }
6094
- function writerTokenContractForAction(action, claimedC1Posture = false) {
7615
+ function taskWithEffectiveStepAllowedWrites(store, task, stepId) {
7616
+ const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
7617
+ return {
7618
+ ...task,
7619
+ steps: task.steps.map((step) => step.id === stepId ? { ...step, allowedWrites } : step),
7620
+ };
7621
+ }
7622
+ function reviewScopeAugmentations(store, task, stepId) {
7623
+ const step = requireStep(task, stepId);
7624
+ const paths = effectiveStepAllowedWrites(store, task, stepId)
7625
+ .filter((candidate) => !pathAllowed(candidate, step.allowedWrites));
7626
+ return paths.length === 0 ? [] : [{
7627
+ stepId,
7628
+ paths,
7629
+ authority: 'machine-derived-check-support',
7630
+ }];
7631
+ }
7632
+ function reviewCauseBindings(event) {
7633
+ return (event.remediationCauses ?? []).map((cause) => ({
7634
+ authority: 'external-reviewer',
7635
+ causeId: cause.causeId,
7636
+ causeFingerprint: cause.causeFingerprint,
7637
+ planConflict: cause.planConflict,
7638
+ }));
7639
+ }
7640
+ function writerTokenContractForAction(action, claimedC1Posture = false, existingLease = false) {
6095
7641
  const alwaysLeaseBound = [
6096
7642
  'task step-complete',
6097
7643
  'task downstream-proof-recover',
@@ -6117,16 +7663,19 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
6117
7663
  'task handback',
6118
7664
  'task handback-create',
6119
7665
  ];
6120
- if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
7666
+ if (!alwaysLeaseBound.includes(action)
7667
+ && !(claimedC1Posture && claimedC1LeaseBound.includes(action))
7668
+ && !(existingLease && ['task run', 'task step-review', 'task step-review-record'].includes(action))) {
6121
7669
  return null;
6122
7670
  }
6123
7671
  return {
6124
7672
  required: true,
6125
- option: '--writer-token',
6126
- source: 'active-writer-lease',
6127
- sensitive: true,
6128
- renewalDisposition: 'reuse-presented-token',
6129
- tokenReissuedOnRenewal: false,
7673
+ option: '--writer-credential-ref',
7674
+ source: 'local-credential-vault',
7675
+ transport: 'local-vault-v1',
7676
+ secretInArgv: false,
7677
+ secretInOutput: false,
7678
+ renewalDisposition: 'reuse-reference',
6130
7679
  };
6131
7680
  }
6132
7681
  function observationPreflightBlockers(observation) {