codex-workflow-v2 2.0.0-alpha.6 → 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.
@@ -7,13 +7,14 @@ import { assertDelegationCanAuthorize, delegationCanAuthorize, hashDelegationPol
7
7
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
8
8
  import { pathAllowed, validatePlan } from './domain/validation.js';
9
9
  import { WorkflowError } from './errors.js';
10
+ import { inspectLocalDependency } from './diagnostics.js';
10
11
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
11
12
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
12
13
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
13
14
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
14
- 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';
15
16
  import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
16
- import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
17
+ import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, readReviewerAttestations, } from './alpha6/review.js';
17
18
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
18
19
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
19
20
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
@@ -23,12 +24,15 @@ import { WriterLockManager } from './state/lock.js';
23
24
  import { FileStateStore } from './state/store.js';
24
25
  import { createEntityId } from './ulid.js';
25
26
  import { PACKAGE_VERSION } from './version.js';
27
+ const ALPHA6_CORRECTIVE_RESCUE_HEAD = '5c63798cc7b8fbb5e1c8b26ebf93876bc642295a';
26
28
  export class WorkflowService {
27
29
  store;
28
30
  now;
29
- constructor(store = new FileStateStore(), now = () => new Date()) {
31
+ alpha6CorrectiveRescueHead;
32
+ constructor(store = new FileStateStore(), now = () => new Date(), alpha6CorrectiveRescueHead = ALPHA6_CORRECTIVE_RESCUE_HEAD) {
30
33
  this.store = store;
31
34
  this.now = now;
35
+ this.alpha6CorrectiveRescueHead = alpha6CorrectiveRescueHead;
32
36
  }
33
37
  context(repository) {
34
38
  const identity = resolveRepositoryIdentity(repository);
@@ -564,14 +568,13 @@ export class WorkflowService {
564
568
  this.validateKnowledgeTargets(knowledgeMap, plan);
565
569
  const task = this.store.readTask(identity.projectId, taskId);
566
570
  assertExpectedRevision(task, expectedRevision);
567
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
571
+ if (!['planning', 'awaiting_execution_authorization', 'ready', 'needs_fix', 'blocked'].includes(task.status)) {
568
572
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
569
573
  }
570
574
  const currentPlanCorrectiveDecisions = task.planHash
571
575
  ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
572
576
  : [];
573
577
  const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
574
- const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
575
578
  const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
576
579
  const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
577
580
  if (currentHardBlockingDecisions.length > 0) {
@@ -592,33 +595,22 @@ export class WorkflowService {
592
595
  requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
593
596
  });
594
597
  }
595
- if (currentContinueFixDecisions.length > 0) {
596
- 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.`, {
597
- taskId: task.id,
598
- requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
599
- decisions: currentContinueFixDecisions.map((event) => ({
600
- eventId: event.eventId,
601
- stepId: event.stepId,
602
- triggeringAttemptCount: event.triggeringAttemptCount,
603
- })),
604
- });
605
- }
606
- const currentPlanRiskAudit = task.status === 'needs_fix'
607
- ? readCurrentPlanRiskAudit(this.store, task)
608
- : null;
598
+ const currentPlanRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
609
599
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
610
600
  if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
611
601
  validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
612
602
  }
613
- const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
603
+ const guardedRemediationGate = ['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)
604
+ && currentPlanRiskAudit
614
605
  ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
615
606
  : null;
616
- if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
617
- 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.`, {
618
609
  taskId: task.id,
619
610
  stepId: guardedRemediationGate.stepId,
620
611
  attemptCount: guardedRemediationGate.attemptCount,
621
612
  requiredSidecar: 'corrective-decisions.jsonl',
613
+ requiredAction: 'Run task corrective-decision first.',
622
614
  allowedDecision: 'continue-fix',
623
615
  escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
624
616
  });
@@ -630,25 +622,57 @@ export class WorkflowService {
630
622
  requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
631
623
  });
632
624
  }
633
- if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
634
- 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.`, {
635
633
  taskId: task.id,
636
634
  stepId: guardedRemediationGate.stepId,
637
- decision: correctiveAudit?.decision ?? null,
635
+ decisionEventId: guardedRemediationGate.correctiveDecision.eventId,
638
636
  });
639
637
  }
640
- for (const requirement of task.requirements) {
641
- if (!plan.requirements.includes(requirement)) {
642
- 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
+ });
643
653
  }
644
654
  }
645
- for (const acceptance of task.acceptance) {
646
- if (!plan.acceptance.includes(acceptance)) {
647
- 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
+ }
648
665
  }
649
666
  }
650
667
  const root = this.store.taskRoot(identity.projectId, taskId);
651
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
+ }
652
676
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
653
677
  ? { ...authorization, decision: 'superseded' }
654
678
  : authorization);
@@ -678,9 +702,8 @@ export class WorkflowService {
678
702
  stepId: guardedRemediationGate.stepId,
679
703
  });
680
704
  }
681
- if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
682
- assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
683
- appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
705
+ if (guardedContinueFixDecision) {
706
+ assertNonTargetStepsUnchanged(task, plan, guardedRemediationGate.stepId);
684
707
  }
685
708
  const candidate = {
686
709
  ...task,
@@ -703,12 +726,16 @@ export class WorkflowService {
703
726
  evidenceHash: null,
704
727
  blockReason: null,
705
728
  };
706
- if (planRiskAudit)
707
- validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
729
+ const effectivePlanRiskAudit = planRiskAudit ?? (guardedContinueFixDecision ? currentPlanRiskAudit?.audit ?? null : null);
730
+ if (effectivePlanRiskAudit)
731
+ validatePlanRiskAuditCandidate(this.store, candidate, effectivePlanRiskAudit);
708
732
  const saved = this.store.writeTask(candidate, expectedRevision);
709
733
  this.store.publishArtifact(stagedPlan);
710
- if (planRiskAudit)
711
- 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
+ }
712
739
  if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
713
740
  appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
714
741
  }
@@ -734,6 +761,11 @@ export class WorkflowService {
734
761
  if (activeStep) {
735
762
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
736
763
  }
764
+ const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
765
+ const breakerPlanRiskAudit = currentPlanRiskAudit
766
+ && findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
767
+ ? currentPlanRiskAudit
768
+ : null;
737
769
  const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
738
770
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
739
771
  if (task.knowledgeMapRevision === knowledgeMap.revision &&
@@ -793,6 +825,9 @@ export class WorkflowService {
793
825
  blockReason: null,
794
826
  }, expectedRevision);
795
827
  this.store.publishArtifact(stagedPlan);
828
+ if (breakerPlanRiskAudit) {
829
+ appendReboundPlanRiskAudit(this.store, task, saved, breakerPlanRiskAudit, this.now());
830
+ }
796
831
  return saved;
797
832
  }
798
833
  refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
@@ -814,6 +849,8 @@ export class WorkflowService {
814
849
  const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
815
850
  const transitions = [];
816
851
  const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
852
+ const breakerPlanRiskAuditWillBePreserved = currentPlanRiskAudit
853
+ && findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit) !== null;
817
854
  // Validate every delegated transition before the first write so a narrow grant cannot
818
855
  // leave the composite operation half-applied.
819
856
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
@@ -835,7 +872,7 @@ export class WorkflowService {
835
872
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
836
873
  }
837
874
  let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
838
- if (currentPlanRiskAudit) {
875
+ if (currentPlanRiskAudit && !breakerPlanRiskAuditWillBePreserved) {
839
876
  appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
840
877
  }
841
878
  transitions.push('task knowledge-rebind');
@@ -1117,9 +1154,9 @@ export class WorkflowService {
1117
1154
  currentAuditPresent: Boolean(planRiskAudit),
1118
1155
  });
1119
1156
  }
1120
- if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1121
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1122
- }
1157
+ const remediationGate = planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
1158
+ ? assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
1159
+ : null;
1123
1160
  const dirty = changedFiles(context.identity.repositoryRoot);
1124
1161
  if (step.status === 'planned' && dirty.length > 0) {
1125
1162
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
@@ -1222,17 +1259,31 @@ export class WorkflowService {
1222
1259
  stepId,
1223
1260
  expectedRevision: saved.revision,
1224
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
+ : {}),
1225
1271
  knowledgeMap: knowledgeSummary(knowledgeMap),
1226
1272
  knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
1227
1273
  },
1228
1274
  };
1229
1275
  }
1230
- taskContext(repository, taskId, role) {
1276
+ taskContext(repository, taskId, role, allowHistoricalStrictReview = false) {
1231
1277
  const { identity } = this.context(repository);
1232
1278
  const task = this.store.readTask(identity.projectId, taskId);
1279
+ const historicalStrictReview = allowHistoricalStrictReview
1280
+ && role === 'independent-reviewer'
1281
+ && task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
1233
1282
  const knowledgeMap = task.knowledgeMapRevision === null
1234
1283
  ? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
1235
- : this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
1284
+ : historicalStrictReview
1285
+ ? this.strictReviewKnowledgeMap(identity.repositoryRoot, task)
1286
+ : this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
1236
1287
  const root = this.store.taskRoot(task.projectId, task.id);
1237
1288
  const planRiskAudit = task.planHash && !['planning', 'merged', 'cancelled'].includes(task.status)
1238
1289
  ? readCurrentPlanRiskAudit(this.store, task)
@@ -1387,7 +1438,161 @@ export class WorkflowService {
1387
1438
  });
1388
1439
  return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
1389
1440
  }
1441
+ alpha6C1StrictReviewRescuePreflight(repository) {
1442
+ const context = this.context(repository);
1443
+ const dependency = inspectLocalDependency(context.identity.repositoryRoot);
1444
+ const tasks = context.store.listTasks(context.identity.projectId);
1445
+ const dirty = changedFiles(context.identity.repositoryRoot);
1446
+ const currentHead = headCommit(context.identity.repositoryRoot);
1447
+ const now = this.now().getTime();
1448
+ const candidates = [];
1449
+ const malformedCandidates = [];
1450
+ for (const task of tasks) {
1451
+ if (task.status !== 'in_progress')
1452
+ continue;
1453
+ const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
1454
+ if (!planRiskAudit)
1455
+ continue;
1456
+ for (const step of task.steps) {
1457
+ if (step.status !== 'in_progress' || !step.evidence)
1458
+ continue;
1459
+ if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
1460
+ continue;
1461
+ try {
1462
+ const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
1463
+ if (cycle.resolution !== 'pending')
1464
+ continue;
1465
+ validateTaskHistory(context.identity.repositoryRoot, task);
1466
+ const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
1467
+ const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
1468
+ const knowledgeBindingStale = knowledgeMap.status !== 'active'
1469
+ || task.knowledgeMapRevision !== knowledgeMap.revision
1470
+ || task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
1471
+ const posture = readTaskC1Posture(this.store, task);
1472
+ const lease = context.locks.inspect(task.id);
1473
+ const claimant = posture.state === 'claimed' ? posture.claimant : null;
1474
+ const tokenBound = posture.state === 'claimed'
1475
+ && lease !== null
1476
+ && posture.writerLeaseTokenHash !== null
1477
+ && posture.writerLeaseTokenHash === sha256Hex(lease.token);
1478
+ candidates.push({
1479
+ taskId: task.id,
1480
+ taskRevision: task.revision,
1481
+ stepId: step.id,
1482
+ completionCommit: step.evidence.commitSha,
1483
+ currentHead,
1484
+ knowledgeBindingStale,
1485
+ claimant,
1486
+ handoffExpiresAt: posture.state === 'claimed' ? posture.latestEvent.expiresAt : null,
1487
+ lease: lease
1488
+ ? {
1489
+ entityId: lease.entityId,
1490
+ owner: lease.owner,
1491
+ pid: lease.pid,
1492
+ hostname: lease.hostname,
1493
+ acquiredAt: lease.acquiredAt,
1494
+ heartbeatAt: lease.heartbeatAt,
1495
+ expiresAt: lease.expiresAt,
1496
+ stale: Date.parse(lease.expiresAt) <= now,
1497
+ tokenBound,
1498
+ }
1499
+ : null,
1500
+ });
1501
+ }
1502
+ catch (error) {
1503
+ malformedCandidates.push(error instanceof Error ? error.message : String(error));
1504
+ }
1505
+ }
1506
+ }
1507
+ const candidate = candidates.length === 1 ? candidates[0] : null;
1508
+ const unrelatedLeases = context.locks.list().filter((lease) => lease.entityId !== candidate?.taskId);
1509
+ const blockers = [
1510
+ ...(dependency.declared !== '2.0.0-alpha.6'
1511
+ ? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
1512
+ : []),
1513
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1514
+ ...(candidates.length !== 1
1515
+ ? [`Expected exactly one pending alpha.6 strict review; found ${candidates.length}.`]
1516
+ : []),
1517
+ ...(candidate && candidate.currentHead !== candidate.completionCommit
1518
+ ? ['HEAD does not match the pending strict review completion commit.']
1519
+ : []),
1520
+ ...(candidate && !candidate.knowledgeBindingStale
1521
+ ? ['The pending strict review is not blocked by a stale Project Knowledge Map.']
1522
+ : []),
1523
+ ...(candidate && candidate.claimant === null
1524
+ ? ['The rescue Task does not have an active claimed C1 posture.']
1525
+ : []),
1526
+ ...(candidate && candidate.lease === null
1527
+ ? ['The claimed C1 rescue Task has no bound writer lease.']
1528
+ : []),
1529
+ ...(candidate?.lease && candidate.claimant !== candidate.lease.owner
1530
+ ? ['The writer lease owner does not match the active C1 claimant.']
1531
+ : []),
1532
+ ...(candidate?.lease && !candidate.lease.tokenBound
1533
+ ? ['The writer lease token does not match the active C1 binding.']
1534
+ : []),
1535
+ ...(unrelatedLeases.length > 0
1536
+ ? ['A writer lease exists outside the exact rescue Task.']
1537
+ : []),
1538
+ ...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
1539
+ ];
1540
+ const eligible = blockers.length === 0 && candidate !== null && candidate.claimant !== null;
1541
+ return {
1542
+ eligible,
1543
+ mutationFree: true,
1544
+ runnerPackageVersion: PACKAGE_VERSION,
1545
+ requiredProjectPackageVersion: '2.0.0-alpha.6',
1546
+ declaredProjectPackageVersion: dependency.declared,
1547
+ projectId: context.identity.projectId,
1548
+ repositoryRoot: context.identity.repositoryRoot,
1549
+ clean: dirty.length === 0,
1550
+ candidate,
1551
+ blockers,
1552
+ requiredAction: eligible && candidate && candidate.claimant
1553
+ ? {
1554
+ action: 'update rescue-review',
1555
+ taskId: candidate.taskId,
1556
+ stepId: candidate.stepId,
1557
+ expectedRevision: candidate.taskRevision,
1558
+ actor: candidate.claimant,
1559
+ requiresBoundWriterToken: true,
1560
+ }
1561
+ : null,
1562
+ };
1563
+ }
1564
+ alpha6C1StrictReviewRescue(repository, taskId, stepId, expectedRevision, actor, writerToken, runner) {
1565
+ const preflight = this.alpha6C1StrictReviewRescuePreflight(repository);
1566
+ if (!preflight.eligible || !preflight.candidate || !preflight.requiredAction) {
1567
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 C1 strict-review rescue preflight is not eligible.', {
1568
+ blockers: preflight.blockers,
1569
+ });
1570
+ }
1571
+ const candidate = preflight.candidate;
1572
+ if (candidate.taskId !== taskId
1573
+ || candidate.stepId !== stepId
1574
+ || candidate.taskRevision !== expectedRevision) {
1575
+ throw new WorkflowError('STATE_CONFLICT', 'Rescue command does not match the exact preflight candidate.', {
1576
+ expected: preflight.requiredAction,
1577
+ actual: { taskId, stepId, expectedRevision, actor },
1578
+ });
1579
+ }
1580
+ if (candidate.claimant !== actor.trim()) {
1581
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Rescue actor does not match the active C1 claimant.', {
1582
+ expectedActor: candidate.claimant,
1583
+ actualActor: actor.trim(),
1584
+ });
1585
+ }
1586
+ const lease = this.context(repository).locks.inspect(taskId);
1587
+ if (!lease || lease.token !== writerToken) {
1588
+ throw new WorkflowError('LOCK_TOKEN_INVALID', `Writer token does not own ${taskId}`);
1589
+ }
1590
+ return this.reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, true);
1591
+ }
1390
1592
  reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
1593
+ return this.reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, false);
1594
+ }
1595
+ reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, allowStaleKnowledgeBinding) {
1391
1596
  const context = this.context(repository);
1392
1597
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1393
1598
  const task = this.store.readTask(context.identity.projectId, taskId);
@@ -1398,7 +1603,8 @@ export class WorkflowService {
1398
1603
  throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
1399
1604
  }
1400
1605
  assertTaskBranch(context.identity.repositoryRoot, task);
1401
- this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1606
+ if (!allowStaleKnowledgeBinding)
1607
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1402
1608
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1403
1609
  if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1404
1610
  throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
@@ -1459,7 +1665,7 @@ export class WorkflowService {
1459
1665
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
1460
1666
  }
1461
1667
  ensurePendingStrictStepReview(this.store, task, stepId, this.now());
1462
- const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
1668
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer', allowStaleKnowledgeBinding);
1463
1669
  const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
1464
1670
  const recorded = recordStrictStepReview(this.store, task, stepId, {
1465
1671
  review: launch.review,
@@ -1567,7 +1773,7 @@ export class WorkflowService {
1567
1773
  assertExpectedRevision(task, expectedRevision);
1568
1774
  this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1569
1775
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1570
- if (task.status !== 'needs_fix' && task.status !== 'blocked') {
1776
+ if (!['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)) {
1571
1777
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
1572
1778
  }
1573
1779
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
@@ -1590,25 +1796,45 @@ export class WorkflowService {
1590
1796
  stepId,
1591
1797
  });
1592
1798
  }
1593
- if (task.status === 'needs_fix') {
1594
- if (step.status !== 'failed') {
1595
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
1596
- taskId,
1597
- stepId,
1598
- stepStatus: step.status,
1599
- });
1600
- }
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
+ });
1601
1806
  }
1602
- else {
1603
- const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1604
- if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
1605
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
1606
- taskId,
1607
- stepId,
1608
- stepStatus: step.status,
1609
- resolution: currentCycle.resolution,
1610
- });
1611
- }
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
+ });
1612
1838
  }
1613
1839
  assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
1614
1840
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
@@ -1845,6 +2071,243 @@ export class WorkflowService {
1845
2071
  adoption,
1846
2072
  };
1847
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
+ }
1848
2311
  scanKnowledgeMap(repository) {
1849
2312
  const { identity } = this.context(repository);
1850
2313
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
@@ -2132,6 +2595,25 @@ export class WorkflowService {
2132
2595
  ? this.delegatedApprovalOptions(projectId, transition, task)
2133
2596
  : [];
2134
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);
2135
2617
  const next = nextForTask(task);
2136
2618
  const handoffPosture = readTaskC1Posture(this.store, task);
2137
2619
  const strictStepReviewStep = planRiskAudit
@@ -2204,7 +2686,53 @@ export class WorkflowService {
2204
2686
  },
2205
2687
  }
2206
2688
  : {}),
2207
- ...(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 } : {}),
2208
2736
  };
2209
2737
  if (handoffPosture.state === 'pending') {
2210
2738
  return {
@@ -2371,6 +2899,28 @@ export class WorkflowService {
2371
2899
  }
2372
2900
  return inspected;
2373
2901
  }
2902
+ strictReviewKnowledgeMap(repositoryRoot, task) {
2903
+ const stored = this.store.readKnowledgeMap(task.projectId);
2904
+ const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repositoryRoot, task.projectId, this.now()));
2905
+ if (inspected.status === 'active' && inspected.approval)
2906
+ return inspected;
2907
+ const storedHash = hashKnowledgeMap(stored);
2908
+ if (stored.status === 'active'
2909
+ && stored.approval
2910
+ && task.knowledgeMapRevision === stored.revision
2911
+ && task.knowledgeMapHash === storedHash
2912
+ && stored.approval.mapHash === storedHash) {
2913
+ return stored;
2914
+ }
2915
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review has neither a current active Knowledge Map nor its exact historical Task binding.', {
2916
+ taskId: task.id,
2917
+ inspectedStatus: inspected.status,
2918
+ taskKnowledgeMapRevision: task.knowledgeMapRevision,
2919
+ storedKnowledgeMapRevision: stored.revision,
2920
+ taskKnowledgeMapHash: task.knowledgeMapHash,
2921
+ storedKnowledgeMapHash: storedHash,
2922
+ });
2923
+ }
2374
2924
  inspectAdoptionPreparation(identity, tasks, milestones) {
2375
2925
  const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
2376
2926
  const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
@@ -2698,6 +3248,67 @@ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
2698
3248
  throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
2699
3249
  }
2700
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
+ }
2701
3312
  function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
2702
3313
  appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
2703
3314
  recordedAt: now.toISOString(),