codex-workflow-v2 2.0.0-alpha.6.1 → 2.0.0-alpha.6.2

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.
@@ -12,9 +12,9 @@ import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, ins
12
12
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
13
13
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
14
14
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
15
- import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
15
+ import { appendCorrectivePlanAuditRecord, appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, hasCorrectivePlanAuditBinding, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
16
  import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
- import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
17
+ import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, readReviewerAttestations, } from './alpha6/review.js';
18
18
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
19
19
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
20
20
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
@@ -24,12 +24,15 @@ import { WriterLockManager } from './state/lock.js';
24
24
  import { FileStateStore } from './state/store.js';
25
25
  import { createEntityId } from './ulid.js';
26
26
  import { PACKAGE_VERSION } from './version.js';
27
+ const ALPHA6_CORRECTIVE_RESCUE_HEAD = '5c63798cc7b8fbb5e1c8b26ebf93876bc642295a';
27
28
  export class WorkflowService {
28
29
  store;
29
30
  now;
30
- constructor(store = new FileStateStore(), now = () => new Date()) {
31
+ alpha6CorrectiveRescueHead;
32
+ constructor(store = new FileStateStore(), now = () => new Date(), alpha6CorrectiveRescueHead = ALPHA6_CORRECTIVE_RESCUE_HEAD) {
31
33
  this.store = store;
32
34
  this.now = now;
35
+ this.alpha6CorrectiveRescueHead = alpha6CorrectiveRescueHead;
33
36
  }
34
37
  context(repository) {
35
38
  const identity = resolveRepositoryIdentity(repository);
@@ -565,14 +568,13 @@ export class WorkflowService {
565
568
  this.validateKnowledgeTargets(knowledgeMap, plan);
566
569
  const task = this.store.readTask(identity.projectId, taskId);
567
570
  assertExpectedRevision(task, expectedRevision);
568
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
571
+ if (!['planning', 'awaiting_execution_authorization', 'ready', 'needs_fix', 'blocked'].includes(task.status)) {
569
572
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
570
573
  }
571
574
  const currentPlanCorrectiveDecisions = task.planHash
572
575
  ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
573
576
  : [];
574
577
  const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
575
- const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
576
578
  const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
577
579
  const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
578
580
  if (currentHardBlockingDecisions.length > 0) {
@@ -593,33 +595,22 @@ export class WorkflowService {
593
595
  requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
594
596
  });
595
597
  }
596
- if (currentContinueFixDecisions.length > 0) {
597
- throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} already has a current continue-fix corrective decision; use the current Plan retry path instead of issuing another plan-set.`, {
598
- taskId: task.id,
599
- requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
600
- decisions: currentContinueFixDecisions.map((event) => ({
601
- eventId: event.eventId,
602
- stepId: event.stepId,
603
- triggeringAttemptCount: event.triggeringAttemptCount,
604
- })),
605
- });
606
- }
607
- const currentPlanRiskAudit = task.status === 'needs_fix'
608
- ? readCurrentPlanRiskAudit(this.store, task)
609
- : null;
598
+ const currentPlanRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
610
599
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
611
600
  if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
612
601
  validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
613
602
  }
614
- const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
603
+ const guardedRemediationGate = ['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)
604
+ && currentPlanRiskAudit
615
605
  ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
616
606
  : null;
617
- if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
618
- throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before another remediation run can be authorized.`, {
607
+ if (guardedRemediationGate && !guardedRemediationGate.correctiveDecision) {
608
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective decision before a corrective Plan can be applied.`, {
619
609
  taskId: task.id,
620
610
  stepId: guardedRemediationGate.stepId,
621
611
  attemptCount: guardedRemediationGate.attemptCount,
622
612
  requiredSidecar: 'corrective-decisions.jsonl',
613
+ requiredAction: 'Run task corrective-decision first.',
623
614
  allowedDecision: 'continue-fix',
624
615
  escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
625
616
  });
@@ -631,25 +622,57 @@ export class WorkflowService {
631
622
  requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
632
623
  });
633
624
  }
634
- if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
635
- throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective-decision record before execution may continue; plan-set only supports continue-fix.`, {
625
+ const guardedContinueFixDecision = guardedRemediationGate?.correctiveDecision?.decision === 'continue-fix'
626
+ ? guardedRemediationGate.correctiveDecision
627
+ : null;
628
+ if (task.status === 'ready' && !guardedContinueFixDecision) {
629
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
630
+ }
631
+ if (guardedRemediationGate?.correctiveDecision && !guardedContinueFixDecision && currentReplanDecisions.length === 0) {
632
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision ${guardedRemediationGate.correctiveDecision.decision} prohibits continue-fix Plan application.`, {
636
633
  taskId: task.id,
637
634
  stepId: guardedRemediationGate.stepId,
638
- decision: correctiveAudit?.decision ?? null,
635
+ decisionEventId: guardedRemediationGate.correctiveDecision.eventId,
639
636
  });
640
637
  }
641
- for (const requirement of task.requirements) {
642
- if (!plan.requirements.includes(requirement)) {
643
- throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
638
+ if (guardedContinueFixDecision) {
639
+ validateGuardedCorrectivePlanAudit(this.store, task, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit);
640
+ }
641
+ if (guardedContinueFixDecision) {
642
+ if (!sameOrderedStrings(plan.requirements, task.requirements)) {
643
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan requirements must exactly preserve Task scope.', {
644
+ expected: task.requirements,
645
+ actual: plan.requirements,
646
+ });
647
+ }
648
+ if (!sameOrderedStrings(plan.acceptance, task.acceptance)) {
649
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan acceptance must exactly preserve Task scope.', {
650
+ expected: task.acceptance,
651
+ actual: plan.acceptance,
652
+ });
644
653
  }
645
654
  }
646
- for (const acceptance of task.acceptance) {
647
- if (!plan.acceptance.includes(acceptance)) {
648
- throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
655
+ else {
656
+ for (const requirement of task.requirements) {
657
+ if (!plan.requirements.includes(requirement)) {
658
+ throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
659
+ }
660
+ }
661
+ for (const acceptance of task.acceptance) {
662
+ if (!plan.acceptance.includes(acceptance)) {
663
+ throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
664
+ }
649
665
  }
650
666
  }
651
667
  const root = this.store.taskRoot(identity.projectId, taskId);
652
668
  const stagedPlan = this.store.stageArtifact(root, 'plan.md', renderPlan(task, plan));
669
+ if (guardedContinueFixDecision && stagedPlan.hash === task.planHash) {
670
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Continue-fix requires a changed full corrective Plan.', {
671
+ taskId: task.id,
672
+ stepId: guardedRemediationGate.stepId,
673
+ planHash: task.planHash,
674
+ });
675
+ }
653
676
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
654
677
  ? { ...authorization, decision: 'superseded' }
655
678
  : authorization);
@@ -679,9 +702,8 @@ export class WorkflowService {
679
702
  stepId: guardedRemediationGate.stepId,
680
703
  });
681
704
  }
682
- if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
683
- assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
684
- appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
705
+ if (guardedContinueFixDecision) {
706
+ assertNonTargetStepsUnchanged(task, plan, guardedRemediationGate.stepId);
685
707
  }
686
708
  const candidate = {
687
709
  ...task,
@@ -704,12 +726,16 @@ export class WorkflowService {
704
726
  evidenceHash: null,
705
727
  blockReason: null,
706
728
  };
707
- if (planRiskAudit)
708
- validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
729
+ const effectivePlanRiskAudit = planRiskAudit ?? (guardedContinueFixDecision ? currentPlanRiskAudit?.audit ?? null : null);
730
+ if (effectivePlanRiskAudit)
731
+ validatePlanRiskAuditCandidate(this.store, candidate, effectivePlanRiskAudit);
709
732
  const saved = this.store.writeTask(candidate, expectedRevision);
710
733
  this.store.publishArtifact(stagedPlan);
711
- if (planRiskAudit)
712
- appendCurrentPlanRiskAudit(this.store, saved, planRiskAudit, this.now());
734
+ if (effectivePlanRiskAudit)
735
+ appendCurrentPlanRiskAudit(this.store, saved, effectivePlanRiskAudit, this.now());
736
+ if (guardedContinueFixDecision && correctiveAudit) {
737
+ appendCorrectivePlanAuditRecord(this.store, task, saved, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit, this.now());
738
+ }
713
739
  if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
714
740
  appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
715
741
  }
@@ -735,6 +761,11 @@ export class WorkflowService {
735
761
  if (activeStep) {
736
762
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
737
763
  }
764
+ const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
765
+ const breakerPlanRiskAudit = currentPlanRiskAudit
766
+ && findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
767
+ ? currentPlanRiskAudit
768
+ : null;
738
769
  const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
739
770
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
740
771
  if (task.knowledgeMapRevision === knowledgeMap.revision &&
@@ -794,6 +825,9 @@ export class WorkflowService {
794
825
  blockReason: null,
795
826
  }, expectedRevision);
796
827
  this.store.publishArtifact(stagedPlan);
828
+ if (breakerPlanRiskAudit) {
829
+ appendReboundPlanRiskAudit(this.store, task, saved, breakerPlanRiskAudit, this.now());
830
+ }
797
831
  return saved;
798
832
  }
799
833
  refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
@@ -815,6 +849,8 @@ export class WorkflowService {
815
849
  const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
816
850
  const transitions = [];
817
851
  const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
852
+ const breakerPlanRiskAuditWillBePreserved = currentPlanRiskAudit
853
+ && findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit) !== null;
818
854
  // Validate every delegated transition before the first write so a narrow grant cannot
819
855
  // leave the composite operation half-applied.
820
856
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
@@ -836,7 +872,7 @@ export class WorkflowService {
836
872
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
837
873
  }
838
874
  let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
839
- if (currentPlanRiskAudit) {
875
+ if (currentPlanRiskAudit && !breakerPlanRiskAuditWillBePreserved) {
840
876
  appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
841
877
  }
842
878
  transitions.push('task knowledge-rebind');
@@ -1118,9 +1154,9 @@ export class WorkflowService {
1118
1154
  currentAuditPresent: Boolean(planRiskAudit),
1119
1155
  });
1120
1156
  }
1121
- if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1122
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1123
- }
1157
+ const remediationGate = planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
1158
+ ? assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
1159
+ : null;
1124
1160
  const dirty = changedFiles(context.identity.repositoryRoot);
1125
1161
  if (step.status === 'planned' && dirty.length > 0) {
1126
1162
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
@@ -1223,6 +1259,15 @@ export class WorkflowService {
1223
1259
  stepId,
1224
1260
  expectedRevision: saved.revision,
1225
1261
  writerToken: lease.token,
1262
+ ...(remediationGate
1263
+ ? {
1264
+ remediation: {
1265
+ attemptOrdinal: remediationGate.nextAttemptOrdinal,
1266
+ mode: remediationGate.mode,
1267
+ correctiveDecisionEventId: remediationGate.correctiveDecision?.eventId ?? null,
1268
+ },
1269
+ }
1270
+ : {}),
1226
1271
  knowledgeMap: knowledgeSummary(knowledgeMap),
1227
1272
  knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
1228
1273
  },
@@ -1728,7 +1773,7 @@ export class WorkflowService {
1728
1773
  assertExpectedRevision(task, expectedRevision);
1729
1774
  this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1730
1775
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1731
- if (task.status !== 'needs_fix' && task.status !== 'blocked') {
1776
+ if (!['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)) {
1732
1777
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
1733
1778
  }
1734
1779
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
@@ -1751,25 +1796,45 @@ export class WorkflowService {
1751
1796
  stepId,
1752
1797
  });
1753
1798
  }
1754
- if (task.status === 'needs_fix') {
1755
- if (step.status !== 'failed') {
1756
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
1757
- taskId,
1758
- stepId,
1759
- stepStatus: step.status,
1760
- });
1761
- }
1799
+ if (step.status !== 'failed') {
1800
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step.`, {
1801
+ taskId,
1802
+ stepId,
1803
+ taskStatus: task.status,
1804
+ stepStatus: step.status,
1805
+ });
1762
1806
  }
1763
- else {
1764
- const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1765
- if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
1766
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
1767
- taskId,
1768
- stepId,
1769
- stepStatus: step.status,
1770
- resolution: currentCycle.resolution,
1771
- });
1772
- }
1807
+ const remediationEvents = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
1808
+ if (remediationEvents.length >= 3) {
1809
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} reached the hard stop after its third failed remediation review.`, {
1810
+ taskId,
1811
+ stepId,
1812
+ failedAttemptCount: remediationEvents.length,
1813
+ allowedDecisions: ['split-required', 'stop-escalate'],
1814
+ });
1815
+ }
1816
+ if (remediationEvents.length !== 2) {
1817
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires exactly two guarded failures.`, {
1818
+ taskId,
1819
+ stepId,
1820
+ attemptCount: remediationEvents.length,
1821
+ });
1822
+ }
1823
+ const existingDecision = readCorrectiveDecisionEvents(this.store, task)
1824
+ .find((event) => event.stepId === stepId && event.triggeringAttemptCount === 3) ?? null;
1825
+ if (existingDecision) {
1826
+ throw new WorkflowError('STATE_CONFLICT', `Corrective decision for ${stepId} already exists.`, {
1827
+ taskId,
1828
+ stepId,
1829
+ decisionEventId: existingDecision.eventId,
1830
+ });
1831
+ }
1832
+ const root = this.store.taskRoot(task.projectId, task.id);
1833
+ if (this.store.hashArtifact(root, 'brief.md') !== task.briefHash || this.store.hashArtifact(root, 'plan.md') !== task.planHash) {
1834
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision requires unchanged Brief and exact current Plan bindings.', {
1835
+ taskId,
1836
+ stepId,
1837
+ });
1773
1838
  }
1774
1839
  assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
1775
1840
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
@@ -2006,6 +2071,243 @@ export class WorkflowService {
2006
2071
  adoption,
2007
2072
  };
2008
2073
  }
2074
+ alpha6CorrectiveRescuePreflight(repository) {
2075
+ const identity = resolveRepositoryIdentity(repository);
2076
+ const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, { ensureRoot: false });
2077
+ const dependency = inspectLocalDependency(identity.repositoryRoot);
2078
+ const tasks = this.store.listTasks(identity.projectId);
2079
+ const leases = locks.list();
2080
+ const dirty = changedFiles(identity.repositoryRoot);
2081
+ const currentHead = headCommit(identity.repositoryRoot);
2082
+ const candidates = [];
2083
+ const malformed = [];
2084
+ for (const task of tasks) {
2085
+ if (task.revision !== 14 || task.status !== 'ready')
2086
+ continue;
2087
+ const step = task.steps.find((entry) => entry.id === 'STEP-001') ?? null;
2088
+ if (!step || step.status !== 'failed' || !task.planHash)
2089
+ continue;
2090
+ try {
2091
+ const root = this.store.taskRoot(task.projectId, task.id);
2092
+ if (this.store.hashArtifact(root, 'brief.md') !== task.briefHash) {
2093
+ throw new WorkflowError('STATE_CORRUPT', 'Brief artifact changed after the recovery boundary.');
2094
+ }
2095
+ const discovery = this.store.readDiscovery(task.projectId, task.discoveryId);
2096
+ if (sha256Hex(renderBrief(discovery, task.title)) !== task.briefHash) {
2097
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery Task Brief no longer matches its immutable Discovery scope.');
2098
+ }
2099
+ if (this.store.hashArtifact(root, 'plan.md') !== task.planHash) {
2100
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact does not match the recovery Task binding.');
2101
+ }
2102
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2103
+ if (!planRiskAudit || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
2104
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery Step is not guarded by the exact current Plan Risk Audit.');
2105
+ }
2106
+ const allRemediationEvents = readRemediationEvents(this.store, task);
2107
+ const remediationEvents = allRemediationEvents.filter((event) => event.stepId === step.id);
2108
+ if (allRemediationEvents.length !== 2
2109
+ || remediationEvents.length !== allRemediationEvents.length
2110
+ || remediationEvents.some((event, index) => event.attemptOrdinal !== index + 1
2111
+ || event.mode !== 'ordinary'
2112
+ || event.failureKind !== 'review-failed'
2113
+ || event.failureEvidence.reviewEventHash === null
2114
+ || event.failureEvidence.reviewerAttestationHash === null)) {
2115
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery requires exactly two ordinary verified strict-review failures.');
2116
+ }
2117
+ const failureIdentities = new Set(remediationEvents.map((event) => [
2118
+ event.failureEvidence.completionCommit,
2119
+ event.failureEvidence.reviewEventHash,
2120
+ event.failureEvidence.reviewerAttestationHash,
2121
+ ].join('\0')));
2122
+ if (failureIdentities.size !== 2) {
2123
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery strict-review failures are not distinct.');
2124
+ }
2125
+ const attestations = readReviewerAttestations(this.store, task);
2126
+ if (remediationEvents.some((event) => {
2127
+ const attestation = attestations.find((entry) => entry.eventHash === event.failureEvidence.reviewerAttestationHash);
2128
+ return !attestation || attestation.isolationResult !== 'verified';
2129
+ })) {
2130
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery strict-review failures lack verified reviewer isolation.');
2131
+ }
2132
+ if (readCorrectiveDecisionEvents(this.store, task).some((event) => event.stepId === step.id)) {
2133
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery Task already has a corrective decision.');
2134
+ }
2135
+ const posture = readTaskC1Posture(this.store, task);
2136
+ if (posture.state !== 'claimed' || !posture.writerLeaseTokenHash) {
2137
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery Task requires claimed C1 with a bound writer lease token.');
2138
+ }
2139
+ const lease = leases.find((entry) => entry.entityId === task.id) ?? null;
2140
+ if (!lease
2141
+ || lease.owner !== posture.claimant
2142
+ || sha256Hex(lease.token) !== posture.writerLeaseTokenHash) {
2143
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery lease does not exactly match the C1 claimant/token binding.');
2144
+ }
2145
+ const { token: _secret, ...publicLease } = lease;
2146
+ let milestoneScopeHash = null;
2147
+ if (task.milestoneId) {
2148
+ const milestone = this.store.readMilestone(task.projectId, task.milestoneId);
2149
+ const matchingMemberships = milestone.memberships.filter((entry) => entry.taskId === task.id);
2150
+ if (matchingMemberships.length !== 1 || !milestone.taskIds.includes(task.id)) {
2151
+ throw new WorkflowError('STATE_CORRUPT', 'Recovery Task no longer matches exact Milestone membership scope.');
2152
+ }
2153
+ milestoneScopeHash = milestoneScopeDigest(milestone);
2154
+ }
2155
+ candidates.push({
2156
+ taskId: task.id,
2157
+ taskRevision: 14,
2158
+ taskStatus: 'ready',
2159
+ stepId: 'STEP-001',
2160
+ stepStatus: 'failed',
2161
+ attemptCount: 2,
2162
+ briefHash: task.briefHash,
2163
+ planHash: task.planHash,
2164
+ claimant: posture.claimant,
2165
+ lease: {
2166
+ ...publicLease,
2167
+ stale: Date.parse(lease.expiresAt) <= this.now().getTime(),
2168
+ tokenBound: true,
2169
+ },
2170
+ milestoneScopeHash,
2171
+ });
2172
+ }
2173
+ catch (error) {
2174
+ malformed.push(error instanceof Error ? error.message : String(error));
2175
+ }
2176
+ }
2177
+ const candidate = candidates.length === 1 ? candidates[0] : null;
2178
+ const blockers = [
2179
+ ...(dependency.declared !== '2.0.0-alpha.6'
2180
+ ? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
2181
+ : []),
2182
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
2183
+ ...(currentHead !== this.alpha6CorrectiveRescueHead
2184
+ ? [`HEAD must exactly equal ${this.alpha6CorrectiveRescueHead}.`]
2185
+ : []),
2186
+ ...(candidates.length !== 1
2187
+ ? [`Expected exactly one alpha.6 corrective recovery candidate; found ${candidates.length}.`]
2188
+ : []),
2189
+ ...(candidate && leases.some((lease) => lease.entityId !== candidate.taskId)
2190
+ ? ['An unrelated writer lease exists outside the recovery Task.']
2191
+ : []),
2192
+ ...malformed.map((message) => `Corrective recovery candidate is invalid: ${message}`),
2193
+ ];
2194
+ return {
2195
+ eligible: blockers.length === 0 && candidate !== null,
2196
+ mutationFree: true,
2197
+ runnerPackageVersion: PACKAGE_VERSION,
2198
+ requiredProjectPackageVersion: '2.0.0-alpha.6',
2199
+ declaredProjectPackageVersion: dependency.declared,
2200
+ requiredHead: this.alpha6CorrectiveRescueHead,
2201
+ currentHead,
2202
+ clean: dirty.length === 0,
2203
+ candidate,
2204
+ blockers,
2205
+ };
2206
+ }
2207
+ alpha6CorrectiveRescueApply(repository, taskId, expectedRevision, actor, writerToken, plan, decisionAudit, correctivePlanAudit) {
2208
+ const identity = resolveRepositoryIdentity(repository);
2209
+ const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
2210
+ const task = this.store.readTask(identity.projectId, taskId);
2211
+ assertExpectedRevision(task, expectedRevision);
2212
+ const posture = readTaskC1Posture(this.store, task);
2213
+ const lease = locks.inspect(taskId);
2214
+ // Authentication is deliberately completed before heartbeat. A wrong actor/token must
2215
+ // not refresh a stale lease or create any observable recovery mutation.
2216
+ if (posture.state !== 'claimed'
2217
+ || actor.trim() !== posture.claimant
2218
+ || !lease
2219
+ || lease.owner !== posture.claimant
2220
+ || lease.token !== writerToken
2221
+ || !posture.writerLeaseTokenHash
2222
+ || sha256Hex(writerToken) !== posture.writerLeaseTokenHash) {
2223
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue actor/token does not match the claimed C1 lease binding.', {
2224
+ taskId,
2225
+ actor: actor.trim(),
2226
+ claimant: posture.state === 'claimed' ? posture.claimant : null,
2227
+ });
2228
+ }
2229
+ const preflight = this.alpha6CorrectiveRescuePreflight(repository);
2230
+ if (!preflight.eligible || preflight.candidate?.taskId !== taskId || expectedRevision !== 14) {
2231
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue preflight is not eligible for the requested Task.', {
2232
+ taskId,
2233
+ expectedRevision,
2234
+ blockers: preflight.blockers,
2235
+ });
2236
+ }
2237
+ validatePlan(plan);
2238
+ const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
2239
+ this.validateKnowledgeTargets(knowledgeMap, plan);
2240
+ if (!sameOrderedStrings(plan.requirements, task.requirements)
2241
+ || !sameOrderedStrings(plan.acceptance, task.acceptance)) {
2242
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue Plan must preserve requirements and acceptance exactly.');
2243
+ }
2244
+ assertNonTargetStepsUnchanged(task, plan, 'STEP-001');
2245
+ if (decisionAudit.decision !== 'continue-fix') {
2246
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue only supports an independent continue-fix decision.');
2247
+ }
2248
+ assertCorrectiveAuditorIndependence(this.store, task, 'STEP-001', decisionAudit.auditor);
2249
+ if (correctivePlanAudit.auditor.trim() === decisionAudit.auditor.trim()) {
2250
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue Plan auditor must be independent from the decision auditor.');
2251
+ }
2252
+ if (!decisionAudit.auditor.trim()
2253
+ || !decisionAudit.summary.trim()
2254
+ || !correctivePlanAudit.auditor.trim()
2255
+ || !correctivePlanAudit.summary.trim()
2256
+ || correctivePlanAudit.decision !== 'continue-fix'
2257
+ || decisionAudit.reviewedFindingIds.length === 0
2258
+ || correctivePlanAudit.reviewedFindingIds.length === 0) {
2259
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue audits must independently cover the exact strict-review finding.');
2260
+ }
2261
+ assertCorrectiveAuditorIndependence(this.store, task, 'STEP-001', correctivePlanAudit.auditor);
2262
+ const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2263
+ if (!currentPlanRiskAudit) {
2264
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective rescue requires the exact current Plan Risk Audit.');
2265
+ }
2266
+ const stagedPlan = this.store.stageArtifact(this.store.taskRoot(task.projectId, task.id), 'plan.md', renderPlan(task, plan));
2267
+ if (stagedPlan.hash === task.planHash) {
2268
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue requires a changed full corrective Plan.');
2269
+ }
2270
+ const completedIds = task.steps
2271
+ .filter((candidate) => candidate.status === 'completed')
2272
+ .map((candidate) => candidate.id);
2273
+ const previewSteps = plan.steps.map((step) => preserveCompletedStep(task, step) ?? ({
2274
+ ...step,
2275
+ status: dependenciesComplete(step, completedIds) ? 'planned' : 'blocked',
2276
+ evidence: null,
2277
+ }));
2278
+ validatePlanRiskAuditCandidate(this.store, {
2279
+ ...task,
2280
+ status: 'awaiting_execution_authorization',
2281
+ planHash: stagedPlan.hash,
2282
+ planObjective: plan.objective,
2283
+ risks: plan.risks,
2284
+ knowledgeImpact: plan.knowledgeImpact,
2285
+ knowledgeImpactReason: plan.knowledgeImpactReason,
2286
+ knowledgeTargets: plan.knowledgeTargets,
2287
+ knowledgeMapRevision: plan.knowledgeMapRevision,
2288
+ knowledgeMapHash: plan.knowledgeMapHash,
2289
+ graphUse: plan.graphUse,
2290
+ steps: previewSteps,
2291
+ }, currentPlanRiskAudit.audit);
2292
+ const milestoneScopeBefore = task.milestoneId
2293
+ ? milestoneScopeDigest(this.store.readMilestone(task.projectId, task.milestoneId))
2294
+ : null;
2295
+ const refreshedLease = locks.heartbeat(taskId, writerToken);
2296
+ const decision = appendCorrectiveDecisionEvent(this.store, task, 'STEP-001', decisionAudit, currentPlanRiskAudit, task.planHash, this.now());
2297
+ validateGuardedCorrectivePlanAudit(this.store, task, 'STEP-001', decision, correctivePlanAudit);
2298
+ const saved = this.setTaskPlan(repository, taskId, expectedRevision, plan, correctivePlanAudit, null);
2299
+ if (this.store.hashArtifact(this.store.taskRoot(task.projectId, task.id), 'brief.md') !== preflight.candidate.briefHash) {
2300
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective rescue changed the immutable Brief.');
2301
+ }
2302
+ const milestoneScopeAfter = task.milestoneId
2303
+ ? milestoneScopeDigest(this.store.readMilestone(task.projectId, task.milestoneId))
2304
+ : null;
2305
+ if (milestoneScopeAfter !== milestoneScopeBefore) {
2306
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective rescue changed Milestone scope.');
2307
+ }
2308
+ const { token: _secret, ...publicLease } = refreshedLease;
2309
+ return { task: saved, decision, lease: { ...publicLease, tokenRetained: true } };
2310
+ }
2009
2311
  scanKnowledgeMap(repository) {
2010
2312
  const { identity } = this.context(repository);
2011
2313
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
@@ -2293,6 +2595,25 @@ export class WorkflowService {
2293
2595
  ? this.delegatedApprovalOptions(projectId, transition, task)
2294
2596
  : [];
2295
2597
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
2598
+ const guardedFailedSteps = planRiskAudit
2599
+ ? task.steps.filter((step) => step.status === 'failed' && planRiskAudit.reviewRequiredStepIds.includes(step.id))
2600
+ : [];
2601
+ const breakerSteps = guardedFailedSteps.map((step) => ({
2602
+ step,
2603
+ attemptCount: readRemediationEvents(this.store, task).filter((event) => event.stepId === step.id).length,
2604
+ })).filter((entry) => entry.attemptCount >= 2);
2605
+ if (breakerSteps.length > 1) {
2606
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple guarded failed Steps simultaneously carry remediation-breaker posture.', {
2607
+ taskId: task.id,
2608
+ stepIds: breakerSteps.map((entry) => entry.step.id),
2609
+ });
2610
+ }
2611
+ const breakerStep = breakerSteps[0] ?? null;
2612
+ const guardedCorrectiveGate = breakerStep && breakerStep.attemptCount === 2 && planRiskAudit
2613
+ ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, planRiskAudit)
2614
+ : null;
2615
+ const correctivePlanRequired = guardedCorrectiveGate?.correctiveDecision?.decision === 'continue-fix'
2616
+ && !hasCorrectivePlanAuditBinding(this.store, task, guardedCorrectiveGate.correctiveDecision);
2296
2617
  const next = nextForTask(task);
2297
2618
  const handoffPosture = readTaskC1Posture(this.store, task);
2298
2619
  const strictStepReviewStep = planRiskAudit
@@ -2365,7 +2686,53 @@ export class WorkflowService {
2365
2686
  },
2366
2687
  }
2367
2688
  : {}),
2368
- ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2689
+ ...(guardedCorrectiveGate
2690
+ ? guardedCorrectiveGate.correctiveDecision === null
2691
+ ? {
2692
+ action: 'task corrective-decision',
2693
+ stepId: guardedCorrectiveGate.stepId,
2694
+ correctiveDecisionGate: {
2695
+ attemptCount: guardedCorrectiveGate.attemptCount,
2696
+ auditorMustDifferFromLatestStrictReviewer: true,
2697
+ allowedDecision: 'continue-fix',
2698
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2699
+ humanApprovalRequired: false,
2700
+ },
2701
+ }
2702
+ : correctivePlanRequired
2703
+ ? {
2704
+ action: 'task plan-set with independent corrective audit',
2705
+ stepId: guardedCorrectiveGate.stepId,
2706
+ correctivePlanGate: {
2707
+ attemptCount: guardedCorrectiveGate.attemptCount,
2708
+ decisionEventId: guardedCorrectiveGate.correctiveDecision.eventId,
2709
+ fullPlanRequired: true,
2710
+ auditFileRequired: true,
2711
+ auditorMustDifferFromDecisionAuditor: true,
2712
+ auditorMustDifferFromLatestStrictReviewer: true,
2713
+ allowedDecision: 'continue-fix',
2714
+ },
2715
+ }
2716
+ : guardedCorrectiveGate.correctiveDecision.decision !== 'continue-fix'
2717
+ ? {
2718
+ action: 'task corrective-hard-stop',
2719
+ stepId: guardedCorrectiveGate.stepId,
2720
+ correctiveDecision: guardedCorrectiveGate.correctiveDecision.decision,
2721
+ }
2722
+ : {}
2723
+ : {}),
2724
+ ...(breakerStep?.attemptCount && breakerStep.attemptCount >= 3
2725
+ ? {
2726
+ action: 'task corrective-hard-stop',
2727
+ stepId: breakerStep.step.id,
2728
+ correctiveHardStop: {
2729
+ failedAttemptCount: breakerStep.attemptCount,
2730
+ continueFixAllowed: false,
2731
+ allowedDecisions: ['split-required', 'stop-escalate'],
2732
+ },
2733
+ }
2734
+ : {}),
2735
+ ...(delegatedApprovalOptions.length > 0 && !breakerStep ? { delegatedApprovalOptions } : {}),
2369
2736
  };
2370
2737
  if (handoffPosture.state === 'pending') {
2371
2738
  return {
@@ -2881,6 +3248,67 @@ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
2881
3248
  throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
2882
3249
  }
2883
3250
  }
3251
+ function validateGuardedCorrectivePlanAudit(store, task, stepId, decision, audit) {
3252
+ if (!audit) {
3253
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} requires a full corrective Plan with an independent corrective audit.`, {
3254
+ taskId: task.id,
3255
+ stepId,
3256
+ decisionEventId: decision.eventId,
3257
+ requiredAction: 'Pass the full Plan and --corrective-audit-file to task plan-set.',
3258
+ });
3259
+ }
3260
+ if (audit.decision !== 'continue-fix' || !audit.auditor.trim() || !audit.summary.trim()) {
3261
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit must independently approve continue-fix with auditor and summary.');
3262
+ }
3263
+ if (audit.auditor.trim() === decision.auditor.trim()) {
3264
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan auditor must be distinct from the corrective-decision auditor.', {
3265
+ taskId: task.id,
3266
+ stepId,
3267
+ decisionAuditor: decision.auditor,
3268
+ planAuditor: audit.auditor.trim(),
3269
+ });
3270
+ }
3271
+ assertCorrectiveAuditorIndependence(store, task, stepId, audit.auditor);
3272
+ const reviewed = new Set(audit.reviewedFindingIds);
3273
+ if (reviewed.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
3274
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
3275
+ }
3276
+ const missing = decision.coveredFindingIds.filter((id) => !reviewed.has(id));
3277
+ if (missing.length > 0) {
3278
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every finding in the corrective decision.', {
3279
+ taskId: task.id,
3280
+ stepId,
3281
+ missingFindingIds: missing,
3282
+ });
3283
+ }
3284
+ }
3285
+ function sameOrderedStrings(left, right) {
3286
+ return left.length === right.length && left.every((value, index) => value === right[index]);
3287
+ }
3288
+ function assertNonTargetStepsUnchanged(task, plan, targetStepId) {
3289
+ const stripRuntime = (step) => stripStepRuntime(step);
3290
+ const previous = task.steps.filter((step) => step.id !== targetStepId).map(stripRuntime);
3291
+ const replacement = plan.steps.filter((step) => step.id !== targetStepId);
3292
+ if (JSON.stringify(previous) !== JSON.stringify(replacement)) {
3293
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan must preserve every non-target Step exactly.', {
3294
+ taskId: task.id,
3295
+ targetStepId,
3296
+ });
3297
+ }
3298
+ }
3299
+ function milestoneScopeDigest(milestone) {
3300
+ return sha256Hex(canonicalJsonStringify({
3301
+ id: milestone.id,
3302
+ outcome: milestone.outcome,
3303
+ successSignal: milestone.successSignal,
3304
+ acceptance: milestone.acceptance,
3305
+ checks: milestone.checks,
3306
+ taskIds: milestone.taskIds,
3307
+ memberships: milestone.memberships,
3308
+ membershipRevision: milestone.membershipRevision,
3309
+ planHash: milestone.planHash,
3310
+ }));
3311
+ }
2884
3312
  function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
2885
3313
  appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
2886
3314
  recordedAt: now.toISOString(),