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

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 (36) 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 +407 -43
  32. package/dist/src/workflow.js.map +1 -1
  33. package/docs/release.md +3 -1
  34. package/package.json +1 -1
  35. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +36 -0
  36. 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,
@@ -969,7 +976,7 @@ export class WorkflowService {
969
976
  resultHash,
970
977
  evidenceHash,
971
978
  headCommit: currentHead,
972
- reason: `Confirmed with ${gate.confirmationCode}.`,
979
+ reason: 'Milestone final acceptance confirmed against the current validated state.',
973
980
  authorizationMode: delegation ? 'delegated' : 'human',
974
981
  ...(delegation ? { delegation } : {}),
975
982
  };
@@ -1416,7 +1423,7 @@ export class WorkflowService {
1416
1423
  claimTokenAvailable: false,
1417
1424
  };
1418
1425
  }
1419
- handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
1426
+ handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
1420
1427
  const context = this.#mutationContext(repository);
1421
1428
  const task = this.store.readTask(context.identity.projectId, taskId);
1422
1429
  assertExpectedRevision(task, expectedRevision);
@@ -1475,6 +1482,7 @@ export class WorkflowService {
1475
1482
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1476
1483
  grantId,
1477
1484
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
1485
+ replacePending,
1478
1486
  }, this.now());
1479
1487
  releaseLease();
1480
1488
  return { task, event: prepared.event, bundle: prepared.bundle };
@@ -1542,6 +1550,43 @@ export class WorkflowService {
1542
1550
  throw error;
1543
1551
  }
1544
1552
  }
1553
+ replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
1554
+ const context = this.#mutationContext(repository);
1555
+ const task = this.store.readTask(context.identity.projectId, taskId);
1556
+ assertExpectedRevision(task, expectedRevision);
1557
+ const normalizedActor = actor.trim();
1558
+ if (!normalizedActor)
1559
+ throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
1560
+ const posture = readTaskC1Posture(this.store, task);
1561
+ const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1562
+ const action = String(navigation.action);
1563
+ const currentLease = context.locks.inspect(taskId);
1564
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
1565
+ const requiredActor = writerTokenContract
1566
+ ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1567
+ : null;
1568
+ if (!requiredActor || requiredActor !== normalizedActor) {
1569
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
1570
+ taskId,
1571
+ posture: posture.state,
1572
+ claimant: posture.state === 'claimed' ? posture.claimant : null,
1573
+ actor: normalizedActor,
1574
+ requiredActor,
1575
+ nextAction: action,
1576
+ });
1577
+ }
1578
+ const lease = currentLease
1579
+ ? context.locks.replaceCredential(taskId, normalizedActor)
1580
+ : context.locks.acquire(taskId, normalizedActor);
1581
+ if (posture.state === 'claimed') {
1582
+ appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1583
+ }
1584
+ return {
1585
+ task,
1586
+ lease: toPublicWriterLease(lease, this.now()),
1587
+ writerCredential: { disposition: 'issued', token: lease.token },
1588
+ };
1589
+ }
1545
1590
  handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1546
1591
  const context = this.#mutationContext(repository);
1547
1592
  const task = this.store.readTask(context.identity.projectId, taskId);
@@ -1636,6 +1681,7 @@ export class WorkflowService {
1636
1681
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
1637
1682
  }
1638
1683
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1684
+ let resumesCorrectiveReplan = false;
1639
1685
  if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1640
1686
  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
1687
  taskId,
@@ -1650,20 +1696,21 @@ export class WorkflowService {
1650
1696
  && guardedPosture.correctiveDecision.planHash !== task.planHash
1651
1697
  ? guardedPosture.correctiveDecision
1652
1698
  : null;
1653
- const recovery = staleDecision
1699
+ const recovery = staleDecision?.decision === 'continue-fix'
1654
1700
  ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
1655
1701
  : null;
1656
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1702
+ const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1703
+ resumesCorrectiveReplan = gate.posture === 'replan-ready';
1657
1704
  }
1658
1705
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1659
1706
  const dirty = changedFiles(context.identity.repositoryRoot);
1660
- if (step.status === 'planned' && dirty.length > 0) {
1707
+ if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
1661
1708
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
1662
1709
  changedFiles: dirty,
1663
1710
  });
1664
1711
  }
1665
1712
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
1666
- if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
1713
+ if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
1667
1714
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
1668
1715
  changedFiles: dirtyOutsideStep,
1669
1716
  });
@@ -1986,10 +2033,18 @@ export class WorkflowService {
1986
2033
  resolution: currentCycle.resolution,
1987
2034
  });
1988
2035
  }
1989
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
2036
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2037
+ if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2038
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
2039
+ taskId: task.id,
2040
+ stepId,
2041
+ infrastructureFailures,
2042
+ requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
2043
+ });
2044
+ }
1990
2045
  }
1991
2046
  else if (currentCycle.resolution !== 'pending') {
1992
- if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
2047
+ if (currentCycle.resolution === 'failed') {
1993
2048
  if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1994
2049
  throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1995
2050
  taskId: task.id,
@@ -1997,7 +2052,7 @@ export class WorkflowService {
1997
2052
  resolution: currentCycle.resolution,
1998
2053
  });
1999
2054
  }
2000
- appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2055
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2001
2056
  completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
2002
2057
  checksHash: currentCycle.reviewRecordedEvent.checksHash,
2003
2058
  reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
@@ -2015,7 +2070,7 @@ export class WorkflowService {
2015
2070
  reviewedCommit: launch.reviewedCommit,
2016
2071
  isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
2017
2072
  }, this.now());
2018
- if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
2073
+ if (recorded.resolution === 'failed') {
2019
2074
  const reviewRecordedEvent = recorded.reviewRecordedEvent;
2020
2075
  const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
2021
2076
  if (!reviewRecordedEvent || !reviewerAttestationEvent) {
@@ -2025,7 +2080,7 @@ export class WorkflowService {
2025
2080
  resolution: recorded.resolution,
2026
2081
  });
2027
2082
  }
2028
- appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2083
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2029
2084
  completionCommit: reviewRecordedEvent.completionCommit,
2030
2085
  checksHash: reviewRecordedEvent.checksHash,
2031
2086
  reviewEventHash: reviewRecordedEvent.eventHash,
@@ -2034,6 +2089,130 @@ export class WorkflowService {
2034
2089
  }
2035
2090
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
2036
2091
  }
2092
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2093
+ const identity = resolveRepositoryIdentity(repository);
2094
+ const task = this.store.readTask(identity.projectId, taskId);
2095
+ assertExpectedRevision(task, expectedRevision);
2096
+ if (task.status !== 'in_progress' && task.status !== 'blocked') {
2097
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
2098
+ }
2099
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2100
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2101
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
2102
+ }
2103
+ const step = requireStep(task, stepId);
2104
+ if (step.status !== 'in_progress' || !step.evidence) {
2105
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
2106
+ }
2107
+ validateTaskHistory(identity.repositoryRoot, task);
2108
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
2109
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2110
+ }
2111
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2112
+ return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
2113
+ }
2114
+ recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2115
+ const context = this.#mutationContext(repository);
2116
+ const task = this.store.readTask(context.identity.projectId, taskId);
2117
+ assertExpectedRevision(task, expectedRevision);
2118
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2119
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2120
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2121
+ let review;
2122
+ try {
2123
+ review = validateExternalStrictStepReviewInput(input, packet);
2124
+ }
2125
+ catch (error) {
2126
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
2127
+ }
2128
+ if (review.reviewer.trim() === actor.trim()) {
2129
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
2130
+ }
2131
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
2132
+ if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
2133
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
2134
+ taskId,
2135
+ stepId,
2136
+ resolution: currentCycle.resolution,
2137
+ });
2138
+ }
2139
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2140
+ if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2141
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
2142
+ taskId,
2143
+ stepId,
2144
+ infrastructureFailures,
2145
+ });
2146
+ }
2147
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2148
+ const recorded = recordStrictStepReview(this.store, task, stepId, {
2149
+ review,
2150
+ reviewerOutputHash: strictReviewInputHash(review),
2151
+ reviewedCommit: packet.reviewedCommit,
2152
+ isolationResult: 'verified',
2153
+ }, this.now());
2154
+ if (recorded.resolution === 'failed') {
2155
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2156
+ completionCommit: recorded.reviewRecordedEvent.completionCommit,
2157
+ checksHash: recorded.reviewRecordedEvent.checksHash,
2158
+ reviewEventHash: recorded.reviewRecordedEvent.eventHash,
2159
+ reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
2160
+ }, readCurrentPlanRiskAudit(this.store, task), this.now());
2161
+ }
2162
+ const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2163
+ return {
2164
+ mode: 'external-sealed',
2165
+ packetHash: packet.packetHash,
2166
+ repositorySealHash: packet.repositorySealHash,
2167
+ reviewerThreadId: input.reviewerThreadId,
2168
+ task: saved,
2169
+ };
2170
+ }
2171
+ prepareExternalTaskReview(repository, taskId, expectedRevision) {
2172
+ const identity = resolveRepositoryIdentity(repository);
2173
+ const task = this.store.readTask(identity.projectId, taskId);
2174
+ assertExpectedRevision(task, expectedRevision);
2175
+ const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
2176
+ if (task.status !== 'validating' && !retryingUnverified) {
2177
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
2178
+ }
2179
+ validateTaskHistory(identity.repositoryRoot, task);
2180
+ assertTaskBranch(identity.repositoryRoot, task);
2181
+ if (!isClean(identity.repositoryRoot)) {
2182
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
2183
+ }
2184
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2185
+ if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2186
+ throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2187
+ }
2188
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
2189
+ }
2190
+ recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2191
+ const context = this.#mutationContext(repository);
2192
+ const task = this.store.readTask(context.identity.projectId, taskId);
2193
+ assertExpectedRevision(task, expectedRevision);
2194
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2195
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2196
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
2197
+ let review;
2198
+ try {
2199
+ review = validateExternalStrictTaskReviewInput(input, packet);
2200
+ }
2201
+ catch (error) {
2202
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
2203
+ }
2204
+ if (review.reviewer.trim() === actor.trim()) {
2205
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
2206
+ }
2207
+ const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2208
+ return {
2209
+ mode: 'external-sealed',
2210
+ packetHash: packet.packetHash,
2211
+ repositorySealHash: packet.repositorySealHash,
2212
+ reviewerThreadId: input.reviewerThreadId,
2213
+ task: saved,
2214
+ };
2215
+ }
2037
2216
  submitTask(repository, taskId, expectedRevision, writerToken, actor) {
2038
2217
  const context = this.#mutationContext(repository);
2039
2218
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -2586,21 +2765,13 @@ export class WorkflowService {
2586
2765
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2587
2766
  if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2588
2767
  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
- }
2768
+ const inspected = inspectKnowledgeMap(current, scanned);
2769
+ const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
2770
+ if (refreshOwner) {
2771
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2772
+ taskId: refreshOwner.task.id,
2773
+ requiredAction: 'Run the exact task context-refresh action returned by next.',
2774
+ });
2604
2775
  }
2605
2776
  }
2606
2777
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
@@ -2629,7 +2800,7 @@ export class WorkflowService {
2629
2800
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
2630
2801
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
2631
2802
  }
2632
- next(repository) {
2803
+ next(repository, taskId = null) {
2633
2804
  const status = this.status(repository);
2634
2805
  if (status.observation.kind === 'blocked') {
2635
2806
  return {
@@ -2743,14 +2914,75 @@ export class WorkflowService {
2743
2914
  });
2744
2915
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
2745
2916
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
2746
- const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2917
+ const requestedTask = taskId === null
2918
+ ? null
2919
+ : status.tasks.find((task) => task.id === taskId) ?? null;
2920
+ if (taskId !== null && !requestedTask) {
2921
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
2922
+ }
2923
+ if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
2924
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
2925
+ taskId: requestedTask.id,
2926
+ taskStatus: requestedTask.status,
2927
+ milestoneId: requestedTask.milestoneId,
2928
+ });
2929
+ }
2930
+ const activeTask = requestedTask
2931
+ ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2747
2932
  if (activeTask) {
2748
- const activePlanRiskAudit = activeTask.planHash
2749
- ? readCurrentPlanRiskAudit(this.store, activeTask)
2933
+ const activeTaskMilestone = activeTask.milestoneId
2934
+ ? milestonesById.get(activeTask.milestoneId) ?? null
2750
2935
  : null;
2936
+ const milestoneCancellationSafe = Boolean(activeTaskMilestone
2937
+ && !activeTask.steps.some((step) => step.status === 'in_progress')
2938
+ && status.observation.leases.length === 0
2939
+ && status.observation.coreTaskOperations.length === 0
2940
+ && status.observation.pendingProjectTransactions.length === 0
2941
+ && status.observation.pendingMilestoneTransactions.length === 0
2942
+ && status.observation.pendingTaskTransactions.length === 0
2943
+ && status.observation.corruptTaskTransactions.length === 0);
2944
+ const withMilestoneCancellation = (value) => (milestoneCancellationSafe
2945
+ ? {
2946
+ ...value,
2947
+ milestoneCancellationOption: {
2948
+ command: 'milestone cancel',
2949
+ milestoneId: activeTaskMilestone.id,
2950
+ expectedRevision: activeTaskMilestone.revision,
2951
+ reasonRequired: true,
2952
+ taskDisposition: 'retained-historical',
2953
+ },
2954
+ }
2955
+ : value);
2956
+ let activePlanRiskAudit = null;
2957
+ let planRiskAuditRecovery = null;
2958
+ if (activeTask.planHash) {
2959
+ try {
2960
+ activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
2961
+ }
2962
+ catch (error) {
2963
+ if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
2964
+ throw error;
2965
+ planRiskAuditRecovery = {
2966
+ scope: 'task',
2967
+ id: activeTask.id,
2968
+ status: activeTask.status,
2969
+ action: 'task plan-risk-audit',
2970
+ revision: activeTask.revision,
2971
+ planRiskAuditRecovery: {
2972
+ state: 'required',
2973
+ reason: error.message,
2974
+ currentBriefHash: activeTask.briefHash,
2975
+ currentPlanHash: activeTask.planHash,
2976
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
2977
+ knowledgeMapHash: activeTask.knowledgeMapHash,
2978
+ },
2979
+ };
2980
+ }
2981
+ }
2751
2982
  const taskNext = activeTask.status === 'planning'
2752
2983
  ? null
2753
- : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2984
+ : planRiskAuditRecovery
2985
+ ?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2754
2986
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
2755
2987
  // strict review are executable while context refresh correctly remains forbidden.
2756
2988
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -2762,12 +2994,27 @@ export class WorkflowService {
2762
2994
  || hardStop
2763
2995
  || terminalCorrectiveDecision === 'split-required'
2764
2996
  || terminalCorrectiveDecision === 'stop-escalate') {
2765
- return withAdoption(taskNext);
2997
+ return withAdoption(withMilestoneCancellation(taskNext));
2766
2998
  }
2767
2999
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
2768
- return withAdoption(taskNext);
3000
+ return withAdoption(withMilestoneCancellation(taskNext));
3001
+ }
3002
+ let knowledgeMap;
3003
+ try {
3004
+ knowledgeMap = this.knowledgeMapStatus(repository);
3005
+ }
3006
+ catch (error) {
3007
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3008
+ throw error;
3009
+ return withAdoption(withMilestoneCancellation({
3010
+ scope: 'project-memory',
3011
+ status: 'missing',
3012
+ action: 'project-memory scan',
3013
+ stalenessOriginTaskId: activeTask.id,
3014
+ authorizesTask: false,
3015
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
3016
+ }));
2769
3017
  }
2770
- const knowledgeMap = this.knowledgeMapStatus(repository);
2771
3018
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
2772
3019
  const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
2773
3020
  const delegatedApprovalOptions = action === 'project-memory approve'
@@ -2783,7 +3030,17 @@ export class WorkflowService {
2783
3030
  const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
2784
3031
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2785
3032
  : [];
2786
- return withAdoption({
3033
+ if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
3034
+ const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
3035
+ if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
3036
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
3037
+ taskId: requestedTask.id,
3038
+ blockingTaskId: refreshOwner.task.id,
3039
+ requiredAction: 'Run repository next and its exact task context-refresh action first.',
3040
+ });
3041
+ }
3042
+ }
3043
+ return withAdoption(withMilestoneCancellation({
2787
3044
  scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2788
3045
  status: knowledgeMap.status,
2789
3046
  action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
@@ -2815,14 +3072,14 @@ export class WorkflowService {
2815
3072
  },
2816
3073
  }
2817
3074
  : {}),
2818
- });
3075
+ }));
2819
3076
  }
2820
3077
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
2821
3078
  if (taskCanRebindKnowledge(activeTask) &&
2822
3079
  activeTask.planHash &&
2823
3080
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
2824
3081
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
2825
- return withAdoption({
3082
+ return withAdoption(withMilestoneCancellation({
2826
3083
  scope: 'task',
2827
3084
  id: activeTask.id,
2828
3085
  status: activeTask.status,
@@ -2830,9 +3087,9 @@ export class WorkflowService {
2830
3087
  revision: activeTask.revision,
2831
3088
  knowledgeMapRevision: knowledgeMap.revision,
2832
3089
  knowledgeMapHash,
2833
- });
3090
+ }));
2834
3091
  }
2835
- return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
3092
+ return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
2836
3093
  }
2837
3094
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2838
3095
  if (activeMilestone) {
@@ -2911,6 +3168,19 @@ export class WorkflowService {
2911
3168
  status: activeMilestone.status,
2912
3169
  action: nextForMilestone(activeMilestone),
2913
3170
  revision: activeMilestone.revision,
3171
+ ...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
3172
+ ? {
3173
+ milestoneAuthorizationOptions: [
3174
+ { action: 'milestone authorize', mode: 'direct-human' },
3175
+ {
3176
+ action: 'milestone autonomy-prepare',
3177
+ mode: 'delegated',
3178
+ requiresExactHumanConfirmation: true,
3179
+ maximumDurationHours: 72,
3180
+ },
3181
+ ],
3182
+ }
3183
+ : {}),
2914
3184
  ...(activeMilestone.status === 'active'
2915
3185
  ? {
2916
3186
  completionPolicy: {
@@ -3092,6 +3362,11 @@ export class WorkflowService {
3092
3362
  return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
3093
3363
  }) ?? null
3094
3364
  : null;
3365
+ const strictReviewInfrastructureFailures = strictStepReviewStep
3366
+ ? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
3367
+ : 0;
3368
+ const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
3369
+ || (task.status === 'blocked' && task.review.status === 'unverified');
3095
3370
  const handoffOverlay = handoffPosture.state === 'pending'
3096
3371
  ? {
3097
3372
  action: 'task claim',
@@ -3100,6 +3375,15 @@ export class WorkflowService {
3100
3375
  state: 'pending',
3101
3376
  targetActor: handoffPosture.targetActor,
3102
3377
  eventId: handoffPosture.latestEvent.eventId,
3378
+ credentialRecovery: {
3379
+ action: 'task handoff-replace',
3380
+ when: 'claim credential is unavailable',
3381
+ requiredActor: handoffPosture.latestEvent.actor,
3382
+ targetActor: handoffPosture.targetActor,
3383
+ grantId: handoffPosture.latestEvent.grantId,
3384
+ expectedRevision: task.revision,
3385
+ invalidatesPriorCredential: true,
3386
+ },
3103
3387
  },
3104
3388
  }
3105
3389
  : handoffPosture.state === 'claimed'
@@ -3361,6 +3645,30 @@ export class WorkflowService {
3361
3645
  strictStepReview: {
3362
3646
  reviewRequired: true,
3363
3647
  completionCommit: strictStepReviewStep.evidence.commitSha,
3648
+ infrastructureFailures: strictReviewInfrastructureFailures,
3649
+ maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
3650
+ recommendedMode: 'external-sealed',
3651
+ externalSealedReview: {
3652
+ packetCommand: 'task step-review-packet',
3653
+ recordCommand: 'task step-review-record',
3654
+ reviewerMustUseSeparateChat: true,
3655
+ repositorySealRequired: true,
3656
+ },
3657
+ },
3658
+ }
3659
+ : {}),
3660
+ ...(strictTaskReviewRequired
3661
+ ? {
3662
+ strictTaskReview: {
3663
+ reviewRequired: true,
3664
+ localInfrastructureFailureObserved: task.review.status === 'unverified',
3665
+ recommendedMode: 'external-sealed',
3666
+ externalSealedReview: {
3667
+ packetCommand: 'task review-packet',
3668
+ recordCommand: 'task review-sealed-record',
3669
+ reviewerMustUseSeparateChat: true,
3670
+ repositorySealRequired: true,
3671
+ },
3364
3672
  },
3365
3673
  }
3366
3674
  : {}),
@@ -3414,6 +3722,12 @@ export class WorkflowService {
3414
3722
  exactInclusionRequired: true,
3415
3723
  briefHash: task.briefHash,
3416
3724
  knowledgeMap: knowledgeSummary(map),
3725
+ planRiskAuditContract: {
3726
+ required: true,
3727
+ option: '--risk-audit-file',
3728
+ atomicWithPlanSet: true,
3729
+ plannerAuditorSeparationRequired: true,
3730
+ },
3417
3731
  };
3418
3732
  }
3419
3733
  assertTaskContextChangeAllowed(task) {
@@ -3493,6 +3807,22 @@ export class WorkflowService {
3493
3807
  policyHash: grant.policyHash,
3494
3808
  }));
3495
3809
  }
3810
+ findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
3811
+ for (const task of tasks) {
3812
+ if (!taskIsActionableFromRepositoryNext(task, milestonesById)
3813
+ || !task.planHash
3814
+ || !taskCanRebindKnowledge(task)
3815
+ || this.taskHasTerminalCorrectiveDecision(task))
3816
+ continue;
3817
+ const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
3818
+ if (!assessment.eligible)
3819
+ continue;
3820
+ const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
3821
+ if (options.length > 0)
3822
+ return { task, assessment, options };
3823
+ }
3824
+ return null;
3825
+ }
3496
3826
  assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
3497
3827
  const grant = this.store.readDelegation(projectId, grantId);
3498
3828
  if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
@@ -3595,14 +3925,44 @@ export class WorkflowService {
3595
3925
  if (!action)
3596
3926
  return next;
3597
3927
  const policy = gitMutationPolicyForAction(task, action);
3598
- const claimedC1Posture = readTaskC1Posture(this.store, task).state === 'claimed';
3928
+ const c1Posture = readTaskC1Posture(this.store, task);
3929
+ const claimedC1Posture = c1Posture.state === 'claimed';
3599
3930
  const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
3931
+ const activeLease = writerTokenContract
3932
+ ? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
3933
+ : null;
3934
+ const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
3935
+ const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
3936
+ ? {
3937
+ action: 'task writer-credential-replace',
3938
+ when: 'the active writer credential is unavailable',
3939
+ requiredActor: writerCredentialRecoveryActor,
3940
+ expectedRevision: task.revision,
3941
+ invalidatesPriorCredential: true,
3942
+ acquiresLeaseWhenAbsent: true,
3943
+ }
3944
+ : null;
3600
3945
  return {
3601
3946
  ...next,
3602
3947
  ...(policy ? { gitMutationPolicy: policy } : {}),
3603
3948
  ...(writerTokenContract ? { writerTokenContract } : {}),
3949
+ ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
3604
3950
  };
3605
3951
  }
3952
+ writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
3953
+ if (claimedActor)
3954
+ return claimedActor;
3955
+ if (activeLeaseOwner)
3956
+ return activeLeaseOwner;
3957
+ const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
3958
+ ? 'final_acceptance'
3959
+ : ['task step-complete', 'task submit'].includes(action)
3960
+ ? 'execution'
3961
+ : null;
3962
+ if (!authorizationKind)
3963
+ return null;
3964
+ return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
3965
+ }
3606
3966
  describeMilestoneInitialAssembly(projectId, milestone, tasks) {
3607
3967
  const assessment = assessMilestoneInitialAssembly({
3608
3968
  store: this.store,
@@ -4373,8 +4733,10 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
4373
4733
  const claimedC1LeaseBound = [
4374
4734
  'task run',
4375
4735
  'task step-review',
4736
+ 'task step-review-record',
4376
4737
  'task review-record',
4377
4738
  'task review-launch',
4739
+ 'task review-sealed-record',
4378
4740
  'task corrective-decision-recover',
4379
4741
  'task remediation-mode-recover',
4380
4742
  'task corrective-decision',
@@ -4392,6 +4754,8 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
4392
4754
  option: '--writer-token',
4393
4755
  source: 'active-writer-lease',
4394
4756
  sensitive: true,
4757
+ renewalDisposition: 'reuse-presented-token',
4758
+ tokenReissuedOnRenewal: false,
4395
4759
  };
4396
4760
  }
4397
4761
  function observationPreflightBlockers(observation) {