codex-workflow-v2 2.0.0-beta.13.5 → 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.
@@ -3,7 +3,7 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
3
3
  import { createHash } from 'node:crypto';
4
4
  import path from 'node:path';
5
5
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
6
- import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
6
+ import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, workflowDependencyVersionAt, } from './dependency-provenance.js';
7
7
  import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
8
8
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
9
9
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
@@ -15,11 +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, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
23
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';
24
25
  import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
25
26
  import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
@@ -2756,11 +2757,15 @@ export class WorkflowService {
2756
2757
  const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
2757
2758
  ? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2758
2759
  : null;
2760
+ const rootCauseReplanDirtyCarryover = step.status === 'planned' && dirty.length > 0
2761
+ ? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
2762
+ : null;
2759
2763
  if (step.status === 'planned'
2760
2764
  && dirty.length > 0
2761
2765
  && !resumesCorrectiveReplan
2762
2766
  && !downstreamProofCarryover?.eligible
2763
- && !planIntegrityDirtyCarryover?.eligible) {
2767
+ && !planIntegrityDirtyCarryover?.eligible
2768
+ && !rootCauseReplanDirtyCarryover?.eligible) {
2764
2769
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
2765
2770
  changedFiles: dirty,
2766
2771
  ...(downstreamProofCarryover?.applicable
@@ -2769,6 +2774,9 @@ export class WorkflowService {
2769
2774
  ...(planIntegrityDirtyCarryover?.applicable
2770
2775
  ? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
2771
2776
  : {}),
2777
+ ...(rootCauseReplanDirtyCarryover?.applicable
2778
+ ? { rootCauseReplanDirtyCarryoverBlockers: rootCauseReplanDirtyCarryover.blockers }
2779
+ : {}),
2772
2780
  });
2773
2781
  }
2774
2782
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
@@ -4174,7 +4182,299 @@ export class WorkflowService {
4174
4182
  const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4175
4183
  if (!task)
4176
4184
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4177
- return this.inspectDependencyProvenanceCandidate(snapshot, task);
4185
+ const candidate = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, headCommit(snapshot.identity.repositoryRoot));
4186
+ const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, candidate?.parentCommitSha);
4187
+ const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
4188
+ if (!preflight.rootCauseReplan)
4189
+ return preflight;
4190
+ let baseTip = null;
4191
+ let baseDependency = null;
4192
+ try {
4193
+ baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
4194
+ baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
4195
+ }
4196
+ catch {
4197
+ // The blocker below keeps a missing or unreadable base reference fail closed.
4198
+ }
4199
+ const bridgeBlockers = [
4200
+ ...(preflight.dependencyCommits.length !== 1
4201
+ ? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
4202
+ ...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
4203
+ ? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
4204
+ ...(preflight.parentCommitSha
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}.`] : []),
4209
+ ...(!baseDependency
4210
+ || baseDependency.commitSha !== baseTip
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.'] : []),
4215
+ ];
4216
+ return bridgeBlockers.length === 0
4217
+ ? preflight
4218
+ : {
4219
+ ...preflight,
4220
+ eligible: false,
4221
+ action: null,
4222
+ commitSha: null,
4223
+ parentCommitSha: null,
4224
+ blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
4225
+ };
4226
+ }
4227
+ correctiveCarryoverUpdatePreflight(repository, taskId, candidate = null) {
4228
+ const snapshot = this.observationSnapshot(repository);
4229
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
4230
+ if (!task)
4231
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
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';
4235
+ const sourceSurfaces = {
4236
+ declared: versions.declared,
4237
+ locked: versions.locked,
4238
+ installed: versions.installed,
4239
+ currentBranch: versions.currentBranch,
4240
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
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);
4247
+ const now = this.now().getTime();
4248
+ const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
4249
+ const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
4250
+ const transactionCount = snapshot.observation.pendingProjectTransactions.length
4251
+ + snapshot.observation.pendingMilestoneTransactions.length
4252
+ + snapshot.observation.pendingTaskTransactions.length;
4253
+ const authorizationBlockers = [];
4254
+ if (assessment.eligible) {
4255
+ try {
4256
+ this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
4257
+ }
4258
+ catch (error) {
4259
+ authorizationBlockers.push(error instanceof Error ? error.message : String(error));
4260
+ }
4261
+ }
4262
+ const baseBlockers = [
4263
+ ...assessment.blockers,
4264
+ ...authorizationBlockers,
4265
+ ...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
4266
+ ...Object.entries(sourceSurfaces)
4267
+ .filter(([, version]) => version !== sourceVersion)
4268
+ .map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
4269
+ ...observationPreflightBlockers(snapshot.observation),
4270
+ ...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
4271
+ ...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
4272
+ ...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
4273
+ ...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
4274
+ ? ['The exact original Worker C1 claim must remain current.'] : []),
4275
+ ...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
4276
+ ...(staleLeases.some((lease) => lease.entityId !== task.id
4277
+ || c1.state !== 'claimed' || lease.owner !== c1.claimant
4278
+ || createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
4279
+ ? ['No unrelated or differently owned stale writer lease may exist.'] : []),
4280
+ ...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
4281
+ ];
4282
+ const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
4283
+ ? staleLeases[0]
4284
+ : null;
4285
+ const blockers = [
4286
+ ...baseBlockers,
4287
+ ...(repairableStale
4288
+ ? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
4289
+ : []),
4290
+ ];
4291
+ const compatibility = assessment.compatibility;
4292
+ return {
4293
+ readOnly: true,
4294
+ eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
4295
+ action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
4296
+ ? 'update corrective-carryover-transport'
4297
+ : null,
4298
+ projectId: task.projectId,
4299
+ taskId: task.id,
4300
+ taskRevision: task.revision,
4301
+ stepId: compatibility?.stepId ?? null,
4302
+ sourceVersion,
4303
+ targetVersion,
4304
+ sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
4305
+ taskBranch: task.taskBranch,
4306
+ baseBranch: task.baseBranch,
4307
+ dirtyFiles: compatibility?.dirtyFiles ?? [],
4308
+ dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
4309
+ compatibilityHash: compatibility?.compatibilityHash ?? null,
4310
+ c1Posture: c1.state === 'claimed' ? {
4311
+ state: 'claimed',
4312
+ claimant: c1.claimant,
4313
+ claimEventId: compatibility?.c1ClaimEventId ?? null,
4314
+ claimEventHash: compatibility?.c1ClaimEventHash ?? null,
4315
+ } : null,
4316
+ staleLeaseRepair: repairableStale ? [{
4317
+ action: 'locks repair',
4318
+ entityId: task.id,
4319
+ owner: repairableStale.owner,
4320
+ expiresAt: repairableStale.expiresAt,
4321
+ }] : [],
4322
+ requiredTransport: [
4323
+ 'Commit only package.json and package-lock.json on the active Milestone base.',
4324
+ 'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
4325
+ 'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
4326
+ ],
4327
+ blockers,
4328
+ };
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
+ };
4178
4478
  }
4179
4479
  historicalStepProvenanceRecoveryPreflight(repository, taskId) {
4180
4480
  const snapshot = this.observationSnapshot(repository);
@@ -4645,7 +4945,9 @@ export class WorkflowService {
4645
4945
  ? Boolean(record.historicalStepProvenance)
4646
4946
  : recoveryMode === 'plan-integrity'
4647
4947
  ? Boolean(record.planIntegrity)
4648
- : Boolean(record.activeDownstreamProof) === recoveryMode));
4948
+ : recoveryMode === false && record.rootCauseReplan
4949
+ ? true
4950
+ : Boolean(record.activeDownstreamProof) === recoveryMode));
4649
4951
  if (prior && current.systemCommits.includes(currentHead))
4650
4952
  return current;
4651
4953
  assertExpectedRevision(current, expectedRevision);
@@ -4661,13 +4963,26 @@ export class WorkflowService {
4661
4963
  ? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
4662
4964
  : recoveryMode
4663
4965
  ? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
4664
- : this.inspectDependencyProvenanceCandidate(snapshot, task);
4966
+ : this.dependencyProvenanceRecoveryPreflight(repository, taskId);
4665
4967
  if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
4666
4968
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
4667
4969
  taskId,
4668
4970
  blockers: preflight.blockers,
4669
4971
  });
4670
4972
  }
4973
+ if (preflight.rootCauseReplan) {
4974
+ const posture = readTaskC1Posture(this.store, task);
4975
+ if (posture.state !== 'claimed'
4976
+ || posture.claimant !== preflight.rootCauseReplan.claimant
4977
+ || normalizedActor !== posture.claimant) {
4978
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
4979
+ taskId,
4980
+ actor: normalizedActor,
4981
+ requiredActor: preflight.rootCauseReplan.claimant,
4982
+ posture: posture.state,
4983
+ });
4984
+ }
4985
+ }
4671
4986
  const now = this.now();
4672
4987
  const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
4673
4988
  const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
@@ -4693,6 +5008,9 @@ export class WorkflowService {
4693
5008
  ...(isTerminalCommit && preflight.planIntegrity
4694
5009
  ? { planIntegrity: preflight.planIntegrity }
4695
5010
  : {}),
5011
+ ...(isTerminalCommit && preflight.rootCauseReplan
5012
+ ? { rootCauseReplan: preflight.rootCauseReplan }
5013
+ : {}),
4696
5014
  };
4697
5015
  });
4698
5016
  const nextTask = {
@@ -6039,6 +6357,9 @@ export class WorkflowService {
6039
6357
  }
6040
6358
  const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
6041
6359
  const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
6360
+ const rootCauseStepId = nextStepId
6361
+ ?? task.steps.find((candidate) => candidate.status === 'planned')?.id
6362
+ ?? null;
6042
6363
  const downstreamProofCarryover = next.action === 'task run'
6043
6364
  && nextStepId
6044
6365
  && changedFiles(repositoryRoot).length > 0
@@ -6049,6 +6370,24 @@ export class WorkflowService {
6049
6370
  && changedFiles(repositoryRoot).length > 0
6050
6371
  ? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
6051
6372
  : null;
6373
+ let rootCauseReplanDirtyCarryover = ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
6374
+ && rootCauseStepId
6375
+ && changedFiles(repositoryRoot).length > 0
6376
+ ? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
6377
+ : null;
6378
+ if (rootCauseReplanDirtyCarryover?.eligible) {
6379
+ try {
6380
+ this.assertExecutionAuthorizationFresh(task, repositoryRoot);
6381
+ }
6382
+ catch (error) {
6383
+ rootCauseReplanDirtyCarryover = {
6384
+ applicable: true,
6385
+ eligible: false,
6386
+ compatibility: null,
6387
+ blockers: [error instanceof Error ? error.message : String(error)],
6388
+ };
6389
+ }
6390
+ }
6052
6391
  let mechanicalFeasibilityOverlay = {};
6053
6392
  if (task.status === 'awaiting_execution_authorization' && task.planHash) {
6054
6393
  try {
@@ -6678,7 +7017,7 @@ export class WorkflowService {
6678
7017
  downstreamProofCarryover: {
6679
7018
  state: 'validated',
6680
7019
  invalidationEventId: downstreamProofCarryover.invalidationEventId,
6681
- stepId: nextStepId,
7020
+ stepId: rootCauseStepId,
6682
7021
  dirtyFiles: downstreamProofCarryover.dirtyFiles,
6683
7022
  dirtyWorktreeHash: downstreamProofCarryover.dirtyWorktreeHash,
6684
7023
  meaning: 'The replacement Step may adopt exactly these preserved uncommitted bytes on task run.',
@@ -6718,6 +7057,52 @@ export class WorkflowService {
6718
7057
  },
6719
7058
  }
6720
7059
  : {}),
7060
+ ...(rootCauseReplanDirtyCarryover?.applicable
7061
+ ? rootCauseReplanDirtyCarryover.eligible && rootCauseReplanDirtyCarryover.compatibility
7062
+ ? {
7063
+ action: 'task run',
7064
+ rootCauseReplanDirtyCarryover: {
7065
+ state: 'validated',
7066
+ stepId: nextStepId,
7067
+ compatibilityHash: rootCauseReplanDirtyCarryover.compatibility.compatibilityHash,
7068
+ dirtyFiles: rootCauseReplanDirtyCarryover.compatibility.dirtyFiles,
7069
+ dirtyWorktreeHash: rootCauseReplanDirtyCarryover.compatibility.dirtyWorktreeHash,
7070
+ meaning: 'The replacement Step may adopt the exact retained root-cause failure bytes.',
7071
+ },
7072
+ }
7073
+ : rootCauseReplanDirtyCarryover.blockers.length === 1
7074
+ && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1')
7075
+ ? handoffPosture.state === 'none'
7076
+ ? {
7077
+ action: 'task handoff-prepare',
7078
+ blockedAction: 'task run',
7079
+ rootCauseReplanDirtyCarryover: {
7080
+ state: 'awaiting-c1',
7081
+ stepId: rootCauseStepId,
7082
+ requiredActor: defaultTaskWorkerActor(task.id),
7083
+ },
7084
+ }
7085
+ : handoffPosture.state === 'pending'
7086
+ ? {}
7087
+ : {
7088
+ action: 'doctor',
7089
+ blockedAction: 'task run',
7090
+ rootCauseReplanDirtyCarryover: {
7091
+ state: 'blocked',
7092
+ stepId: rootCauseStepId,
7093
+ blockers: rootCauseReplanDirtyCarryover.blockers,
7094
+ },
7095
+ }
7096
+ : {
7097
+ action: 'doctor',
7098
+ blockedAction: 'task run',
7099
+ rootCauseReplanDirtyCarryover: {
7100
+ state: 'blocked',
7101
+ stepId: rootCauseStepId,
7102
+ blockers: rootCauseReplanDirtyCarryover.blockers,
7103
+ },
7104
+ }
7105
+ : {}),
6721
7106
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
6722
7107
  ...(correctiveDerivedAuthority && correctiveDerivedActor
6723
7108
  ? {
@@ -6734,7 +7119,10 @@ export class WorkflowService {
6734
7119
  }
6735
7120
  : {}),
6736
7121
  };
6737
- if (handoffPosture.state === 'pending') {
7122
+ const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
7123
+ || (rootCauseReplanDirtyCarryover.blockers.length === 1
7124
+ && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
7125
+ if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
6738
7126
  return this.withTaskNavigationContracts(task, {
6739
7127
  ...result,
6740
7128
  action: 'task claim',
@@ -7256,7 +7644,7 @@ export class WorkflowService {
7256
7644
  blockers,
7257
7645
  };
7258
7646
  }
7259
- inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
7647
+ inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null, rootCauseReplan = null) {
7260
7648
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
7261
7649
  const now = this.now().getTime();
7262
7650
  return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
@@ -7276,7 +7664,89 @@ export class WorkflowService {
7276
7664
  currentBranch: versions.currentBranch,
7277
7665
  baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
7278
7666
  },
7279
- }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
7667
+ }, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
7668
+ }
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;
7673
+ if (recorded) {
7674
+ const recordedStep = task.steps.find((candidate) => candidate.id === recorded.stepId) ?? null;
7675
+ if (!recordedStep) {
7676
+ return {
7677
+ applicable: true,
7678
+ eligible: false,
7679
+ compatibility: null,
7680
+ blockers: [`Recorded corrective carryover Step ${recorded.stepId} is missing from the current Plan.`],
7681
+ };
7682
+ }
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
+ }
7730
+ }
7731
+ const candidates = task.steps
7732
+ .filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
7733
+ .flatMap((step) => [...new Set(readRemediationEvents(this.store, task)
7734
+ .filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed')
7735
+ .map((event) => event.failureEvidence.completionCommit))]
7736
+ .map((historyHead) => assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead })))
7737
+ .filter((assessment) => assessment.applicable);
7738
+ const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
7739
+ if (eligible.length === 1)
7740
+ return eligible[0];
7741
+ return {
7742
+ applicable: candidates.length > 0,
7743
+ eligible: false,
7744
+ compatibility: null,
7745
+ blockers: [...new Set([
7746
+ ...candidates.flatMap((assessment) => assessment.blockers),
7747
+ ...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
7748
+ ])],
7749
+ };
7280
7750
  }
7281
7751
  inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
7282
7752
  const now = this.now().getTime();