codex-workflow-v2 2.0.0-beta.4 → 2.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/src/alpha6/handoff.d.ts +2 -0
  3. package/dist/src/alpha6/handoff.js +124 -12
  4. package/dist/src/alpha6/handoff.js.map +1 -1
  5. package/dist/src/alpha6/plan-risk.js +9 -7
  6. package/dist/src/alpha6/plan-risk.js.map +1 -1
  7. package/dist/src/alpha6/review.d.ts +1 -0
  8. package/dist/src/alpha6/review.js +9 -0
  9. package/dist/src/alpha6/review.js.map +1 -1
  10. package/dist/src/cli.js +72 -9
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/contracts.d.ts +1 -1
  13. package/dist/src/lifecycle/corrective-replan-credential-core.js +31 -2
  14. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -1
  15. package/dist/src/reviewer.d.ts +39 -1
  16. package/dist/src/reviewer.js +179 -43
  17. package/dist/src/reviewer.js.map +1 -1
  18. package/dist/src/state/corrective-replan-executor.js +15 -5
  19. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  20. package/dist/src/state/corrective-replan-public.js +53 -6
  21. package/dist/src/state/corrective-replan-public.js.map +1 -1
  22. package/dist/src/state/corrective-replan-transaction.d.ts +2 -0
  23. package/dist/src/state/corrective-replan-transaction.js +11 -4
  24. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  25. package/dist/src/state/lock.d.ts +1 -0
  26. package/dist/src/state/lock.js +29 -0
  27. package/dist/src/state/lock.js.map +1 -1
  28. package/dist/src/version.d.ts +1 -1
  29. package/dist/src/version.js +1 -1
  30. package/dist/src/workflow.d.ts +28 -3
  31. package/dist/src/workflow.js +443 -43
  32. package/dist/src/workflow.js.map +1 -1
  33. package/docs/release.md +3 -1
  34. package/docs/stable-release-defect-register.md +53 -2
  35. package/package.json +1 -1
  36. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +36 -0
  37. package/schemas/task-handoff-event.schema.json +1 -1
@@ -9,20 +9,21 @@ import { pathAllowed, validatePlan } from './domain/validation.js';
9
9
  import { WorkflowError } from './errors.js';
10
10
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
11
11
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
12
- import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
12
+ import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
13
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
14
14
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
15
15
  import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
16
  import { assessMilestoneInitialAssembly, applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
17
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
18
- import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
18
+ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
19
19
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
20
20
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
21
21
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
22
22
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
23
+ import { ensureDirectory } from './fs-utils.js';
23
24
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
24
25
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
25
- import { launchStrictStepReviewer } from './reviewer.js';
26
+ import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
26
27
  import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
27
28
  import { FileStateStore } from './state/store.js';
28
29
  import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
@@ -33,6 +34,7 @@ import { inspectWorkflowObservationBoundary, } from './observation.js';
33
34
  import { createEntityId } from './ulid.js';
34
35
  import { PACKAGE_VERSION } from './version.js';
35
36
  import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
37
+ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
36
38
  export class WorkflowService {
37
39
  store;
38
40
  now;
@@ -71,6 +73,11 @@ export class WorkflowService {
71
73
  const projectFile = path.join(projectRoot, 'project.json');
72
74
  const projectAlreadyRegistered = existsSync(projectFile);
73
75
  if (!projectAlreadyRegistered) {
76
+ // The registration posture is a sidecar under the project root. A completely
77
+ // fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
78
+ // only that directory before writing the posture; project.json remains the
79
+ // final registration record written below.
80
+ ensureDirectory(projectRoot);
74
81
  initializeProjectRegistrationAdoption(this.store, {
75
82
  projectId: identity.projectId,
76
83
  repositoryIdentity: identity,
@@ -783,6 +790,7 @@ export class WorkflowService {
783
790
  const { identity } = this.#mutationContext(repository);
784
791
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
785
792
  assertExpectedRevision(milestone, expectedRevision);
793
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
786
794
  return prepareMilestoneAutonomyContract({
787
795
  projectId: identity.projectId,
788
796
  milestone,
@@ -796,6 +804,7 @@ export class WorkflowService {
796
804
  const { identity } = this.#mutationContext(repository);
797
805
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
798
806
  assertExpectedRevision(milestone, expectedRevision);
807
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
799
808
  return applyMilestoneAutonomyContract({
800
809
  store: this.store,
801
810
  projectId: identity.projectId,
@@ -844,6 +853,7 @@ export class WorkflowService {
844
853
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
845
854
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
846
855
  }
856
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
847
857
  const root = this.store.milestoneRoot(identity.projectId, milestoneId);
848
858
  const planHash = this.store.hashArtifact(root, 'plan.json');
849
859
  if (planHash !== milestone.planHash) {
@@ -969,7 +979,7 @@ export class WorkflowService {
969
979
  resultHash,
970
980
  evidenceHash,
971
981
  headCommit: currentHead,
972
- reason: `Confirmed with ${gate.confirmationCode}.`,
982
+ reason: 'Milestone final acceptance confirmed against the current validated state.',
973
983
  authorizationMode: delegation ? 'delegated' : 'human',
974
984
  ...(delegation ? { delegation } : {}),
975
985
  };
@@ -1416,7 +1426,7 @@ export class WorkflowService {
1416
1426
  claimTokenAvailable: false,
1417
1427
  };
1418
1428
  }
1419
- handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
1429
+ handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
1420
1430
  const context = this.#mutationContext(repository);
1421
1431
  const task = this.store.readTask(context.identity.projectId, taskId);
1422
1432
  assertExpectedRevision(task, expectedRevision);
@@ -1475,6 +1485,7 @@ export class WorkflowService {
1475
1485
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1476
1486
  grantId,
1477
1487
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
1488
+ replacePending,
1478
1489
  }, this.now());
1479
1490
  releaseLease();
1480
1491
  return { task, event: prepared.event, bundle: prepared.bundle };
@@ -1542,6 +1553,43 @@ export class WorkflowService {
1542
1553
  throw error;
1543
1554
  }
1544
1555
  }
1556
+ replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
1557
+ const context = this.#mutationContext(repository);
1558
+ const task = this.store.readTask(context.identity.projectId, taskId);
1559
+ assertExpectedRevision(task, expectedRevision);
1560
+ const normalizedActor = actor.trim();
1561
+ if (!normalizedActor)
1562
+ throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
1563
+ const posture = readTaskC1Posture(this.store, task);
1564
+ const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1565
+ const action = String(navigation.action);
1566
+ const currentLease = context.locks.inspect(taskId);
1567
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
1568
+ const requiredActor = writerTokenContract
1569
+ ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1570
+ : null;
1571
+ if (!requiredActor || requiredActor !== normalizedActor) {
1572
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
1573
+ taskId,
1574
+ posture: posture.state,
1575
+ claimant: posture.state === 'claimed' ? posture.claimant : null,
1576
+ actor: normalizedActor,
1577
+ requiredActor,
1578
+ nextAction: action,
1579
+ });
1580
+ }
1581
+ const lease = currentLease
1582
+ ? context.locks.replaceCredential(taskId, normalizedActor)
1583
+ : context.locks.acquire(taskId, normalizedActor);
1584
+ if (posture.state === 'claimed') {
1585
+ appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1586
+ }
1587
+ return {
1588
+ task,
1589
+ lease: toPublicWriterLease(lease, this.now()),
1590
+ writerCredential: { disposition: 'issued', token: lease.token },
1591
+ };
1592
+ }
1545
1593
  handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1546
1594
  const context = this.#mutationContext(repository);
1547
1595
  const task = this.store.readTask(context.identity.projectId, taskId);
@@ -1636,6 +1684,7 @@ export class WorkflowService {
1636
1684
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
1637
1685
  }
1638
1686
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1687
+ let resumesCorrectiveReplan = false;
1639
1688
  if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1640
1689
  throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} has remediation history but is no longer marked for guarded review by the current Plan Risk Audit; execution is blocked until the audit and Step posture are reconciled.`, {
1641
1690
  taskId,
@@ -1650,20 +1699,21 @@ export class WorkflowService {
1650
1699
  && guardedPosture.correctiveDecision.planHash !== task.planHash
1651
1700
  ? guardedPosture.correctiveDecision
1652
1701
  : null;
1653
- const recovery = staleDecision
1702
+ const recovery = staleDecision?.decision === 'continue-fix'
1654
1703
  ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
1655
1704
  : null;
1656
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1705
+ const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1706
+ resumesCorrectiveReplan = gate.posture === 'replan-ready';
1657
1707
  }
1658
1708
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1659
1709
  const dirty = changedFiles(context.identity.repositoryRoot);
1660
- if (step.status === 'planned' && dirty.length > 0) {
1710
+ if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
1661
1711
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
1662
1712
  changedFiles: dirty,
1663
1713
  });
1664
1714
  }
1665
1715
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
1666
- if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
1716
+ if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
1667
1717
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
1668
1718
  changedFiles: dirtyOutsideStep,
1669
1719
  });
@@ -1986,10 +2036,18 @@ export class WorkflowService {
1986
2036
  resolution: currentCycle.resolution,
1987
2037
  });
1988
2038
  }
1989
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
2039
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2040
+ if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2041
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
2042
+ taskId: task.id,
2043
+ stepId,
2044
+ infrastructureFailures,
2045
+ requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
2046
+ });
2047
+ }
1990
2048
  }
1991
2049
  else if (currentCycle.resolution !== 'pending') {
1992
- if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
2050
+ if (currentCycle.resolution === 'failed') {
1993
2051
  if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1994
2052
  throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1995
2053
  taskId: task.id,
@@ -1997,7 +2055,7 @@ export class WorkflowService {
1997
2055
  resolution: currentCycle.resolution,
1998
2056
  });
1999
2057
  }
2000
- appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2058
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2001
2059
  completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
2002
2060
  checksHash: currentCycle.reviewRecordedEvent.checksHash,
2003
2061
  reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
@@ -2015,7 +2073,7 @@ export class WorkflowService {
2015
2073
  reviewedCommit: launch.reviewedCommit,
2016
2074
  isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
2017
2075
  }, this.now());
2018
- if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
2076
+ if (recorded.resolution === 'failed') {
2019
2077
  const reviewRecordedEvent = recorded.reviewRecordedEvent;
2020
2078
  const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
2021
2079
  if (!reviewRecordedEvent || !reviewerAttestationEvent) {
@@ -2025,7 +2083,7 @@ export class WorkflowService {
2025
2083
  resolution: recorded.resolution,
2026
2084
  });
2027
2085
  }
2028
- appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2086
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2029
2087
  completionCommit: reviewRecordedEvent.completionCommit,
2030
2088
  checksHash: reviewRecordedEvent.checksHash,
2031
2089
  reviewEventHash: reviewRecordedEvent.eventHash,
@@ -2034,6 +2092,130 @@ export class WorkflowService {
2034
2092
  }
2035
2093
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
2036
2094
  }
2095
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2096
+ const identity = resolveRepositoryIdentity(repository);
2097
+ const task = this.store.readTask(identity.projectId, taskId);
2098
+ assertExpectedRevision(task, expectedRevision);
2099
+ if (task.status !== 'in_progress' && task.status !== 'blocked') {
2100
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
2101
+ }
2102
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2103
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2104
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
2105
+ }
2106
+ const step = requireStep(task, stepId);
2107
+ if (step.status !== 'in_progress' || !step.evidence) {
2108
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
2109
+ }
2110
+ validateTaskHistory(identity.repositoryRoot, task);
2111
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
2112
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2113
+ }
2114
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2115
+ return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
2116
+ }
2117
+ recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2118
+ const context = this.#mutationContext(repository);
2119
+ const task = this.store.readTask(context.identity.projectId, taskId);
2120
+ assertExpectedRevision(task, expectedRevision);
2121
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2122
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2123
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2124
+ let review;
2125
+ try {
2126
+ review = validateExternalStrictStepReviewInput(input, packet);
2127
+ }
2128
+ catch (error) {
2129
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
2130
+ }
2131
+ if (review.reviewer.trim() === actor.trim()) {
2132
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
2133
+ }
2134
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
2135
+ if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
2136
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
2137
+ taskId,
2138
+ stepId,
2139
+ resolution: currentCycle.resolution,
2140
+ });
2141
+ }
2142
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2143
+ if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2144
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
2145
+ taskId,
2146
+ stepId,
2147
+ infrastructureFailures,
2148
+ });
2149
+ }
2150
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2151
+ const recorded = recordStrictStepReview(this.store, task, stepId, {
2152
+ review,
2153
+ reviewerOutputHash: strictReviewInputHash(review),
2154
+ reviewedCommit: packet.reviewedCommit,
2155
+ isolationResult: 'verified',
2156
+ }, this.now());
2157
+ if (recorded.resolution === 'failed') {
2158
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2159
+ completionCommit: recorded.reviewRecordedEvent.completionCommit,
2160
+ checksHash: recorded.reviewRecordedEvent.checksHash,
2161
+ reviewEventHash: recorded.reviewRecordedEvent.eventHash,
2162
+ reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
2163
+ }, readCurrentPlanRiskAudit(this.store, task), this.now());
2164
+ }
2165
+ const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2166
+ return {
2167
+ mode: 'external-sealed',
2168
+ packetHash: packet.packetHash,
2169
+ repositorySealHash: packet.repositorySealHash,
2170
+ reviewerThreadId: input.reviewerThreadId,
2171
+ task: saved,
2172
+ };
2173
+ }
2174
+ prepareExternalTaskReview(repository, taskId, expectedRevision) {
2175
+ const identity = resolveRepositoryIdentity(repository);
2176
+ const task = this.store.readTask(identity.projectId, taskId);
2177
+ assertExpectedRevision(task, expectedRevision);
2178
+ const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
2179
+ if (task.status !== 'validating' && !retryingUnverified) {
2180
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
2181
+ }
2182
+ validateTaskHistory(identity.repositoryRoot, task);
2183
+ assertTaskBranch(identity.repositoryRoot, task);
2184
+ if (!isClean(identity.repositoryRoot)) {
2185
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
2186
+ }
2187
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2188
+ if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2189
+ throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2190
+ }
2191
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
2192
+ }
2193
+ recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2194
+ const context = this.#mutationContext(repository);
2195
+ const task = this.store.readTask(context.identity.projectId, taskId);
2196
+ assertExpectedRevision(task, expectedRevision);
2197
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2198
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2199
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
2200
+ let review;
2201
+ try {
2202
+ review = validateExternalStrictTaskReviewInput(input, packet);
2203
+ }
2204
+ catch (error) {
2205
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
2206
+ }
2207
+ if (review.reviewer.trim() === actor.trim()) {
2208
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
2209
+ }
2210
+ const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2211
+ return {
2212
+ mode: 'external-sealed',
2213
+ packetHash: packet.packetHash,
2214
+ repositorySealHash: packet.repositorySealHash,
2215
+ reviewerThreadId: input.reviewerThreadId,
2216
+ task: saved,
2217
+ };
2218
+ }
2037
2219
  submitTask(repository, taskId, expectedRevision, writerToken, actor) {
2038
2220
  const context = this.#mutationContext(repository);
2039
2221
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -2586,21 +2768,13 @@ export class WorkflowService {
2586
2768
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2587
2769
  if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2588
2770
  const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
2589
- const activeTask = this.store
2590
- .listTasks(identity.projectId)
2591
- .find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2592
- if (activeTask?.planHash
2593
- && taskCanRebindKnowledge(activeTask)
2594
- && !this.taskHasTerminalCorrectiveDecision(activeTask)) {
2595
- const inspected = inspectKnowledgeMap(current, scanned);
2596
- const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
2597
- if (assessment.eligible
2598
- && this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
2599
- throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2600
- taskId: activeTask.id,
2601
- requiredAction: 'Run the exact task context-refresh action returned by next.',
2602
- });
2603
- }
2771
+ const inspected = inspectKnowledgeMap(current, scanned);
2772
+ const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
2773
+ if (refreshOwner) {
2774
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2775
+ taskId: refreshOwner.task.id,
2776
+ requiredAction: 'Run the exact task context-refresh action returned by next.',
2777
+ });
2604
2778
  }
2605
2779
  }
2606
2780
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
@@ -2629,7 +2803,7 @@ export class WorkflowService {
2629
2803
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
2630
2804
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
2631
2805
  }
2632
- next(repository) {
2806
+ next(repository, taskId = null) {
2633
2807
  const status = this.status(repository);
2634
2808
  if (status.observation.kind === 'blocked') {
2635
2809
  return {
@@ -2742,15 +2916,77 @@ export class WorkflowService {
2742
2916
  observation: status.observation,
2743
2917
  });
2744
2918
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
2919
+ const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
2745
2920
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
2746
- const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2921
+ const requestedTask = taskId === null
2922
+ ? null
2923
+ : status.tasks.find((task) => task.id === taskId) ?? null;
2924
+ if (taskId !== null && !requestedTask) {
2925
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
2926
+ }
2927
+ if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
2928
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
2929
+ taskId: requestedTask.id,
2930
+ taskStatus: requestedTask.status,
2931
+ milestoneId: requestedTask.milestoneId,
2932
+ });
2933
+ }
2934
+ const activeTask = requestedTask
2935
+ ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2747
2936
  if (activeTask) {
2748
- const activePlanRiskAudit = activeTask.planHash
2749
- ? readCurrentPlanRiskAudit(this.store, activeTask)
2937
+ const activeTaskMilestone = activeTask.milestoneId
2938
+ ? milestonesById.get(activeTask.milestoneId) ?? null
2750
2939
  : null;
2940
+ const milestoneCancellationSafe = Boolean(activeTaskMilestone
2941
+ && !activeTask.steps.some((step) => step.status === 'in_progress')
2942
+ && status.observation.leases.length === 0
2943
+ && status.observation.coreTaskOperations.length === 0
2944
+ && status.observation.pendingProjectTransactions.length === 0
2945
+ && status.observation.pendingMilestoneTransactions.length === 0
2946
+ && status.observation.pendingTaskTransactions.length === 0
2947
+ && status.observation.corruptTaskTransactions.length === 0);
2948
+ const withMilestoneCancellation = (value) => (milestoneCancellationSafe
2949
+ ? {
2950
+ ...value,
2951
+ milestoneCancellationOption: {
2952
+ command: 'milestone cancel',
2953
+ milestoneId: activeTaskMilestone.id,
2954
+ expectedRevision: activeTaskMilestone.revision,
2955
+ reasonRequired: true,
2956
+ taskDisposition: 'retained-historical',
2957
+ },
2958
+ }
2959
+ : value);
2960
+ let activePlanRiskAudit = null;
2961
+ let planRiskAuditRecovery = null;
2962
+ if (activeTask.planHash) {
2963
+ try {
2964
+ activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
2965
+ }
2966
+ catch (error) {
2967
+ if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
2968
+ throw error;
2969
+ planRiskAuditRecovery = {
2970
+ scope: 'task',
2971
+ id: activeTask.id,
2972
+ status: activeTask.status,
2973
+ action: 'task plan-risk-audit',
2974
+ revision: activeTask.revision,
2975
+ planRiskAuditRecovery: {
2976
+ state: 'required',
2977
+ reason: error.message,
2978
+ currentBriefHash: activeTask.briefHash,
2979
+ currentPlanHash: activeTask.planHash,
2980
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
2981
+ knowledgeMapHash: activeTask.knowledgeMapHash,
2982
+ },
2983
+ };
2984
+ }
2985
+ }
2751
2986
  const taskNext = activeTask.status === 'planning'
2752
2987
  ? null
2753
- : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2988
+ : planRiskAuditRecovery
2989
+ ?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2754
2990
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
2755
2991
  // strict review are executable while context refresh correctly remains forbidden.
2756
2992
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -2762,12 +2998,27 @@ export class WorkflowService {
2762
2998
  || hardStop
2763
2999
  || terminalCorrectiveDecision === 'split-required'
2764
3000
  || terminalCorrectiveDecision === 'stop-escalate') {
2765
- return withAdoption(taskNext);
3001
+ return withAdoption(withMilestoneCancellation(taskNext));
2766
3002
  }
2767
3003
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
2768
- return withAdoption(taskNext);
3004
+ return withAdoption(withMilestoneCancellation(taskNext));
3005
+ }
3006
+ let knowledgeMap;
3007
+ try {
3008
+ knowledgeMap = inspectCurrentKnowledgeMap();
3009
+ }
3010
+ catch (error) {
3011
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3012
+ throw error;
3013
+ return withAdoption(withMilestoneCancellation({
3014
+ scope: 'project-memory',
3015
+ status: 'missing',
3016
+ action: 'project-memory scan',
3017
+ stalenessOriginTaskId: activeTask.id,
3018
+ authorizesTask: false,
3019
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
3020
+ }));
2769
3021
  }
2770
- const knowledgeMap = this.knowledgeMapStatus(repository);
2771
3022
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
2772
3023
  const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
2773
3024
  const delegatedApprovalOptions = action === 'project-memory approve'
@@ -2783,7 +3034,17 @@ export class WorkflowService {
2783
3034
  const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
2784
3035
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2785
3036
  : [];
2786
- return withAdoption({
3037
+ if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
3038
+ const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
3039
+ if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
3040
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
3041
+ taskId: requestedTask.id,
3042
+ blockingTaskId: refreshOwner.task.id,
3043
+ requiredAction: 'Run repository next and its exact task context-refresh action first.',
3044
+ });
3045
+ }
3046
+ }
3047
+ return withAdoption(withMilestoneCancellation({
2787
3048
  scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2788
3049
  status: knowledgeMap.status,
2789
3050
  action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
@@ -2815,14 +3076,14 @@ export class WorkflowService {
2815
3076
  },
2816
3077
  }
2817
3078
  : {}),
2818
- });
3079
+ }));
2819
3080
  }
2820
3081
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
2821
3082
  if (taskCanRebindKnowledge(activeTask) &&
2822
3083
  activeTask.planHash &&
2823
3084
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
2824
3085
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
2825
- return withAdoption({
3086
+ return withAdoption(withMilestoneCancellation({
2826
3087
  scope: 'task',
2827
3088
  id: activeTask.id,
2828
3089
  status: activeTask.status,
@@ -2830,9 +3091,41 @@ export class WorkflowService {
2830
3091
  revision: activeTask.revision,
2831
3092
  knowledgeMapRevision: knowledgeMap.revision,
2832
3093
  knowledgeMapHash,
2833
- });
3094
+ }));
2834
3095
  }
2835
- return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
3096
+ return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
3097
+ }
3098
+ let repositoryKnowledgeMap;
3099
+ try {
3100
+ repositoryKnowledgeMap = inspectCurrentKnowledgeMap();
3101
+ }
3102
+ catch (error) {
3103
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3104
+ throw error;
3105
+ return withAdoption({
3106
+ scope: 'project-memory',
3107
+ status: 'missing',
3108
+ action: 'project-memory scan',
3109
+ authorizesMilestone: false,
3110
+ provenanceNotice: 'Project Knowledge must be current before Discovery or Milestone authorization.',
3111
+ });
3112
+ }
3113
+ if (repositoryKnowledgeMap.status !== 'active' || !repositoryKnowledgeMap.approval) {
3114
+ const action = repositoryKnowledgeMap.status === 'stale'
3115
+ ? 'project-memory reconcile'
3116
+ : 'project-memory approve';
3117
+ const delegatedApprovalOptions = action === 'project-memory approve'
3118
+ ? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', repositoryKnowledgeMap)
3119
+ : [];
3120
+ return withAdoption({
3121
+ scope: 'project-memory',
3122
+ status: repositoryKnowledgeMap.status,
3123
+ action,
3124
+ revision: repositoryKnowledgeMap.revision,
3125
+ authorizesMilestone: false,
3126
+ provenanceNotice: 'Project Knowledge must be reconciled and approved before Discovery or Milestone authorization.',
3127
+ ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3128
+ });
2836
3129
  }
2837
3130
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2838
3131
  if (activeMilestone) {
@@ -2911,6 +3204,19 @@ export class WorkflowService {
2911
3204
  status: activeMilestone.status,
2912
3205
  action: nextForMilestone(activeMilestone),
2913
3206
  revision: activeMilestone.revision,
3207
+ ...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
3208
+ ? {
3209
+ milestoneAuthorizationOptions: [
3210
+ { action: 'milestone authorize', mode: 'direct-human' },
3211
+ {
3212
+ action: 'milestone autonomy-prepare',
3213
+ mode: 'delegated',
3214
+ requiresExactHumanConfirmation: true,
3215
+ maximumDurationHours: 72,
3216
+ },
3217
+ ],
3218
+ }
3219
+ : {}),
2914
3220
  ...(activeMilestone.status === 'active'
2915
3221
  ? {
2916
3222
  completionPolicy: {
@@ -3092,6 +3398,11 @@ export class WorkflowService {
3092
3398
  return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
3093
3399
  }) ?? null
3094
3400
  : null;
3401
+ const strictReviewInfrastructureFailures = strictStepReviewStep
3402
+ ? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
3403
+ : 0;
3404
+ const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
3405
+ || (task.status === 'blocked' && task.review.status === 'unverified');
3095
3406
  const handoffOverlay = handoffPosture.state === 'pending'
3096
3407
  ? {
3097
3408
  action: 'task claim',
@@ -3100,6 +3411,15 @@ export class WorkflowService {
3100
3411
  state: 'pending',
3101
3412
  targetActor: handoffPosture.targetActor,
3102
3413
  eventId: handoffPosture.latestEvent.eventId,
3414
+ credentialRecovery: {
3415
+ action: 'task handoff-replace',
3416
+ when: 'claim credential is unavailable',
3417
+ requiredActor: handoffPosture.latestEvent.actor,
3418
+ targetActor: handoffPosture.targetActor,
3419
+ grantId: handoffPosture.latestEvent.grantId,
3420
+ expectedRevision: task.revision,
3421
+ invalidatesPriorCredential: true,
3422
+ },
3103
3423
  },
3104
3424
  }
3105
3425
  : handoffPosture.state === 'claimed'
@@ -3361,6 +3681,30 @@ export class WorkflowService {
3361
3681
  strictStepReview: {
3362
3682
  reviewRequired: true,
3363
3683
  completionCommit: strictStepReviewStep.evidence.commitSha,
3684
+ infrastructureFailures: strictReviewInfrastructureFailures,
3685
+ maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
3686
+ recommendedMode: 'external-sealed',
3687
+ externalSealedReview: {
3688
+ packetCommand: 'task step-review-packet',
3689
+ recordCommand: 'task step-review-record',
3690
+ reviewerMustUseSeparateChat: true,
3691
+ repositorySealRequired: true,
3692
+ },
3693
+ },
3694
+ }
3695
+ : {}),
3696
+ ...(strictTaskReviewRequired
3697
+ ? {
3698
+ strictTaskReview: {
3699
+ reviewRequired: true,
3700
+ localInfrastructureFailureObserved: task.review.status === 'unverified',
3701
+ recommendedMode: 'external-sealed',
3702
+ externalSealedReview: {
3703
+ packetCommand: 'task review-packet',
3704
+ recordCommand: 'task review-sealed-record',
3705
+ reviewerMustUseSeparateChat: true,
3706
+ repositorySealRequired: true,
3707
+ },
3364
3708
  },
3365
3709
  }
3366
3710
  : {}),
@@ -3414,6 +3758,12 @@ export class WorkflowService {
3414
3758
  exactInclusionRequired: true,
3415
3759
  briefHash: task.briefHash,
3416
3760
  knowledgeMap: knowledgeSummary(map),
3761
+ planRiskAuditContract: {
3762
+ required: true,
3763
+ option: '--risk-audit-file',
3764
+ atomicWithPlanSet: true,
3765
+ plannerAuditorSeparationRequired: true,
3766
+ },
3417
3767
  };
3418
3768
  }
3419
3769
  assertTaskContextChangeAllowed(task) {
@@ -3493,6 +3843,22 @@ export class WorkflowService {
3493
3843
  policyHash: grant.policyHash,
3494
3844
  }));
3495
3845
  }
3846
+ findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
3847
+ for (const task of tasks) {
3848
+ if (!taskIsActionableFromRepositoryNext(task, milestonesById)
3849
+ || !task.planHash
3850
+ || !taskCanRebindKnowledge(task)
3851
+ || this.taskHasTerminalCorrectiveDecision(task))
3852
+ continue;
3853
+ const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
3854
+ if (!assessment.eligible)
3855
+ continue;
3856
+ const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
3857
+ if (options.length > 0)
3858
+ return { task, assessment, options };
3859
+ }
3860
+ return null;
3861
+ }
3496
3862
  assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
3497
3863
  const grant = this.store.readDelegation(projectId, grantId);
3498
3864
  if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
@@ -3595,14 +3961,44 @@ export class WorkflowService {
3595
3961
  if (!action)
3596
3962
  return next;
3597
3963
  const policy = gitMutationPolicyForAction(task, action);
3598
- const claimedC1Posture = readTaskC1Posture(this.store, task).state === 'claimed';
3964
+ const c1Posture = readTaskC1Posture(this.store, task);
3965
+ const claimedC1Posture = c1Posture.state === 'claimed';
3599
3966
  const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
3967
+ const activeLease = writerTokenContract
3968
+ ? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
3969
+ : null;
3970
+ const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
3971
+ const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
3972
+ ? {
3973
+ action: 'task writer-credential-replace',
3974
+ when: 'the active writer credential is unavailable',
3975
+ requiredActor: writerCredentialRecoveryActor,
3976
+ expectedRevision: task.revision,
3977
+ invalidatesPriorCredential: true,
3978
+ acquiresLeaseWhenAbsent: true,
3979
+ }
3980
+ : null;
3600
3981
  return {
3601
3982
  ...next,
3602
3983
  ...(policy ? { gitMutationPolicy: policy } : {}),
3603
3984
  ...(writerTokenContract ? { writerTokenContract } : {}),
3985
+ ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
3604
3986
  };
3605
3987
  }
3988
+ writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
3989
+ if (claimedActor)
3990
+ return claimedActor;
3991
+ if (activeLeaseOwner)
3992
+ return activeLeaseOwner;
3993
+ const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
3994
+ ? 'final_acceptance'
3995
+ : ['task step-complete', 'task submit'].includes(action)
3996
+ ? 'execution'
3997
+ : null;
3998
+ if (!authorizationKind)
3999
+ return null;
4000
+ return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
4001
+ }
3606
4002
  describeMilestoneInitialAssembly(projectId, milestone, tasks) {
3607
4003
  const assessment = assessMilestoneInitialAssembly({
3608
4004
  store: this.store,
@@ -4373,8 +4769,10 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
4373
4769
  const claimedC1LeaseBound = [
4374
4770
  'task run',
4375
4771
  'task step-review',
4772
+ 'task step-review-record',
4376
4773
  'task review-record',
4377
4774
  'task review-launch',
4775
+ 'task review-sealed-record',
4378
4776
  'task corrective-decision-recover',
4379
4777
  'task remediation-mode-recover',
4380
4778
  'task corrective-decision',
@@ -4392,6 +4790,8 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
4392
4790
  option: '--writer-token',
4393
4791
  source: 'active-writer-lease',
4394
4792
  sensitive: true,
4793
+ renewalDisposition: 'reuse-presented-token',
4794
+ tokenReissuedOnRenewal: false,
4395
4795
  };
4396
4796
  }
4397
4797
  function observationPreflightBlockers(observation) {