codex-workflow-v2 2.0.0-beta.13.6 → 2.0.0-beta.13.7

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.
@@ -15,12 +15,12 @@ import { WorkflowError } from './errors.js';
15
15
  import { withObservedRouteMetadata } from './observed-routes.js';
16
16
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
17
17
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
18
- import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
18
+ import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
19
19
  import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
20
20
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
21
21
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
22
22
  import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
23
- import { assessRootCauseReplanDirtyCarryover, } from './alpha6/root-cause-replan-carryover.js';
23
+ import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
24
24
  import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
25
25
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
26
26
  import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
@@ -4182,7 +4182,8 @@ export class WorkflowService {
4182
4182
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4183
4183
  if (!task)
4184
4184
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4185
- const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4185
+ const candidate = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, headCommit(snapshot.identity.repositoryRoot));
4186
+ const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, candidate?.parentCommitSha);
4186
4187
  const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
4187
4188
  if (!preflight.rootCauseReplan)
4188
4189
  return preflight;
@@ -4201,15 +4202,16 @@ export class WorkflowService {
4201
4202
  ...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
4202
4203
  ? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
4203
4204
  ...(preflight.parentCommitSha
4204
- && workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.5'
4205
- ? ['Corrective carryover dependency parent must declare exact source 2.0.0-beta.13.5.'] : []),
4206
- ...(preflight.dependencyCommits.at(-1)?.packageVersion !== '2.0.0-beta.13.6'
4207
- ? ['Corrective carryover dependency commit must declare exact target 2.0.0-beta.13.6.'] : []),
4205
+ && workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
4206
+ ? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
4207
+ ...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
4208
+ ? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
4208
4209
  ...(!baseDependency
4209
4210
  || baseDependency.commitSha !== baseTip
4210
- || baseDependency.packageVersion !== '2.0.0-beta.13.6'
4211
- || workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha) !== '2.0.0-beta.13.5'
4212
- ? ['Corrective carryover Milestone-base tip must be one exact 2.0.0-beta.13.5 to 2.0.0-beta.13.6 dependency-only commit.'] : []),
4211
+ || baseDependency.packageVersion !== PACKAGE_VERSION
4212
+ || workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
4213
+ !== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
4214
+ ? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
4213
4215
  ];
4214
4216
  return bridgeBlockers.length === 0
4215
4217
  ? preflight
@@ -4222,14 +4224,14 @@ export class WorkflowService {
4222
4224
  blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
4223
4225
  };
4224
4226
  }
4225
- correctiveCarryoverUpdatePreflight(repository, taskId) {
4227
+ correctiveCarryoverUpdatePreflight(repository, taskId, candidate = null) {
4226
4228
  const snapshot = this.observationSnapshot(repository);
4227
4229
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4228
4230
  if (!task)
4229
4231
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4230
- const sourceVersion = '2.0.0-beta.13.5';
4231
- const targetVersion = '2.0.0-beta.13.6';
4232
4232
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4233
+ const sourceVersion = '2.0.0-beta.13.6';
4234
+ const targetVersion = '2.0.0-beta.13.7';
4233
4235
  const sourceSurfaces = {
4234
4236
  declared: versions.declared,
4235
4237
  locked: versions.locked,
@@ -4237,8 +4239,11 @@ export class WorkflowService {
4237
4239
  currentBranch: versions.currentBranch,
4238
4240
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4239
4241
  };
4240
- const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4241
4242
  const c1 = readTaskC1Posture(this.store, task);
4243
+ if (c1.state !== 'claimed') {
4244
+ return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
4245
+ }
4246
+ const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
4242
4247
  const now = this.now().getTime();
4243
4248
  const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
4244
4249
  const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
@@ -4322,6 +4327,155 @@ export class WorkflowService {
4322
4327
  blockers,
4323
4328
  };
4324
4329
  }
4330
+ consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
4331
+ const repositoryRoot = snapshot.identity.repositoryRoot;
4332
+ const sourceVersion = '2.0.0-beta.13.6';
4333
+ const targetVersion = '2.0.0-beta.13.7';
4334
+ const c1 = readTaskC1Posture(this.store, task);
4335
+ const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
4336
+ const currentHead = headCommit(repositoryRoot);
4337
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
4338
+ const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
4339
+ const navigation = this.next(repositoryRoot, task.id);
4340
+ const action = String(navigation.action);
4341
+ const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
4342
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
4343
+ const terminal = readRemediationEvents(this.store, task)
4344
+ .filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
4345
+ const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
4346
+ const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
4347
+ const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
4348
+ const missingClaimOnly = assessment.blockers.length === 1
4349
+ && assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
4350
+ const rollingWave = navigation.rollingWave;
4351
+ const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
4352
+ && rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
4353
+ || (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
4354
+ || (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
4355
+ && c1.state === 'pending' && c1.targetActor === yielded?.actor);
4356
+ const blockers = [
4357
+ ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4358
+ ...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
4359
+ .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4360
+ ...observationPreflightBlockers(snapshot.observation),
4361
+ ...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
4362
+ ...(snapshot.observation.pendingProjectTransactions.length
4363
+ + snapshot.observation.pendingMilestoneTransactions.length
4364
+ + snapshot.observation.pendingTaskTransactions.length
4365
+ + snapshot.observation.corruptTaskTransactions.length
4366
+ + snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
4367
+ ...(!recorded || recorded.stepId === stepId
4368
+ || task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
4369
+ ? ['A different, verified completed Step must own the historical carryover binding.'] : []),
4370
+ ...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
4371
+ ...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
4372
+ || task.workspaceOwner !== 'local'
4373
+ || currentBranch(repositoryRoot) !== task.taskBranch
4374
+ || task.steps.some((candidate) => candidate.status === 'in_progress')
4375
+ ? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
4376
+ ...(!step || dirtyFiles.length === 0
4377
+ || dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
4378
+ ? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
4379
+ ...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
4380
+ || terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
4381
+ ? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
4382
+ ...(!yielded || !terminal || yielded.planHash === null
4383
+ || !Number.isFinite(Date.parse(yielded.recordedAt))
4384
+ || Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
4385
+ ? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
4386
+ ];
4387
+ let candidateBinding = null;
4388
+ if (action === 'task plan-set') {
4389
+ if (!candidate) {
4390
+ blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
4391
+ }
4392
+ else {
4393
+ try {
4394
+ validatePlan(candidate.plan);
4395
+ const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
4396
+ if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
4397
+ throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
4398
+ }
4399
+ const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
4400
+ if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
4401
+ throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
4402
+ }
4403
+ if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
4404
+ throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
4405
+ }
4406
+ if (candidate.plan.objective !== task.planObjective
4407
+ || canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
4408
+ || canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
4409
+ throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
4410
+ }
4411
+ const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
4412
+ this.validateKnowledgeTargets(map, candidate.plan);
4413
+ const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
4414
+ if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
4415
+ === normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
4416
+ throw new Error('Candidate must change the failed Plan semantics before source recovery.');
4417
+ }
4418
+ if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
4419
+ throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
4420
+ }
4421
+ const auditTask = {
4422
+ ...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
4423
+ knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
4424
+ knowledgeMapHash: candidate.plan.knowledgeMapHash,
4425
+ steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
4426
+ ...candidateStep, status: 'planned', evidence: null, failurePause: null,
4427
+ }),
4428
+ };
4429
+ validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
4430
+ candidateBinding = {
4431
+ candidatePlanHash: prepared.planHash,
4432
+ candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
4433
+ riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
4434
+ };
4435
+ }
4436
+ catch (error) {
4437
+ blockers.push(error instanceof Error ? error.message : String(error));
4438
+ }
4439
+ }
4440
+ }
4441
+ try {
4442
+ validateTaskHistory(repositoryRoot, task);
4443
+ this.assertTaskKnowledgeBinding(repositoryRoot, task);
4444
+ const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
4445
+ if (!sourceAudit || !yielded?.planHash || !task.planHash
4446
+ || normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
4447
+ !== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
4448
+ throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
4449
+ }
4450
+ }
4451
+ catch (error) {
4452
+ blockers.push(error instanceof Error ? error.message : String(error));
4453
+ }
4454
+ const binding = {
4455
+ taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
4456
+ planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
4457
+ sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
4458
+ completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
4459
+ terminalRemediationEventHash: terminal?.eventHash ?? null,
4460
+ correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
4461
+ c1Posture: c1.state,
4462
+ candidateBinding,
4463
+ };
4464
+ return {
4465
+ readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
4466
+ action: null, transportAllowed: false,
4467
+ ...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
4468
+ compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
4469
+ sourceRecovery: blockers.length === 0 ? {
4470
+ eligible: true, action,
4471
+ ...(action === 'task plan-set' ? { ...candidateBinding } : {}),
4472
+ ...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
4473
+ ...(action === 'task claim' ? { actor: yielded.actor } : {}),
4474
+ instruction: 'Execute only this exact action with the installed source package; run source status then next and repeat this read-only preflight. Ordinary Plan Risk Audit and execution authorization retain their normal gates.',
4475
+ } : null,
4476
+ blockers,
4477
+ };
4478
+ }
4325
4479
  historicalStepProvenanceRecoveryPreflight(repository, taskId) {
4326
4480
  const snapshot = this.observationSnapshot(repository);
4327
4481
  if (snapshot.observation.kind !== 'assessment') {
@@ -7512,21 +7666,67 @@ export class WorkflowService {
7512
7666
  },
7513
7667
  }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
7514
7668
  }
7515
- assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId) {
7516
- const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
7517
- && task.systemCommits.includes(record.commitSha))?.rootCauseReplan ?? null;
7669
+ assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
7670
+ const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
7671
+ && task.systemCommits.includes(record.commitSha)) ?? null;
7672
+ const recorded = recordedRecovery?.rootCauseReplan ?? null;
7518
7673
  if (recorded) {
7519
- if (stepId && stepId !== recorded.stepId) {
7674
+ const recordedStep = task.steps.find((candidate) => candidate.id === recorded.stepId) ?? null;
7675
+ if (!recordedStep) {
7520
7676
  return {
7521
7677
  applicable: true,
7522
7678
  eligible: false,
7523
7679
  compatibility: null,
7524
- blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
7680
+ blockers: [`Recorded corrective carryover Step ${recorded.stepId} is missing from the current Plan.`],
7525
7681
  };
7526
7682
  }
7527
- return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
7528
- binding: recorded,
7529
- });
7683
+ if (recordedStep.status === 'completed') {
7684
+ const blockers = [];
7685
+ if (!recordedStep.evidence) {
7686
+ blockers.push(`Completed corrective carryover Step ${recorded.stepId} has no execution evidence.`);
7687
+ }
7688
+ try {
7689
+ validateTaskHistory(repositoryRoot, task, historyHead);
7690
+ const sourceAudit = readPlanRiskAuditEvents(this.store, task).find((event) => event.eventId === recorded.replacementPlanRiskAuditEventId
7691
+ && event.eventHash === recorded.replacementPlanRiskAuditEventHash) ?? null;
7692
+ const classification = sourceAudit?.stepClassifications.find((candidate) => candidate.stepId === recorded.stepId) ?? null;
7693
+ if (!sourceAudit || !classification) {
7694
+ blockers.push('Completed corrective carryover Step lost its exact replacement Plan Risk Audit binding.');
7695
+ }
7696
+ else if (classification.reviewRequired
7697
+ && recordedStep.evidence
7698
+ && !verifiedHistoricalStepReviewBindings(this.store, task).some((binding) => binding.stepId === recorded.stepId
7699
+ && binding.commitSha === recordedStep.evidence.commitSha)) {
7700
+ blockers.push(`Completed corrective carryover Step ${recorded.stepId} lacks verified strict-review proof.`);
7701
+ }
7702
+ }
7703
+ catch (error) {
7704
+ blockers.push(error instanceof Error ? error.message : String(error));
7705
+ }
7706
+ if (blockers.length > 0) {
7707
+ return {
7708
+ applicable: true,
7709
+ eligible: false,
7710
+ compatibility: null,
7711
+ blockers: [...new Set(blockers)],
7712
+ };
7713
+ }
7714
+ // A reviewed, history-valid completed Step consumed this carryover. Keep its
7715
+ // immutable recovery record, but let later Steps use their own lifecycle route.
7716
+ }
7717
+ else {
7718
+ if (stepId && stepId !== recorded.stepId) {
7719
+ return {
7720
+ applicable: true,
7721
+ eligible: false,
7722
+ compatibility: null,
7723
+ blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
7724
+ };
7725
+ }
7726
+ return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
7727
+ binding: recorded,
7728
+ });
7729
+ }
7530
7730
  }
7531
7731
  const candidates = task.steps
7532
7732
  .filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))