codex-workflow-v2 2.0.0-beta.1 → 2.0.0-beta.11

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 (187) hide show
  1. package/README.md +94 -12
  2. package/dist/src/alpha6/adoption.js +13 -3
  3. package/dist/src/alpha6/adoption.js.map +1 -1
  4. package/dist/src/alpha6/handoff.d.ts +26 -2
  5. package/dist/src/alpha6/handoff.js +277 -29
  6. package/dist/src/alpha6/handoff.js.map +1 -1
  7. package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
  8. package/dist/src/alpha6/mechanical-feasibility.js +303 -0
  9. package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
  10. package/dist/src/alpha6/milestone.d.ts +37 -2
  11. package/dist/src/alpha6/milestone.js +472 -33
  12. package/dist/src/alpha6/milestone.js.map +1 -1
  13. package/dist/src/alpha6/plan-integrity.d.ts +14 -0
  14. package/dist/src/alpha6/plan-integrity.js +127 -0
  15. package/dist/src/alpha6/plan-integrity.js.map +1 -0
  16. package/dist/src/alpha6/plan-risk.d.ts +1 -0
  17. package/dist/src/alpha6/plan-risk.js +15 -7
  18. package/dist/src/alpha6/plan-risk.js.map +1 -1
  19. package/dist/src/alpha6/remediation.d.ts +2 -1
  20. package/dist/src/alpha6/remediation.js +186 -9
  21. package/dist/src/alpha6/remediation.js.map +1 -1
  22. package/dist/src/alpha6/review.d.ts +3 -0
  23. package/dist/src/alpha6/review.js +24 -0
  24. package/dist/src/alpha6/review.js.map +1 -1
  25. package/dist/src/cli.js +486 -46
  26. package/dist/src/cli.js.map +1 -1
  27. package/dist/src/contracts.d.ts +238 -6
  28. package/dist/src/contracts.js +1 -1
  29. package/dist/src/credential-output.d.ts +10 -0
  30. package/dist/src/credential-output.js +73 -0
  31. package/dist/src/credential-output.js.map +1 -0
  32. package/dist/src/dependency-provenance.d.ts +19 -0
  33. package/dist/src/dependency-provenance.js +185 -0
  34. package/dist/src/dependency-provenance.js.map +1 -0
  35. package/dist/src/errors.d.ts +1 -1
  36. package/dist/src/errors.js.map +1 -1
  37. package/dist/src/git.d.ts +1 -1
  38. package/dist/src/git.js +13 -5
  39. package/dist/src/git.js.map +1 -1
  40. package/dist/src/index.d.ts +2 -1
  41. package/dist/src/index.js +2 -1
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
  44. package/dist/src/lifecycle/canonical-hash.js +24 -0
  45. package/dist/src/lifecycle/canonical-hash.js.map +1 -0
  46. package/dist/src/lifecycle/catalog.d.ts +26 -0
  47. package/dist/src/lifecycle/catalog.js +208 -0
  48. package/dist/src/lifecycle/catalog.js.map +1 -0
  49. package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
  50. package/dist/src/lifecycle/compiler-inspection.js +29 -0
  51. package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
  52. package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
  53. package/dist/src/lifecycle/core-static-readiness.js +130 -0
  54. package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
  55. package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
  56. package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
  57. package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
  58. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
  59. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
  60. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
  61. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
  62. package/dist/src/lifecycle/corrective-replan-credential-core.js +563 -0
  63. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
  64. package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
  65. package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
  66. package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
  67. package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
  68. package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
  69. package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
  70. package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
  71. package/dist/src/lifecycle/corrective-replan.js +553 -0
  72. package/dist/src/lifecycle/corrective-replan.js.map +1 -0
  73. package/dist/src/lifecycle/evaluator.d.ts +2 -0
  74. package/dist/src/lifecycle/evaluator.js +35 -0
  75. package/dist/src/lifecycle/evaluator.js.map +1 -0
  76. package/dist/src/lifecycle/fingerprint.d.ts +31 -0
  77. package/dist/src/lifecycle/fingerprint.js +388 -0
  78. package/dist/src/lifecycle/fingerprint.js.map +1 -0
  79. package/dist/src/lifecycle/immutable.d.ts +1 -0
  80. package/dist/src/lifecycle/immutable.js +10 -0
  81. package/dist/src/lifecycle/immutable.js.map +1 -0
  82. package/dist/src/lifecycle/implementation-table.d.ts +29 -0
  83. package/dist/src/lifecycle/implementation-table.js +87 -0
  84. package/dist/src/lifecycle/implementation-table.js.map +1 -0
  85. package/dist/src/lifecycle/index.d.ts +4 -0
  86. package/dist/src/lifecycle/index.js +5 -0
  87. package/dist/src/lifecycle/index.js.map +1 -0
  88. package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
  89. package/dist/src/lifecycle/schema-artifact.js +214 -0
  90. package/dist/src/lifecycle/schema-artifact.js.map +1 -0
  91. package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
  92. package/dist/src/lifecycle/semantic-registry.js +489 -0
  93. package/dist/src/lifecycle/semantic-registry.js.map +1 -0
  94. package/dist/src/lifecycle/types.d.ts +718 -0
  95. package/dist/src/lifecycle/types.js +4 -0
  96. package/dist/src/lifecycle/types.js.map +1 -0
  97. package/dist/src/observation.d.ts +20 -0
  98. package/dist/src/observation.js +171 -0
  99. package/dist/src/observation.js.map +1 -0
  100. package/dist/src/reviewer.d.ts +43 -1
  101. package/dist/src/reviewer.js +203 -50
  102. package/dist/src/reviewer.js.map +1 -1
  103. package/dist/src/state/corrective-replan-executor.d.ts +64 -0
  104. package/dist/src/state/corrective-replan-executor.js +608 -0
  105. package/dist/src/state/corrective-replan-executor.js.map +1 -0
  106. package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
  107. package/dist/src/state/corrective-replan-public-schema.js +43 -0
  108. package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
  109. package/dist/src/state/corrective-replan-public.d.ts +95 -0
  110. package/dist/src/state/corrective-replan-public.js +665 -0
  111. package/dist/src/state/corrective-replan-public.js.map +1 -0
  112. package/dist/src/state/corrective-replan-transaction.d.ts +160 -0
  113. package/dist/src/state/corrective-replan-transaction.js +730 -0
  114. package/dist/src/state/corrective-replan-transaction.js.map +1 -0
  115. package/dist/src/state/corrective-yield-executor.d.ts +31 -0
  116. package/dist/src/state/corrective-yield-executor.js +236 -0
  117. package/dist/src/state/corrective-yield-executor.js.map +1 -0
  118. package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
  119. package/dist/src/state/corrective-yield-transaction.js +591 -0
  120. package/dist/src/state/corrective-yield-transaction.js.map +1 -0
  121. package/dist/src/state/lock.d.ts +76 -3
  122. package/dist/src/state/lock.js +611 -30
  123. package/dist/src/state/lock.js.map +1 -1
  124. package/dist/src/state/store.d.ts +3 -0
  125. package/dist/src/state/store.js +65 -12
  126. package/dist/src/state/store.js.map +1 -1
  127. package/dist/src/transition-core.d.ts +67 -0
  128. package/dist/src/transition-core.js +318 -0
  129. package/dist/src/transition-core.js.map +1 -0
  130. package/dist/src/version.d.ts +1 -1
  131. package/dist/src/version.js +1 -1
  132. package/dist/src/version.js.map +1 -1
  133. package/dist/src/workflow.d.ts +88 -24
  134. package/dist/src/workflow.js +1647 -270
  135. package/dist/src/workflow.js.map +1 -1
  136. package/docs/autonomy-guardrails.md +61 -1
  137. package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
  138. package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
  139. package/docs/delegated-approval.md +26 -1
  140. package/docs/development-flow.md +63 -8
  141. package/docs/lifecycle/state-machine-stabilization.md +641 -0
  142. package/docs/pdf/README.md +24 -0
  143. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  144. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  145. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  146. package/docs/pdf/requirements.txt +1 -0
  147. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +234 -0
  148. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
  149. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +413 -0
  150. package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
  151. package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
  152. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
  153. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
  154. package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
  155. package/docs/problem-briefs/README.md +120 -0
  156. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
  157. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
  158. package/docs/release.md +30 -6
  159. package/docs/split-required-recovery.md +19 -25
  160. package/docs/stable-release-defect-register.md +465 -3
  161. package/docs/updating-existing-project.md +59 -4
  162. package/docs/validation-report.md +124 -56
  163. package/package.json +9 -1
  164. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
  165. package/plugins/codex-workflow-gateway/references/protocol.md +92 -4
  166. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
  167. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +263 -4
  168. package/references/state-machine.md +13 -6
  169. package/roles/delivery-coordinator.md +39 -0
  170. package/schemas/adoption-posture-event.schema.json +1 -1
  171. package/schemas/authorization-event.schema.json +64 -1
  172. package/schemas/context-envelope.schema.json +1 -1
  173. package/schemas/corrective-decision-event.schema.json +48 -3
  174. package/schemas/corrective-replan-credentials.private.schema.json +98 -0
  175. package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
  176. package/schemas/corrective-replan-public.schema.json +142 -0
  177. package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
  178. package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
  179. package/schemas/milestone-scope-change-event.schema.json +6 -1
  180. package/schemas/milestone.schema.json +6 -1
  181. package/schemas/remediation-event.schema.json +11 -1
  182. package/schemas/task-handoff-event.schema.json +19 -3
  183. package/schemas/task.schema.json +24 -1
  184. package/schemas/transition-definition.schema.json +2609 -0
  185. package/schemas/transition-payloads.schema.json +140 -0
  186. package/scripts/generate-pdf-docs.py +512 -0
  187. package/scripts/run-pdf-docs.mjs +62 -0
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
5
+ import { inspectDependencyProvenanceRecovery } from './dependency-provenance.js';
5
6
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
6
7
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
7
8
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
@@ -9,24 +10,34 @@ import { pathAllowed, validatePlan } from './domain/validation.js';
9
10
  import { WorkflowError } from './errors.js';
10
11
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
11
12
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
12
- import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
+ import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
14
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
14
15
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
15
- import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
- import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
16
+ import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
17
+ import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
18
+ import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
19
+ import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
20
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
18
- import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
21
+ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
19
22
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
20
23
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
21
24
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
22
25
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
26
+ import { ensureDirectory } from './fs-utils.js';
23
27
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
24
28
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
25
- import { launchStrictStepReviewer } from './reviewer.js';
26
- import { WriterLockManager } from './state/lock.js';
29
+ import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
30
+ import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
27
31
  import { FileStateStore } from './state/store.js';
32
+ import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
33
+ import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
34
+ import { createCorrectiveYieldExecutor } from './state/corrective-yield-executor.js';
35
+ import { createCorrectiveReplanPublicController, } from './state/corrective-replan-public.js';
36
+ import { inspectWorkflowObservationBoundary, } from './observation.js';
28
37
  import { createEntityId } from './ulid.js';
29
- import { PACKAGE_VERSION } from './version.js';
38
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
39
+ import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
40
+ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
30
41
  export class WorkflowService {
31
42
  store;
32
43
  now;
@@ -34,13 +45,42 @@ export class WorkflowService {
34
45
  this.store = store;
35
46
  this.now = now;
36
47
  }
37
- context(repository) {
48
+ #mutationContext(repository) {
38
49
  const identity = resolveRepositoryIdentity(repository);
39
50
  const projectRoot = this.store.projectRoot(identity.projectId);
51
+ const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
52
+ const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
53
+ const taskTransactions = {
54
+ corrupt: [...replanTransactions.corrupt, ...yieldTransactions.corrupt],
55
+ pending: [...replanTransactions.pending, ...yieldTransactions.pending],
56
+ };
57
+ if (taskTransactions.corrupt.length > 0) {
58
+ throw new WorkflowError('STATE_CORRUPT', 'A Task transaction journal failed integrity validation.', {
59
+ transactions: taskTransactions.corrupt,
60
+ });
61
+ }
62
+ if (taskTransactions.pending.length > 0) {
63
+ throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'A Task transaction requires explicit recovery.', {
64
+ taskIds: [...new Set(taskTransactions.pending.map((transaction) => transaction.taskId))].sort(),
65
+ transactionIds: taskTransactions.pending.map((transaction) => transaction.transactionId).sort(),
66
+ });
67
+ }
68
+ const coreTaskOperations = listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root);
69
+ if (coreTaskOperations.length > 0) {
70
+ throw new WorkflowError('LOCKED', 'A Core Task operation owns the project mutation boundary.', {
71
+ taskIds: coreTaskOperations.map((operation) => operation.entityId).sort(),
72
+ operationIds: coreTaskOperations.map((operation) => operation.operationId).sort(),
73
+ });
74
+ }
40
75
  recoverProjectTransactions(projectRoot, identity.projectId);
41
76
  const projectFile = path.join(projectRoot, 'project.json');
42
77
  const projectAlreadyRegistered = existsSync(projectFile);
43
78
  if (!projectAlreadyRegistered) {
79
+ // The registration posture is a sidecar under the project root. A completely
80
+ // fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
81
+ // only that directory before writing the posture; project.json remains the
82
+ // final registration record written below.
83
+ ensureDirectory(projectRoot);
44
84
  initializeProjectRegistrationAdoption(this.store, {
45
85
  projectId: identity.projectId,
46
86
  repositoryIdentity: identity,
@@ -54,17 +94,103 @@ export class WorkflowService {
54
94
  return {
55
95
  identity,
56
96
  store: this.store,
57
- locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now),
97
+ locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root),
98
+ };
99
+ }
100
+ inspectWriterLease(repository, entityId) {
101
+ const identity = resolveRepositoryIdentity(repository);
102
+ const lease = inspectRawWriterLease(this.store.lockRoot(identity.projectId), entityId, this.store.root);
103
+ return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
104
+ }
105
+ repairStaleWriterLease(repository, entityId) {
106
+ const context = this.#mutationContext(repository);
107
+ const lease = context.locks.repairStale(entityId);
108
+ return {
109
+ ...this.publicWriterLeaseDiagnostic(lease),
110
+ repaired: true,
111
+ };
112
+ }
113
+ prepareCorrectiveReplan(repository, taskId, candidate) {
114
+ return this.correctiveReplanController(repository).prepare({ taskId, candidate });
115
+ }
116
+ confirmCorrectiveReplan(repository, taskId, candidate, gate, confirmationCode) {
117
+ return this.correctiveReplanController(repository).confirm({ taskId, candidate, gate, confirmationCode });
118
+ }
119
+ executeCorrectiveReplan(repository, taskId, candidate, credential) {
120
+ return this.correctiveReplanController(repository).execute({ taskId, candidate, credential });
121
+ }
122
+ recoverCorrectiveReplan(repository, taskId, transactionId) {
123
+ return this.correctiveReplanController(repository).recover({ taskId, transactionId });
124
+ }
125
+ executeCorrectiveYield(repository, taskId, expectedTaskRevision, actor, writerToken, reason) {
126
+ const identity = resolveRepositoryIdentity(repository);
127
+ return createCorrectiveYieldExecutor({
128
+ store: this.store,
129
+ projectId: identity.projectId,
130
+ now: this.now,
131
+ }).execute({
132
+ projectId: identity.projectId,
133
+ taskId,
134
+ expectedTaskRevision,
135
+ actor,
136
+ writerToken,
137
+ reason,
138
+ });
139
+ }
140
+ recoverCorrectiveYield(repository, taskId, transactionId) {
141
+ const identity = resolveRepositoryIdentity(repository);
142
+ return createCorrectiveYieldExecutor({
143
+ store: this.store,
144
+ projectId: identity.projectId,
145
+ now: this.now,
146
+ }).recover({ taskId, transactionId });
147
+ }
148
+ correctiveReplanController(repository) {
149
+ const identity = resolveRepositoryIdentity(repository);
150
+ return createCorrectiveReplanPublicController({
151
+ store: this.store,
152
+ projectId: identity.projectId,
153
+ repositoryRoot: identity.repositoryRoot,
154
+ now: this.now,
155
+ });
156
+ }
157
+ observationSnapshot(repository) {
158
+ const identity = resolveRepositoryIdentity(repository);
159
+ const boundary = inspectWorkflowObservationBoundary({
160
+ store: this.store,
161
+ identity,
162
+ now: this.now(),
163
+ });
164
+ if (boundary.observation.kind !== 'assessment') {
165
+ return {
166
+ ...boundary,
167
+ discoveries: [],
168
+ tasks: [],
169
+ milestones: [],
170
+ delegations: [],
171
+ adoption: null,
172
+ };
173
+ }
174
+ const tasks = this.store.listTasks(identity.projectId);
175
+ const rawMilestones = this.store.listMilestones(identity.projectId);
176
+ const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones, boundary.leases, boundary.currentAdoptionPosture);
177
+ return {
178
+ ...boundary,
179
+ discoveries: this.store.listDiscoveries(identity.projectId),
180
+ tasks,
181
+ milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
182
+ delegations: this.store.listDelegations(identity.projectId),
183
+ adoption,
58
184
  };
59
185
  }
60
186
  prepareDelegation(repository, input) {
61
- const { identity } = this.context(repository);
187
+ const { identity } = this.#mutationContext(repository);
62
188
  const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
63
189
  this.assertDelegationScopeExists(identity.projectId, gate.policy);
64
190
  return gate;
65
191
  }
66
192
  grantDelegation(repository, input, confirmationCode) {
67
- const { identity } = this.context(repository);
193
+ const { identity } = this.#mutationContext(repository);
68
194
  const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
69
195
  this.assertDelegationScopeExists(identity.projectId, gate.policy);
70
196
  if (confirmationCode.trim() !== gate.confirmationCode) {
@@ -100,7 +226,7 @@ export class WorkflowService {
100
226
  }, null);
101
227
  }
102
228
  revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
103
- const { identity } = this.context(repository);
229
+ const { identity } = this.#mutationContext(repository);
104
230
  const grant = this.store.readDelegation(identity.projectId, grantId);
105
231
  assertExpectedRevision(grant, expectedRevision);
106
232
  if (grant.status !== 'active') {
@@ -124,7 +250,7 @@ export class WorkflowService {
124
250
  startDiscovery(repository, input) {
125
251
  if (!input.goal.trim())
126
252
  throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
127
- const { identity } = this.context(repository);
253
+ const { identity } = this.#mutationContext(repository);
128
254
  const timestamp = this.now().toISOString();
129
255
  const initial = {
130
256
  schemaVersion: STATE_SCHEMA_VERSION,
@@ -152,7 +278,7 @@ export class WorkflowService {
152
278
  return this.store.writeDiscovery(initial, null);
153
279
  }
154
280
  updateDiscovery(repository, discoveryId, expectedRevision, patch) {
155
- const { identity } = this.context(repository);
281
+ const { identity } = this.#mutationContext(repository);
156
282
  const current = this.store.readDiscovery(identity.projectId, discoveryId);
157
283
  if (current.status === 'materialized' || current.status === 'abandoned') {
158
284
  throw new WorkflowError('TRANSITION_BLOCKED', `Discovery ${discoveryId} is ${current.status}.`);
@@ -176,7 +302,7 @@ export class WorkflowService {
176
302
  return this.store.writeDiscovery(updated, expectedRevision);
177
303
  }
178
304
  materializeTask(repository, discoveryId, expectedRevision, title, baseBranch = 'main', milestoneId = null) {
179
- const { identity } = this.context(repository);
305
+ const { identity } = this.#mutationContext(repository);
180
306
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
181
307
  assertExpectedRevision(discovery, expectedRevision);
182
308
  const readiness = assessDiscovery(discovery);
@@ -253,7 +379,7 @@ export class WorkflowService {
253
379
  return { discovery: savedDiscovery, task: savedTask, taskRoot };
254
380
  }
255
381
  replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
256
- const { identity, locks } = this.context(repository);
382
+ const { identity, locks } = this.#mutationContext(repository);
257
383
  const oldTask = this.store.readTask(identity.projectId, oldTaskId);
258
384
  assertExpectedRevision(oldTask, expectedOldTaskRevision);
259
385
  if (!oldTask.milestoneId) {
@@ -270,6 +396,32 @@ export class WorkflowService {
270
396
  disposition: membership?.disposition ?? null,
271
397
  });
272
398
  }
399
+ const incidentDependencies = milestone.memberships.flatMap((candidate) => {
400
+ if (!Array.isArray(candidate.dependsOnTaskIds))
401
+ return [];
402
+ return [
403
+ ...(candidate.taskId === oldTask.id
404
+ ? candidate.dependsOnTaskIds.map((predecessorTaskId) => ({
405
+ predecessorTaskId,
406
+ consumerTaskId: candidate.taskId,
407
+ }))
408
+ : []),
409
+ ...(candidate.dependsOnTaskIds.includes(oldTask.id)
410
+ ? [{ predecessorTaskId: oldTask.id, consumerTaskId: candidate.taskId }]
411
+ : []),
412
+ ];
413
+ });
414
+ if (structuralReplacementIsDisabled()) {
415
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Structural replacement is disabled until a topology-preserving replacement transaction is implemented.', {
416
+ diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
417
+ milestoneId: milestone.id,
418
+ oldTaskId: oldTask.id,
419
+ incidentDependencies,
420
+ requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
421
+ });
422
+ }
423
+ /* P04-B retains the existing one-for-one transaction below as implementation evidence,
424
+ but this route must remain unreachable until dependency rewiring is specified. */
273
425
  const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
274
426
  if (!riskAudit)
275
427
  throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
@@ -379,9 +531,19 @@ export class WorkflowService {
379
531
  checks: milestone.checks,
380
532
  memberships: [
381
533
  ...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
382
- ? { ...candidate, disposition: 'cancelled', reason: replacementReason }
383
- : candidate),
384
- { taskId: replacementTaskId, disposition: 'required', reason: replacementReason },
534
+ ? {
535
+ ...candidate,
536
+ disposition: 'cancelled',
537
+ reason: replacementReason,
538
+ dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])],
539
+ }
540
+ : { ...candidate, dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])] }),
541
+ {
542
+ taskId: replacementTaskId,
543
+ disposition: 'required',
544
+ reason: replacementReason,
545
+ dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])],
546
+ },
385
547
  ],
386
548
  };
387
549
  if (autonomyContractEventHash) {
@@ -504,7 +666,7 @@ export class WorkflowService {
504
666
  };
505
667
  }
506
668
  materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
507
- const { identity } = this.context(repository);
669
+ const { identity } = this.#mutationContext(repository);
508
670
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
509
671
  assertExpectedRevision(discovery, expectedRevision);
510
672
  const readiness = assessDiscovery(discovery);
@@ -552,7 +714,7 @@ export class WorkflowService {
552
714
  };
553
715
  }
554
716
  setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
555
- const { identity } = this.context(repository);
717
+ const { identity } = this.#mutationContext(repository);
556
718
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
557
719
  assertExpectedRevision(milestone, expectedRevision);
558
720
  if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
@@ -590,41 +752,18 @@ export class WorkflowService {
590
752
  throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
591
753
  }
592
754
  }
593
- const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
594
- ? { ...authorization, decision: 'superseded' }
595
- : authorization);
596
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
597
- const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify({
598
- outcome: normalized.outcome,
599
- successSignal: normalized.successSignal,
600
- acceptance: normalized.acceptance,
601
- memberships: normalized.memberships,
602
- checks: normalized.checks,
603
- }, null, 2));
604
- const candidate = {
605
- ...milestone,
606
- status: 'awaiting_execution_authorization',
607
- outcome: normalized.outcome,
608
- successSignal: normalized.successSignal,
609
- acceptance: normalized.acceptance,
610
- taskIds,
611
- memberships: normalized.memberships,
612
- checks: normalized.checks,
613
- membershipRevision: milestone.membershipRevision + 1,
614
- authorizations,
615
- planHash: stagedPlan.hash,
616
- resultHash: null,
617
- evidenceHash: null,
618
- acceptedHead: null,
619
- cancellationReason: null,
620
- };
621
- assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
622
- const saved = this.store.writeMilestone(candidate, expectedRevision);
623
- this.store.publishArtifact(stagedPlan);
624
- return saved;
755
+ return applyInitialMilestonePlanTransaction({
756
+ store: this.store,
757
+ milestone,
758
+ expectedRevision,
759
+ normalizedPlan: normalized,
760
+ now: this.now(),
761
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
762
+ listTasks: () => this.store.listTasks(identity.projectId),
763
+ });
625
764
  }
626
765
  prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
627
- const { identity } = this.context(repository);
766
+ const { identity } = this.#mutationContext(repository);
628
767
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
629
768
  assertExpectedRevision(milestone, expectedRevision);
630
769
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -641,7 +780,7 @@ export class WorkflowService {
641
780
  };
642
781
  }
643
782
  applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
644
- const { identity } = this.context(repository);
783
+ const { identity } = this.#mutationContext(repository);
645
784
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
646
785
  assertExpectedRevision(milestone, expectedRevision);
647
786
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -664,9 +803,10 @@ export class WorkflowService {
664
803
  });
665
804
  }
666
805
  prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
667
- const { identity } = this.context(repository);
806
+ const { identity } = this.#mutationContext(repository);
668
807
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
669
808
  assertExpectedRevision(milestone, expectedRevision);
809
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
670
810
  return prepareMilestoneAutonomyContract({
671
811
  projectId: identity.projectId,
672
812
  milestone,
@@ -677,9 +817,10 @@ export class WorkflowService {
677
817
  });
678
818
  }
679
819
  grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
680
- const { identity } = this.context(repository);
820
+ const { identity } = this.#mutationContext(repository);
681
821
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
682
822
  assertExpectedRevision(milestone, expectedRevision);
823
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
683
824
  return applyMilestoneAutonomyContract({
684
825
  store: this.store,
685
826
  projectId: identity.projectId,
@@ -692,7 +833,7 @@ export class WorkflowService {
692
833
  });
693
834
  }
694
835
  evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
695
- const { identity } = this.context(repository);
836
+ const { identity } = this.#mutationContext(repository);
696
837
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
697
838
  assertExpectedRevision(milestone, expectedRevision);
698
839
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -722,12 +863,14 @@ export class WorkflowService {
722
863
  });
723
864
  }
724
865
  authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
725
- const { identity } = this.context(repository);
866
+ const { identity } = this.#mutationContext(repository);
726
867
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
727
868
  assertExpectedRevision(milestone, expectedRevision);
728
869
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
729
870
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
730
871
  }
872
+ assertMilestoneDependenciesDeclared(milestone);
873
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
731
874
  const root = this.store.milestoneRoot(identity.projectId, milestoneId);
732
875
  const planHash = this.store.hashArtifact(root, 'plan.json');
733
876
  if (planHash !== milestone.planHash) {
@@ -751,7 +894,7 @@ export class WorkflowService {
751
894
  return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
752
895
  }
753
896
  validateMilestone(repository, milestoneId, expectedRevision) {
754
- const { identity } = this.context(repository);
897
+ const { identity } = this.#mutationContext(repository);
755
898
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
756
899
  assertExpectedRevision(milestone, expectedRevision);
757
900
  if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
@@ -814,7 +957,7 @@ export class WorkflowService {
814
957
  return saved;
815
958
  }
816
959
  acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
817
- const { identity } = this.context(repository);
960
+ const { identity } = this.#mutationContext(repository);
818
961
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
819
962
  assertExpectedRevision(milestone, expectedRevision);
820
963
  if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
@@ -853,7 +996,7 @@ export class WorkflowService {
853
996
  resultHash,
854
997
  evidenceHash,
855
998
  headCommit: currentHead,
856
- reason: `Confirmed with ${gate.confirmationCode}.`,
999
+ reason: 'Milestone final acceptance confirmed against the current validated state.',
857
1000
  authorizationMode: delegation ? 'delegated' : 'human',
858
1001
  ...(delegation ? { delegation } : {}),
859
1002
  };
@@ -869,7 +1012,7 @@ export class WorkflowService {
869
1012
  cancelMilestone(repository, milestoneId, expectedRevision, reason) {
870
1013
  if (!reason.trim())
871
1014
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
872
- const { identity } = this.context(repository);
1015
+ const { identity } = this.#mutationContext(repository);
873
1016
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
874
1017
  assertExpectedRevision(milestone, expectedRevision);
875
1018
  if (['accepted', 'cancelled'].includes(milestone.status)) {
@@ -879,21 +1022,59 @@ export class WorkflowService {
879
1022
  }
880
1023
  setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
881
1024
  validatePlan(plan);
882
- const { identity } = this.context(repository);
1025
+ const { identity } = this.#mutationContext(repository);
883
1026
  const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
884
1027
  this.validateKnowledgeTargets(knowledgeMap, plan);
885
1028
  const task = this.store.readTask(identity.projectId, taskId);
886
1029
  assertExpectedRevision(task, expectedRevision);
887
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
888
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
889
- }
1030
+ const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
890
1031
  const currentPlanCorrectiveDecisions = task.planHash
891
- ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
1032
+ ? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
892
1033
  : [];
893
1034
  const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
894
- const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
1035
+ const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => {
1036
+ if (event.decision !== 'continue-fix')
1037
+ return false;
1038
+ const passedBoundary = latestPassedVerifiedStepReviewEvent(this.store, task, event.stepId);
1039
+ return !passedBoundary || passedBoundary.recordedAt <= event.recordedAt;
1040
+ });
895
1041
  const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
896
- const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
1042
+ const currentPlanRiskAudit = task.status === 'needs_fix' || currentReplanDecisions.length > 0
1043
+ ? readCurrentPlanRiskAudit(this.store, task)
1044
+ : null;
1045
+ const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
1046
+ ? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
1047
+ : null;
1048
+ const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
1049
+ ? classifiedCorrectivePosture
1050
+ : currentPlanRiskAudit
1051
+ ? identifyCorrectiveReplanPosture(task, currentPlanRiskAudit.reviewRequiredStepIds, correctiveDecisionEvents, readRemediationEvents(this.store, task))
1052
+ : null;
1053
+ const correctiveReplanAvailability = correctiveReplanPosture?.posture === 'replan-required'
1054
+ ? evaluateTaskCorrectiveReplanAvailability(task, correctiveReplanPosture)
1055
+ : null;
1056
+ if (currentReplanDecisions.length > 0) {
1057
+ if (!correctiveReplanAvailability?.allowed) {
1058
+ throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} is blocked for Task ${task.id}.`, {
1059
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1060
+ posture: correctiveReplanPosture?.posture ?? null,
1061
+ evaluationPhase: correctiveReplanAvailability?.phase ?? 'availability',
1062
+ blockingGuardIds: correctiveReplanAvailability?.blockingGuardIds ?? [],
1063
+ pendingGuardIds: correctiveReplanAvailability?.pendingGuardIds ?? [],
1064
+ guardResults: correctiveReplanAvailability?.guardResults ?? [],
1065
+ });
1066
+ }
1067
+ throw new WorkflowError('TRANSITION_BLOCKED', `Legacy task plan-set cannot execute ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID}.`, {
1068
+ taskId: task.id,
1069
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1070
+ requiredAction: readTaskC1Posture(this.store, task).state === 'claimed'
1071
+ ? 'Run task corrective-yield first, then use task corrective-replan-prepare.'
1072
+ : 'Use task corrective-replan-prepare, confirm, and execute.',
1073
+ });
1074
+ }
1075
+ else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
1076
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
1077
+ }
897
1078
  if (currentHardBlockingDecisions.length > 0) {
898
1079
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
899
1080
  taskId: task.id,
@@ -923,9 +1104,6 @@ export class WorkflowService {
923
1104
  })),
924
1105
  });
925
1106
  }
926
- const currentPlanRiskAudit = task.status === 'needs_fix'
927
- ? readCurrentPlanRiskAudit(this.store, task)
928
- : null;
929
1107
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
930
1108
  if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
931
1109
  validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
@@ -968,7 +1146,7 @@ export class WorkflowService {
968
1146
  }
969
1147
  }
970
1148
  const root = this.store.taskRoot(identity.projectId, taskId);
971
- const stagedPlan = this.store.stageArtifact(root, 'plan.md', renderPlan(task, plan));
1149
+ const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
972
1150
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
973
1151
  ? { ...authorization, decision: 'superseded' }
974
1152
  : authorization);
@@ -985,12 +1163,17 @@ export class WorkflowService {
985
1163
  plannedSteps.every((step) => step.status === 'completed')) {
986
1164
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
987
1165
  }
988
- if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
989
- throw new WorkflowError('TRANSITION_BLOCKED', `Replacement Plan under replan-required must retain every guarded Step targeted by the current corrective decision; removing or renaming one would reset remediation history.`, {
990
- taskId: task.id,
991
- requiredStepIds: currentReplanRequiredStepIds,
992
- plannedStepIds: plannedSteps.map((step) => step.id),
993
- });
1166
+ if (correctiveReplanPosture?.posture === 'replan-required') {
1167
+ const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
1168
+ if (!executionEvaluation.allowed) {
1169
+ throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
1170
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1171
+ evaluationPhase: executionEvaluation.phase,
1172
+ blockingGuardIds: executionEvaluation.blockingGuardIds,
1173
+ pendingGuardIds: executionEvaluation.pendingGuardIds,
1174
+ guardResults: executionEvaluation.guardResults,
1175
+ });
1176
+ }
994
1177
  }
995
1178
  if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
996
1179
  throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
@@ -998,14 +1181,10 @@ export class WorkflowService {
998
1181
  stepId: guardedRemediationGate.stepId,
999
1182
  });
1000
1183
  }
1001
- if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
1002
- assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
1003
- appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
1004
- }
1005
1184
  const candidate = {
1006
1185
  ...task,
1007
1186
  status: 'awaiting_execution_authorization',
1008
- planHash: stagedPlan.hash,
1187
+ planHash: preparedPlan.planHash,
1009
1188
  planObjective: plan.objective,
1010
1189
  requirements: plan.requirements,
1011
1190
  acceptance: plan.acceptance,
@@ -1025,6 +1204,18 @@ export class WorkflowService {
1025
1204
  };
1026
1205
  if (planRiskAudit)
1027
1206
  validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
1207
+ const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
1208
+ if (stagedPlan.hash !== preparedPlan.planHash) {
1209
+ throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
1210
+ taskId: task.id,
1211
+ preparedPlanHash: preparedPlan.planHash,
1212
+ stagedPlanHash: stagedPlan.hash,
1213
+ });
1214
+ }
1215
+ if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
1216
+ assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
1217
+ appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, preparedPlan.planHash, this.now());
1218
+ }
1028
1219
  const saved = this.store.writeTask(candidate, expectedRevision);
1029
1220
  this.store.publishArtifact(stagedPlan);
1030
1221
  if (planRiskAudit)
@@ -1035,7 +1226,7 @@ export class WorkflowService {
1035
1226
  return saved;
1036
1227
  }
1037
1228
  rebindTaskKnowledge(repository, taskId, expectedRevision) {
1038
- const { identity } = this.context(repository);
1229
+ const { identity } = this.#mutationContext(repository);
1039
1230
  const task = this.store.readTask(identity.projectId, taskId);
1040
1231
  assertExpectedRevision(task, expectedRevision);
1041
1232
  this.assertTaskContextChangeAllowed(task);
@@ -1120,7 +1311,7 @@ export class WorkflowService {
1120
1311
  if (!delegationGrantId) {
1121
1312
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
1122
1313
  }
1123
- const { identity } = this.context(repository);
1314
+ const { identity } = this.#mutationContext(repository);
1124
1315
  const task = this.store.readTask(identity.projectId, taskId);
1125
1316
  const currentMap = this.store.readKnowledgeMap(identity.projectId);
1126
1317
  assertExpectedRevision(task, expectedTaskRevision);
@@ -1211,7 +1402,7 @@ export class WorkflowService {
1211
1402
  });
1212
1403
  }
1213
1404
  authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
1214
- const { identity } = this.context(repository);
1405
+ const { identity } = this.#mutationContext(repository);
1215
1406
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1216
1407
  const task = this.store.readTask(identity.projectId, taskId);
1217
1408
  assertExpectedRevision(task, expectedRevision);
@@ -1222,7 +1413,24 @@ export class WorkflowService {
1222
1413
  const root = this.store.taskRoot(identity.projectId, taskId);
1223
1414
  const briefHash = this.store.hashArtifact(root, 'brief.md');
1224
1415
  const planHash = this.store.hashArtifact(root, 'plan.md');
1416
+ if (task.briefHash !== briefHash || task.planHash !== planHash) {
1417
+ throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
1418
+ taskId: task.id,
1419
+ stateBriefHash: task.briefHash,
1420
+ artifactBriefHash: briefHash,
1421
+ statePlanHash: task.planHash,
1422
+ artifactPlanHash: planHash,
1423
+ });
1424
+ }
1225
1425
  readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
1426
+ const mechanicalFeasibility = evaluateMechanicalFeasibility(identity.repositoryRoot, task, briefHash, planHash);
1427
+ if (mechanicalFeasibility.decision === 'blocked') {
1428
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task Plan contains a proven mechanical contradiction.', {
1429
+ diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
1430
+ mechanicalFeasibility,
1431
+ requiredAction: 'task plan-set',
1432
+ });
1433
+ }
1226
1434
  const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
1227
1435
  const event = {
1228
1436
  kind: 'execution',
@@ -1232,16 +1440,17 @@ export class WorkflowService {
1232
1440
  briefHash,
1233
1441
  planHash,
1234
1442
  resultHash: null,
1235
- evidenceHash: null,
1236
- headCommit: null,
1443
+ evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
1444
+ headCommit: mechanicalFeasibility.evaluatedHead,
1237
1445
  reason,
1238
1446
  authorizationMode: delegation ? 'delegated' : 'human',
1239
1447
  ...(delegation ? { delegation } : {}),
1448
+ mechanicalFeasibility,
1240
1449
  };
1241
1450
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
1242
1451
  }
1243
1452
  recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
1244
- const { identity } = this.context(repository);
1453
+ const { identity } = this.#mutationContext(repository);
1245
1454
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1246
1455
  const task = this.store.readTask(identity.projectId, taskId);
1247
1456
  assertExpectedRevision(task, expectedRevision);
@@ -1249,7 +1458,7 @@ export class WorkflowService {
1249
1458
  return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
1250
1459
  }
1251
1460
  showTaskHandoff(repository, taskId) {
1252
- const { identity } = this.context(repository);
1461
+ const { identity } = this.#mutationContext(repository);
1253
1462
  const task = this.store.readTask(identity.projectId, taskId);
1254
1463
  return {
1255
1464
  task,
@@ -1257,8 +1466,8 @@ export class WorkflowService {
1257
1466
  claimTokenAvailable: false,
1258
1467
  };
1259
1468
  }
1260
- handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
1261
- const context = this.context(repository);
1469
+ handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
1470
+ const context = this.#mutationContext(repository);
1262
1471
  const task = this.store.readTask(context.identity.projectId, taskId);
1263
1472
  assertExpectedRevision(task, expectedRevision);
1264
1473
  const normalizedActor = actor.trim();
@@ -1268,6 +1477,9 @@ export class WorkflowService {
1268
1477
  if (grantId)
1269
1478
  this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
1270
1479
  const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
1480
+ const dependencyBinding = milestone
1481
+ ? this.currentMilestoneDependencyBinding(milestone, task.id)
1482
+ : null;
1271
1483
  const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
1272
1484
  const milestoneDisplayNumber = milestone
1273
1485
  ? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
@@ -1313,9 +1525,11 @@ export class WorkflowService {
1313
1525
  writerToken: lease.token,
1314
1526
  milestoneDisplayNumber,
1315
1527
  taskDisplayNumber,
1528
+ dependencyBinding,
1316
1529
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1317
1530
  grantId,
1318
1531
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
1532
+ replacePending,
1319
1533
  }, this.now());
1320
1534
  releaseLease();
1321
1535
  return { task, event: prepared.event, bundle: prepared.bundle };
@@ -1327,15 +1541,19 @@ export class WorkflowService {
1327
1541
  }
1328
1542
  }
1329
1543
  claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
1330
- const context = this.context(repository);
1544
+ const context = this.#mutationContext(repository);
1331
1545
  const task = this.store.readTask(context.identity.projectId, taskId);
1332
1546
  assertExpectedRevision(task, expectedRevision);
1333
1547
  const normalizedActor = actor.trim();
1334
1548
  if (!normalizedActor) {
1335
1549
  throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
1336
1550
  }
1337
- const pendingHandoff = readTaskC1Posture(this.store, task);
1338
- if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
1551
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1552
+ const dependencyBinding = task.milestoneId
1553
+ ? this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id)
1554
+ : null;
1555
+ const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now(), dependencyBinding);
1556
+ if (pendingHandoff.latestEvent.grantId) {
1339
1557
  this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
1340
1558
  }
1341
1559
  const existingLease = context.locks.inspect(taskId);
@@ -1367,8 +1585,15 @@ export class WorkflowService {
1367
1585
  lease = context.locks.acquire(taskId, normalizedActor);
1368
1586
  }
1369
1587
  try {
1370
- const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1371
- return { task, event, lease };
1588
+ const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now(), dependencyBinding);
1589
+ return {
1590
+ task,
1591
+ event,
1592
+ lease: toPublicWriterLease(lease, this.now()),
1593
+ writerCredential: acquiredHere
1594
+ ? { disposition: 'issued', token: lease.token }
1595
+ : { disposition: 'renewed' },
1596
+ };
1372
1597
  }
1373
1598
  catch (error) {
1374
1599
  if (acquiredHere)
@@ -1376,8 +1601,61 @@ export class WorkflowService {
1376
1601
  throw error;
1377
1602
  }
1378
1603
  }
1604
+ replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
1605
+ const context = this.#mutationContext(repository);
1606
+ const task = this.store.readTask(context.identity.projectId, taskId);
1607
+ assertExpectedRevision(task, expectedRevision);
1608
+ const normalizedActor = actor.trim();
1609
+ if (!normalizedActor)
1610
+ throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
1611
+ const posture = readTaskC1Posture(this.store, task);
1612
+ if (posture.state === 'claimed' && task.milestoneId) {
1613
+ const dependencyBinding = this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id);
1614
+ const claimedBinding = posture.latestEvent.dependencyBinding ?? null;
1615
+ if (claimedBinding === null
1616
+ || claimedBinding.milestoneRevision !== dependencyBinding.milestoneRevision
1617
+ || claimedBinding.membershipRevision !== dependencyBinding.membershipRevision
1618
+ || claimedBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
1619
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Writer credential replacement requires a current Milestone dependency binding.', {
1620
+ diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
1621
+ taskId: task.id,
1622
+ milestoneId: task.milestoneId,
1623
+ claimDependencyBinding: claimedBinding,
1624
+ currentDependencyBinding: dependencyBinding,
1625
+ });
1626
+ }
1627
+ }
1628
+ const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1629
+ const action = String(navigation.action);
1630
+ const currentLease = context.locks.inspect(taskId);
1631
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
1632
+ const requiredActor = writerTokenContract
1633
+ ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1634
+ : null;
1635
+ if (!requiredActor || requiredActor !== normalizedActor) {
1636
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
1637
+ taskId,
1638
+ posture: posture.state,
1639
+ claimant: posture.state === 'claimed' ? posture.claimant : null,
1640
+ actor: normalizedActor,
1641
+ requiredActor,
1642
+ nextAction: action,
1643
+ });
1644
+ }
1645
+ const lease = currentLease
1646
+ ? context.locks.replaceCredential(taskId, normalizedActor)
1647
+ : context.locks.acquire(taskId, normalizedActor);
1648
+ if (posture.state === 'claimed') {
1649
+ appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1650
+ }
1651
+ return {
1652
+ task,
1653
+ lease: toPublicWriterLease(lease, this.now()),
1654
+ writerCredential: { disposition: 'issued', token: lease.token },
1655
+ };
1656
+ }
1379
1657
  handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1380
- const context = this.context(repository);
1658
+ const context = this.#mutationContext(repository);
1381
1659
  const task = this.store.readTask(context.identity.projectId, taskId);
1382
1660
  assertExpectedRevision(task, expectedRevision);
1383
1661
  const lease = task.status === 'merged' || task.status === 'cancelled'
@@ -1389,23 +1667,16 @@ export class WorkflowService {
1389
1667
  return { task, event };
1390
1668
  }
1391
1669
  startTask(repository, taskId, expectedRevision, workspaceOwner) {
1392
- const { identity } = this.context(repository);
1670
+ const { identity } = this.#mutationContext(repository);
1393
1671
  const task = this.store.readTask(identity.projectId, taskId);
1394
1672
  assertExpectedRevision(task, expectedRevision);
1395
1673
  this.requireExecutionAdoptionPosture(identity, taskId);
1396
1674
  if (task.status !== 'ready')
1397
1675
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
1398
- this.assertExecutionAuthorizationFresh(task);
1676
+ const executionAuthorization = this.assertExecutionAuthorizationFresh(task, identity.repositoryRoot);
1399
1677
  if (task.milestoneId) {
1400
1678
  const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
1401
- const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
1402
- if (milestone.status !== 'active' || membership?.disposition !== 'required') {
1403
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
1404
- milestoneId: milestone.id,
1405
- milestoneStatus: milestone.status,
1406
- disposition: membership?.disposition ?? null,
1407
- });
1408
- }
1679
+ const dependencyBinding = this.currentMilestoneDependencyBinding(milestone, task.id);
1409
1680
  const handoffPosture = readTaskC1Posture(this.store, task);
1410
1681
  if (handoffPosture.state !== 'claimed') {
1411
1682
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
@@ -1415,6 +1686,20 @@ export class WorkflowService {
1415
1686
  requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
1416
1687
  });
1417
1688
  }
1689
+ const claimBinding = handoffPosture.latestEvent.dependencyBinding ?? null;
1690
+ if (claimBinding === null
1691
+ || claimBinding.milestoneRevision !== dependencyBinding.milestoneRevision
1692
+ || claimBinding.membershipRevision !== dependencyBinding.membershipRevision
1693
+ || claimBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
1694
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a current dependency-bound C1 claim.', {
1695
+ diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
1696
+ taskId: task.id,
1697
+ milestoneId: milestone.id,
1698
+ claimDependencyBinding: handoffPosture.latestEvent.dependencyBinding ?? null,
1699
+ currentDependencyBinding: dependencyBinding,
1700
+ requiredAction: 'Prepare and claim a new C1 handoff from the current Milestone Plan.',
1701
+ });
1702
+ }
1418
1703
  }
1419
1704
  if (!isClean(identity.repositoryRoot)) {
1420
1705
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
@@ -1424,12 +1709,22 @@ export class WorkflowService {
1424
1709
  const branch = `codex/task-${task.id.slice(5, 15).toLowerCase()}-${task.slug}`;
1425
1710
  let baseCommit;
1426
1711
  let taskBranch;
1712
+ const authorizedBaseCommit = executionAuthorization.mechanicalFeasibility?.sourceKind === 'pre-start-base'
1713
+ ? executionAuthorization.mechanicalFeasibility.evaluatedHead
1714
+ : null;
1427
1715
  if (workspaceOwner === 'local') {
1428
- baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch);
1716
+ baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch, authorizedBaseCommit);
1429
1717
  taskBranch = branch;
1430
1718
  }
1431
1719
  else {
1432
1720
  baseCommit = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
1721
+ if (authorizedBaseCommit !== null && baseCommit !== authorizedBaseCommit) {
1722
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
1723
+ baseBranch: task.baseBranch,
1724
+ authorizedBaseCommit,
1725
+ currentBaseCommit: baseCommit,
1726
+ });
1727
+ }
1433
1728
  taskBranch = currentBranch(identity.repositoryRoot);
1434
1729
  if (!taskBranch || taskBranch === task.baseBranch) {
1435
1730
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `Externally owned execution requires a dedicated branch different from ${task.baseBranch}.`);
@@ -1441,14 +1736,14 @@ export class WorkflowService {
1441
1736
  return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
1442
1737
  }
1443
1738
  runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1444
- const context = this.context(repository);
1739
+ const context = this.#mutationContext(repository);
1445
1740
  const task = this.store.readTask(context.identity.projectId, taskId);
1446
1741
  assertExpectedRevision(task, expectedRevision);
1447
1742
  this.requireExecutionAdoptionPosture(context.identity, taskId);
1448
1743
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
1449
1744
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
1450
1745
  }
1451
- this.assertExecutionAuthorizationFresh(task);
1746
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1452
1747
  const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
1453
1748
  .filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
1454
1749
  if (currentPlanBlockingDecisions.length > 0) {
@@ -1470,6 +1765,7 @@ export class WorkflowService {
1470
1765
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
1471
1766
  }
1472
1767
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1768
+ let resumesCorrectiveReplan = false;
1473
1769
  if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1474
1770
  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.`, {
1475
1771
  taskId,
@@ -1484,20 +1780,21 @@ export class WorkflowService {
1484
1780
  && guardedPosture.correctiveDecision.planHash !== task.planHash
1485
1781
  ? guardedPosture.correctiveDecision
1486
1782
  : null;
1487
- const recovery = staleDecision
1783
+ const recovery = staleDecision?.decision === 'continue-fix'
1488
1784
  ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
1489
1785
  : null;
1490
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1786
+ const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1787
+ resumesCorrectiveReplan = gate.posture === 'replan-ready';
1491
1788
  }
1492
1789
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1493
1790
  const dirty = changedFiles(context.identity.repositoryRoot);
1494
- if (step.status === 'planned' && dirty.length > 0) {
1791
+ if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
1495
1792
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
1496
1793
  changedFiles: dirty,
1497
1794
  });
1498
1795
  }
1499
1796
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
1500
- if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
1797
+ if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
1501
1798
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
1502
1799
  changedFiles: dirtyOutsideStep,
1503
1800
  });
@@ -1555,7 +1852,8 @@ export class WorkflowService {
1555
1852
  }
1556
1853
  return {
1557
1854
  task: saved,
1558
- lease,
1855
+ lease: toPublicWriterLease(lease, this.now()),
1856
+ writerCredential: { disposition: acquiredHere ? 'issued' : 'renewed' },
1559
1857
  envelope: {
1560
1858
  protocolVersion: PROTOCOL_VERSION,
1561
1859
  packageVersion: PACKAGE_VERSION,
@@ -1591,14 +1889,14 @@ export class WorkflowService {
1591
1889
  taskId,
1592
1890
  stepId,
1593
1891
  expectedRevision: saved.revision,
1594
- writerToken: lease.token,
1892
+ writerToken: acquiredHere ? lease.token : null,
1595
1893
  knowledgeMap: knowledgeSummary(knowledgeMap),
1596
1894
  knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
1597
1895
  },
1598
1896
  };
1599
1897
  }
1600
1898
  taskContext(repository, taskId, role) {
1601
- const { identity } = this.context(repository);
1899
+ const { identity } = this.#mutationContext(repository);
1602
1900
  const task = this.store.readTask(identity.projectId, taskId);
1603
1901
  const historicalStrictReview = role === 'independent-reviewer'
1604
1902
  && task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
@@ -1677,7 +1975,7 @@ export class WorkflowService {
1677
1975
  };
1678
1976
  }
1679
1977
  completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
1680
- const context = this.context(repository);
1978
+ const context = this.#mutationContext(repository);
1681
1979
  const lease = context.locks.heartbeat(taskId, writerToken);
1682
1980
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1683
1981
  const task = this.store.readTask(context.identity.projectId, taskId);
@@ -1763,7 +2061,7 @@ export class WorkflowService {
1763
2061
  return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
1764
2062
  }
1765
2063
  reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
1766
- const context = this.context(repository);
2064
+ const context = this.#mutationContext(repository);
1767
2065
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1768
2066
  const task = this.store.readTask(context.identity.projectId, taskId);
1769
2067
  assertExpectedRevision(task, expectedRevision);
@@ -1819,10 +2117,18 @@ export class WorkflowService {
1819
2117
  resolution: currentCycle.resolution,
1820
2118
  });
1821
2119
  }
1822
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
2120
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2121
+ if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2122
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
2123
+ taskId: task.id,
2124
+ stepId,
2125
+ infrastructureFailures,
2126
+ requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
2127
+ });
2128
+ }
1823
2129
  }
1824
2130
  else if (currentCycle.resolution !== 'pending') {
1825
- if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
2131
+ if (currentCycle.resolution === 'failed') {
1826
2132
  if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1827
2133
  throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1828
2134
  taskId: task.id,
@@ -1830,7 +2136,7 @@ export class WorkflowService {
1830
2136
  resolution: currentCycle.resolution,
1831
2137
  });
1832
2138
  }
1833
- appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2139
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
1834
2140
  completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
1835
2141
  checksHash: currentCycle.reviewRecordedEvent.checksHash,
1836
2142
  reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
@@ -1848,7 +2154,7 @@ export class WorkflowService {
1848
2154
  reviewedCommit: launch.reviewedCommit,
1849
2155
  isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
1850
2156
  }, this.now());
1851
- if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
2157
+ if (recorded.resolution === 'failed') {
1852
2158
  const reviewRecordedEvent = recorded.reviewRecordedEvent;
1853
2159
  const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
1854
2160
  if (!reviewRecordedEvent || !reviewerAttestationEvent) {
@@ -1858,7 +2164,7 @@ export class WorkflowService {
1858
2164
  resolution: recorded.resolution,
1859
2165
  });
1860
2166
  }
1861
- appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2167
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
1862
2168
  completionCommit: reviewRecordedEvent.completionCommit,
1863
2169
  checksHash: reviewRecordedEvent.checksHash,
1864
2170
  reviewEventHash: reviewRecordedEvent.eventHash,
@@ -1867,8 +2173,132 @@ export class WorkflowService {
1867
2173
  }
1868
2174
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
1869
2175
  }
2176
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2177
+ const identity = resolveRepositoryIdentity(repository);
2178
+ const task = this.store.readTask(identity.projectId, taskId);
2179
+ assertExpectedRevision(task, expectedRevision);
2180
+ if (task.status !== 'in_progress' && task.status !== 'blocked') {
2181
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
2182
+ }
2183
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2184
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2185
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
2186
+ }
2187
+ const step = requireStep(task, stepId);
2188
+ if (step.status !== 'in_progress' || !step.evidence) {
2189
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
2190
+ }
2191
+ validateTaskHistory(identity.repositoryRoot, task);
2192
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
2193
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2194
+ }
2195
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2196
+ return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
2197
+ }
2198
+ recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2199
+ const context = this.#mutationContext(repository);
2200
+ const task = this.store.readTask(context.identity.projectId, taskId);
2201
+ assertExpectedRevision(task, expectedRevision);
2202
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2203
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2204
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2205
+ let review;
2206
+ try {
2207
+ review = validateExternalStrictStepReviewInput(input, packet);
2208
+ }
2209
+ catch (error) {
2210
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
2211
+ }
2212
+ if (review.reviewer.trim() === actor.trim()) {
2213
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
2214
+ }
2215
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
2216
+ if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
2217
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
2218
+ taskId,
2219
+ stepId,
2220
+ resolution: currentCycle.resolution,
2221
+ });
2222
+ }
2223
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2224
+ if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2225
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
2226
+ taskId,
2227
+ stepId,
2228
+ infrastructureFailures,
2229
+ });
2230
+ }
2231
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2232
+ const recorded = recordStrictStepReview(this.store, task, stepId, {
2233
+ review,
2234
+ reviewerOutputHash: strictReviewInputHash(review),
2235
+ reviewedCommit: packet.reviewedCommit,
2236
+ isolationResult: 'verified',
2237
+ }, this.now());
2238
+ if (recorded.resolution === 'failed') {
2239
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2240
+ completionCommit: recorded.reviewRecordedEvent.completionCommit,
2241
+ checksHash: recorded.reviewRecordedEvent.checksHash,
2242
+ reviewEventHash: recorded.reviewRecordedEvent.eventHash,
2243
+ reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
2244
+ }, readCurrentPlanRiskAudit(this.store, task), this.now());
2245
+ }
2246
+ const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2247
+ return {
2248
+ mode: 'external-sealed',
2249
+ packetHash: packet.packetHash,
2250
+ repositorySealHash: packet.repositorySealHash,
2251
+ reviewerThreadId: input.reviewerThreadId,
2252
+ task: saved,
2253
+ };
2254
+ }
2255
+ prepareExternalTaskReview(repository, taskId, expectedRevision) {
2256
+ const identity = resolveRepositoryIdentity(repository);
2257
+ const task = this.store.readTask(identity.projectId, taskId);
2258
+ assertExpectedRevision(task, expectedRevision);
2259
+ const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
2260
+ if (task.status !== 'validating' && !retryingUnverified) {
2261
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
2262
+ }
2263
+ validateTaskHistory(identity.repositoryRoot, task);
2264
+ assertTaskBranch(identity.repositoryRoot, task);
2265
+ if (!isClean(identity.repositoryRoot)) {
2266
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
2267
+ }
2268
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2269
+ if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2270
+ throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2271
+ }
2272
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
2273
+ }
2274
+ recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2275
+ const context = this.#mutationContext(repository);
2276
+ const task = this.store.readTask(context.identity.projectId, taskId);
2277
+ assertExpectedRevision(task, expectedRevision);
2278
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2279
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2280
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
2281
+ let review;
2282
+ try {
2283
+ review = validateExternalStrictTaskReviewInput(input, packet);
2284
+ }
2285
+ catch (error) {
2286
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
2287
+ }
2288
+ if (review.reviewer.trim() === actor.trim()) {
2289
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
2290
+ }
2291
+ const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2292
+ return {
2293
+ mode: 'external-sealed',
2294
+ packetHash: packet.packetHash,
2295
+ repositorySealHash: packet.repositorySealHash,
2296
+ reviewerThreadId: input.reviewerThreadId,
2297
+ task: saved,
2298
+ };
2299
+ }
1870
2300
  submitTask(repository, taskId, expectedRevision, writerToken, actor) {
1871
- const context = this.context(repository);
2301
+ const context = this.#mutationContext(repository);
1872
2302
  const lease = context.locks.heartbeat(taskId, writerToken);
1873
2303
  const task = this.store.readTask(context.identity.projectId, taskId);
1874
2304
  assertExpectedRevision(task, expectedRevision);
@@ -1900,11 +2330,11 @@ export class WorkflowService {
1900
2330
  return saved;
1901
2331
  }
1902
2332
  recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
1903
- const { identity } = this.context(repository);
2333
+ const { identity } = this.#mutationContext(repository);
1904
2334
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1905
2335
  const task = this.store.readTask(identity.projectId, taskId);
1906
2336
  assertExpectedRevision(task, expectedRevision);
1907
- this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
2337
+ this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
1908
2338
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1909
2339
  const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
1910
2340
  if (task.status !== 'validating' && !retryingUnverified) {
@@ -1942,30 +2372,71 @@ export class WorkflowService {
1942
2372
  return saved;
1943
2373
  }
1944
2374
  recoverStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1945
- const context = this.context(repository);
2375
+ const context = this.#mutationContext(repository);
1946
2376
  const task = this.store.readTask(context.identity.projectId, taskId);
1947
2377
  assertExpectedRevision(task, expectedRevision);
1948
2378
  this.requireExecutionAdoptionPosture(context.identity, taskId);
1949
2379
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
1950
2380
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
1951
2381
  }
1952
- this.assertExecutionAuthorizationFresh(task);
2382
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
1953
2383
  this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1954
2384
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1955
2385
  }
1956
2386
  recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1957
- const context = this.context(repository);
2387
+ const context = this.#mutationContext(repository);
1958
2388
  const task = this.store.readTask(context.identity.projectId, taskId);
1959
2389
  assertExpectedRevision(task, expectedRevision);
1960
2390
  this.requireExecutionAdoptionPosture(context.identity, taskId);
1961
2391
  return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1962
2392
  }
2393
+ recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
2394
+ const context = this.#mutationContext(repository);
2395
+ const task = this.store.readTask(context.identity.projectId, taskId);
2396
+ assertExpectedRevision(task, expectedRevision);
2397
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
2398
+ this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
2399
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
2400
+ assertTaskBranch(context.identity.repositoryRoot, task);
2401
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2402
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2403
+ const handoff = readTaskC1Posture(this.store, task);
2404
+ const requiredActor = handoff.state === 'claimed'
2405
+ ? handoff.claimant
2406
+ : defaultPlanIntegrityRecoveryActor(task.id);
2407
+ if (actor.trim() !== requiredActor) {
2408
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery actor does not match the fresh Workflow route.', {
2409
+ taskId,
2410
+ stepId,
2411
+ requiredActor,
2412
+ actualActor: actor.trim(),
2413
+ });
2414
+ }
2415
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2416
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2417
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the failed Step to be guarded by the current Plan Risk Audit.', {
2418
+ taskId,
2419
+ stepId,
2420
+ });
2421
+ }
2422
+ const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
2423
+ if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
2424
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
2425
+ taskId,
2426
+ stepId,
2427
+ applicable: assessment.applicable,
2428
+ blockers: assessment.blockers,
2429
+ });
2430
+ }
2431
+ const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
2432
+ return { assessment, event };
2433
+ }
1963
2434
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1964
- const { identity } = this.context(repository);
2435
+ const { identity } = this.#mutationContext(repository);
1965
2436
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1966
2437
  const task = this.store.readTask(identity.projectId, taskId);
1967
2438
  assertExpectedRevision(task, expectedRevision);
1968
- this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
2439
+ this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
1969
2440
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1970
2441
  if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
1971
2442
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
@@ -2007,7 +2478,7 @@ export class WorkflowService {
2007
2478
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
2008
2479
  }
2009
2480
  setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
2010
- const { identity } = this.context(repository);
2481
+ const { identity } = this.#mutationContext(repository);
2011
2482
  const task = this.store.readTask(identity.projectId, taskId);
2012
2483
  assertExpectedRevision(task, expectedRevision);
2013
2484
  if (task.status !== 'validating' || task.review.status !== 'passed') {
@@ -2021,7 +2492,7 @@ export class WorkflowService {
2021
2492
  return saved;
2022
2493
  }
2023
2494
  acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
2024
- const { identity } = this.context(repository);
2495
+ const { identity } = this.#mutationContext(repository);
2025
2496
  const task = this.store.readTask(identity.projectId, taskId);
2026
2497
  assertExpectedRevision(task, expectedRevision);
2027
2498
  if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
@@ -2059,7 +2530,7 @@ export class WorkflowService {
2059
2530
  }, expectedRevision);
2060
2531
  }
2061
2532
  syncTaskBase(repository, taskId, expectedRevision) {
2062
- const { identity } = this.context(repository);
2533
+ const { identity } = this.#mutationContext(repository);
2063
2534
  const task = this.store.readTask(identity.projectId, taskId);
2064
2535
  assertExpectedRevision(task, expectedRevision);
2065
2536
  if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
@@ -2111,7 +2582,7 @@ export class WorkflowService {
2111
2582
  return saved;
2112
2583
  }
2113
2584
  mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
2114
- const context = this.context(repository);
2585
+ const context = this.#mutationContext(repository);
2115
2586
  const task = this.store.readTask(context.identity.projectId, taskId);
2116
2587
  assertExpectedRevision(task, expectedRevision);
2117
2588
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -2133,7 +2604,7 @@ export class WorkflowService {
2133
2604
  return saved;
2134
2605
  }
2135
2606
  confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
2136
- const context = this.context(repository);
2607
+ const context = this.#mutationContext(repository);
2137
2608
  const task = this.store.readTask(context.identity.projectId, taskId);
2138
2609
  assertExpectedRevision(task, expectedRevision);
2139
2610
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -2150,21 +2621,22 @@ export class WorkflowService {
2150
2621
  return saved;
2151
2622
  }
2152
2623
  status(repository) {
2153
- const { identity } = this.context(repository);
2154
- const tasks = this.store.listTasks(identity.projectId);
2155
- const rawMilestones = this.store.listMilestones(identity.projectId);
2156
- const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
2624
+ const snapshot = this.observationSnapshot(repository);
2157
2625
  return {
2158
- projectId: identity.projectId,
2159
- discoveries: this.store.listDiscoveries(identity.projectId),
2160
- tasks,
2161
- milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
2162
- delegations: this.store.listDelegations(identity.projectId),
2163
- adoption,
2626
+ projectId: snapshot.identity.projectId,
2627
+ discoveries: snapshot.discoveries,
2628
+ tasks: snapshot.tasks,
2629
+ milestones: snapshot.milestones,
2630
+ milestoneInitialAssembly: snapshot.milestones
2631
+ .map((milestone) => this.describeMilestoneInitialAssembly(snapshot.identity.projectId, milestone, snapshot.tasks))
2632
+ .filter((value) => value !== null),
2633
+ delegations: snapshot.delegations,
2634
+ adoption: snapshot.adoption,
2635
+ observation: snapshot.observation,
2164
2636
  };
2165
2637
  }
2166
2638
  recoverMilestoneTransactions(repository) {
2167
- const { identity } = this.context(repository);
2639
+ const { identity } = this.#mutationContext(repository);
2168
2640
  const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
2169
2641
  return {
2170
2642
  projectId: identity.projectId,
@@ -2172,11 +2644,11 @@ export class WorkflowService {
2172
2644
  };
2173
2645
  }
2174
2646
  prepareAdoption(repository) {
2175
- const { identity } = this.context(repository);
2647
+ const { identity } = this.#mutationContext(repository);
2176
2648
  return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
2177
2649
  }
2178
2650
  applyAdoption(repository, actor, confirmationCode) {
2179
- const context = this.context(repository);
2651
+ const context = this.#mutationContext(repository);
2180
2652
  const tasks = context.store.listTasks(context.identity.projectId);
2181
2653
  const milestones = context.store.listMilestones(context.identity.projectId);
2182
2654
  return applyAdoptionPosture(this.store, {
@@ -2193,19 +2665,19 @@ export class WorkflowService {
2193
2665
  }, actor, confirmationCode);
2194
2666
  }
2195
2667
  updatePreflight(repository) {
2196
- const context = this.context(repository);
2197
- const tasks = context.store.listTasks(context.identity.projectId);
2198
- const milestones = context.store.listMilestones(context.identity.projectId);
2199
- const dirty = changedFiles(context.identity.repositoryRoot);
2668
+ const snapshot = this.observationSnapshot(repository);
2669
+ const tasks = snapshot.tasks;
2670
+ const milestones = snapshot.milestones;
2671
+ const dirty = changedFiles(snapshot.identity.repositoryRoot);
2200
2672
  const runningSteps = tasks.flatMap((task) => task.steps
2201
2673
  .filter((step) => step.status === 'in_progress')
2202
2674
  .map((step) => ({ taskId: task.id, stepId: step.id })));
2203
- const leases = context.locks.list();
2675
+ const leases = snapshot.leases;
2204
2676
  const now = this.now().getTime();
2205
2677
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
2206
2678
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2207
2679
  const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2208
- const inspectedVersions = inspectWorkflowVersions(context.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2680
+ const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2209
2681
  const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
2210
2682
  ...base,
2211
2683
  milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
@@ -2232,31 +2704,20 @@ export class WorkflowService {
2232
2704
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
2233
2705
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2234
2706
  ...versionBlockers,
2707
+ ...observationPreflightBlockers(snapshot.observation),
2235
2708
  ];
2236
- const adoption = buildAdoptionPreparation({
2237
- projectId: context.identity.projectId,
2238
- repositoryIdentity: context.identity,
2239
- packageVersion: PACKAGE_VERSION,
2240
- protocolVersion: PROTOCOL_VERSION,
2241
- stateSchemaVersion: STATE_SCHEMA_VERSION,
2242
- tasks,
2243
- milestones,
2244
- sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
2245
- blockers,
2246
- now: this.now(),
2247
- }, readCurrentAdoptionPosture(this.store, context.identity.projectId));
2248
2709
  return {
2249
2710
  safe: blockers.length === 0,
2250
2711
  packageVersion: PACKAGE_VERSION,
2251
2712
  protocolVersion: PROTOCOL_VERSION,
2252
2713
  stateSchemaVersion: STATE_SCHEMA_VERSION,
2253
- projectId: context.identity.projectId,
2254
- repositoryRoot: context.identity.repositoryRoot,
2714
+ projectId: snapshot.identity.projectId,
2715
+ repositoryRoot: snapshot.identity.repositoryRoot,
2255
2716
  clean: dirty.length === 0,
2256
2717
  changedFiles: dirty,
2257
2718
  runningSteps,
2258
- activeLeases,
2259
- staleLeases,
2719
+ activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2720
+ staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2260
2721
  blockers,
2261
2722
  workflowVersions: {
2262
2723
  runtime: inspectedVersions.runtime,
@@ -2272,11 +2733,134 @@ export class WorkflowService {
2272
2733
  ? null
2273
2734
  : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
2274
2735
  },
2275
- adoption,
2736
+ adoption: snapshot.adoption,
2737
+ observation: snapshot.observation,
2738
+ };
2739
+ }
2740
+ dependencyProvenanceRecoveryPreflight(repository, taskId) {
2741
+ const snapshot = this.observationSnapshot(repository);
2742
+ if (snapshot.observation.kind !== 'assessment') {
2743
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency provenance recovery requires a coherent observation boundary.', {
2744
+ observation: snapshot.observation,
2745
+ });
2746
+ }
2747
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
2748
+ if (!task)
2749
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
2750
+ return this.inspectDependencyProvenanceCandidate(snapshot, task);
2751
+ }
2752
+ recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
2753
+ const normalizedActor = actor.trim();
2754
+ const normalizedReason = reason.trim();
2755
+ if (!normalizedActor)
2756
+ throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires an actor.');
2757
+ if (!normalizedReason)
2758
+ throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires a reason.');
2759
+ const context = this.#mutationContext(repository);
2760
+ const current = context.store.readTask(context.identity.projectId, taskId);
2761
+ const currentHead = headCommit(context.identity.repositoryRoot);
2762
+ const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead && record.packageVersion === PACKAGE_VERSION);
2763
+ if (prior && current.systemCommits.includes(currentHead))
2764
+ return current;
2765
+ assertExpectedRevision(current, expectedRevision);
2766
+ const snapshot = this.observationSnapshot(repository);
2767
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
2768
+ if (!task)
2769
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
2770
+ const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task);
2771
+ if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
2772
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
2773
+ taskId,
2774
+ blockers: preflight.blockers,
2775
+ });
2776
+ }
2777
+ return context.store.writeTask({
2778
+ ...task,
2779
+ systemCommits: [...task.systemCommits, preflight.commitSha],
2780
+ dependencyProvenanceRecoveries: [
2781
+ ...(task.dependencyProvenanceRecoveries ?? []),
2782
+ {
2783
+ commitSha: preflight.commitSha,
2784
+ parentCommitSha: preflight.parentCommitSha,
2785
+ packageName: PACKAGE_NAME,
2786
+ packageVersion: PACKAGE_VERSION,
2787
+ files: ['package-lock.json', 'package.json'],
2788
+ actor: normalizedActor,
2789
+ reason: normalizedReason,
2790
+ recordedAt: this.now().toISOString(),
2791
+ retainedHistoricalCommits: preflight.retainedHistoricalCommits,
2792
+ },
2793
+ ],
2794
+ }, expectedRevision);
2795
+ }
2796
+ milestoneProgress(repository, milestoneId) {
2797
+ const snapshot = this.observationSnapshot(repository);
2798
+ if (snapshot.observation.kind !== 'assessment') {
2799
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone progress requires a coherent read-only observation boundary.', {
2800
+ observation: snapshot.observation,
2801
+ });
2802
+ }
2803
+ const milestone = snapshot.milestones.find((candidate) => candidate.id === milestoneId);
2804
+ if (!milestone)
2805
+ throw new WorkflowError('NOT_FOUND', `Milestone ${milestoneId} was not found in this repository.`);
2806
+ const tasksById = new Map(snapshot.tasks.map((task) => [task.id, task]));
2807
+ const tasks = milestone.memberships.map((membership, index) => {
2808
+ const task = tasksById.get(membership.taskId);
2809
+ if (!task)
2810
+ throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
2811
+ const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
2812
+ const dependency = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
2813
+ const candidate = tasksById.get(candidateTaskId);
2814
+ if (!candidate)
2815
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
2816
+ return candidate;
2817
+ });
2818
+ const executionEligible = milestone.status === 'active' && !['merged', 'cancelled'].includes(task.status);
2819
+ return {
2820
+ ordinal: `T${String(index + 1).padStart(2, '0')}`,
2821
+ taskId: task.id,
2822
+ title: task.title,
2823
+ status: task.status,
2824
+ revision: task.revision,
2825
+ disposition: membership.disposition,
2826
+ membershipReason: membership.reason,
2827
+ dependencyState: dependency.dependencyState,
2828
+ dependsOnTaskIds: dependency.dependsOnTaskIds,
2829
+ unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
2830
+ runnable: executionEligible && dependency.runnable,
2831
+ dependencyBlockReason: !executionEligible
2832
+ ? milestone.status !== 'active'
2833
+ ? `Milestone is ${milestone.status}, not active.`
2834
+ : `Task is terminal (${task.status}).`
2835
+ : dependency.blockReason,
2836
+ replacementForTaskId: task.replacementForTaskId ?? null,
2837
+ replacedByTaskId: task.replacedByTaskId ?? null,
2838
+ stepsCompleted,
2839
+ stepsTotal: task.steps.length,
2840
+ steps: task.steps.map((step, stepIndex) => ({
2841
+ ordinal: `S${String(stepIndex + 1).padStart(2, '0')}`,
2842
+ stepId: step.id,
2843
+ title: step.title,
2844
+ status: step.status,
2845
+ })),
2846
+ };
2847
+ });
2848
+ const required = tasks.filter((task) => task.disposition === 'required');
2849
+ return {
2850
+ readOnly: true,
2851
+ projectId: snapshot.identity.projectId,
2852
+ milestoneId: milestone.id,
2853
+ milestoneTitle: milestone.title,
2854
+ milestoneStatus: milestone.status,
2855
+ milestoneRevision: milestone.revision,
2856
+ membershipRevision: milestone.membershipRevision,
2857
+ tasksCompleted: required.filter((task) => task.status === 'merged').length,
2858
+ tasksRequired: required.length,
2859
+ tasks,
2276
2860
  };
2277
2861
  }
2278
2862
  alpha6StrictReviewRescuePreflight(repository) {
2279
- const context = this.context(repository);
2863
+ const context = this.#mutationContext(repository);
2280
2864
  const dependency = inspectLocalDependency(context.identity.repositoryRoot);
2281
2865
  const tasks = context.store.listTasks(context.identity.projectId);
2282
2866
  const leases = context.locks.list();
@@ -2371,15 +2955,15 @@ export class WorkflowService {
2371
2955
  projectId: context.identity.projectId,
2372
2956
  repositoryRoot: context.identity.repositoryRoot,
2373
2957
  clean: dirty.length === 0,
2374
- activeLeases,
2375
- staleLeases,
2958
+ activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2959
+ staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2376
2960
  candidate,
2377
2961
  blockers,
2378
2962
  requiredActions,
2379
2963
  };
2380
2964
  }
2381
2965
  scanKnowledgeMap(repository) {
2382
- const { identity } = this.context(repository);
2966
+ const { identity } = this.#mutationContext(repository);
2383
2967
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2384
2968
  try {
2385
2969
  const current = this.store.readKnowledgeMap(identity.projectId);
@@ -2392,11 +2976,11 @@ export class WorkflowService {
2392
2976
  }
2393
2977
  }
2394
2978
  showKnowledgeMap(repository) {
2395
- const { identity } = this.context(repository);
2979
+ const { identity } = this.#mutationContext(repository);
2396
2980
  return this.store.readKnowledgeMap(identity.projectId);
2397
2981
  }
2398
2982
  knowledgeMapStatus(repository) {
2399
- const { identity } = this.context(repository);
2983
+ const { identity } = this.#mutationContext(repository);
2400
2984
  const current = this.store.readKnowledgeMap(identity.projectId);
2401
2985
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2402
2986
  return inspectKnowledgeMap(current, scanned);
@@ -2405,7 +2989,7 @@ export class WorkflowService {
2405
2989
  return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
2406
2990
  }
2407
2991
  approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
2408
- const { identity } = this.context(repository);
2992
+ const { identity } = this.#mutationContext(repository);
2409
2993
  const current = this.store.readKnowledgeMap(identity.projectId);
2410
2994
  assertExpectedRevision(current, expectedRevision);
2411
2995
  const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
@@ -2422,43 +3006,35 @@ export class WorkflowService {
2422
3006
  return this.store.writeKnowledgeMap(approved, expectedRevision);
2423
3007
  }
2424
3008
  reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
2425
- const { identity } = this.context(repository);
3009
+ const { identity } = this.#mutationContext(repository);
2426
3010
  const current = this.store.readKnowledgeMap(identity.projectId);
2427
3011
  assertExpectedRevision(current, expectedRevision);
2428
3012
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2429
3013
  if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2430
3014
  const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
2431
- const activeTask = this.store
2432
- .listTasks(identity.projectId)
2433
- .find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2434
- if (activeTask?.planHash
2435
- && taskCanRebindKnowledge(activeTask)
2436
- && !this.taskHasTerminalCorrectiveDecision(activeTask)) {
2437
- const inspected = inspectKnowledgeMap(current, scanned);
2438
- const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
2439
- if (assessment.eligible
2440
- && this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
2441
- throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2442
- taskId: activeTask.id,
2443
- requiredAction: 'Run the exact task context-refresh action returned by next.',
2444
- });
2445
- }
3015
+ const inspected = inspectKnowledgeMap(current, scanned);
3016
+ const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
3017
+ if (refreshOwner) {
3018
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
3019
+ taskId: refreshOwner.task.id,
3020
+ requiredAction: 'Run the exact task context-refresh action returned by next.',
3021
+ });
2446
3022
  }
2447
3023
  }
2448
3024
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
2449
3025
  }
2450
3026
  graphRefreshRequest(repository, graphKind, mode = 'moderate') {
2451
- const { identity } = this.context(repository);
3027
+ const { identity } = this.#mutationContext(repository);
2452
3028
  return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2453
3029
  }
2454
3030
  graphStatus(repository, graphKind, mode = 'moderate') {
2455
- const { identity } = this.context(repository);
3031
+ const { identity } = this.#mutationContext(repository);
2456
3032
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2457
3033
  const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
2458
3034
  return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
2459
3035
  }
2460
3036
  bindGraph(repository, expectedRevision, request, evidence) {
2461
- const { identity } = this.context(repository);
3037
+ const { identity } = this.#mutationContext(repository);
2462
3038
  const currentRequest = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, request.graphKind, request.mode, this.now());
2463
3039
  validateGraphRequestCurrent(request, currentRequest);
2464
3040
  const existing = this.readGraphBindingOrNull(identity.projectId, request.graphKind);
@@ -2466,13 +3042,78 @@ export class WorkflowService {
2466
3042
  return this.store.writeGraphBinding(binding, expectedRevision);
2467
3043
  }
2468
3044
  recordGraphFallback(repository, graphKind, mode, expectedRevision, reason) {
2469
- const { identity } = this.context(repository);
3045
+ const { identity } = this.#mutationContext(repository);
2470
3046
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2471
3047
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
2472
3048
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
2473
3049
  }
2474
- next(repository) {
3050
+ next(repository, taskId = null) {
2475
3051
  const status = this.status(repository);
3052
+ if (status.observation.kind === 'blocked') {
3053
+ return {
3054
+ scope: 'project',
3055
+ projectId: status.projectId,
3056
+ status: 'observation-blocked',
3057
+ action: 'blocked',
3058
+ observation: status.observation,
3059
+ navigation: {
3060
+ kind: 'blocked',
3061
+ reasonCodes: status.observation.reasonCodes,
3062
+ availableRepairTransitionId: null,
3063
+ },
3064
+ };
3065
+ }
3066
+ if (status.observation.kind === 'repair') {
3067
+ const repair = status.observation.repair;
3068
+ return {
3069
+ scope: 'project',
3070
+ projectId: status.projectId,
3071
+ status: 'repair-required',
3072
+ action: repair?.command ?? 'blocked',
3073
+ observation: status.observation,
3074
+ navigation: {
3075
+ kind: repair ? 'repair' : 'blocked',
3076
+ transitionId: null,
3077
+ availableRepairTransitionId: null,
3078
+ ...(repair ? {
3079
+ command: repair.command,
3080
+ cataloged: repair.cataloged,
3081
+ ...('available' in repair ? { available: repair.available } : {}),
3082
+ bindings: repair.command === 'milestone recover'
3083
+ ? {
3084
+ projectId: repair.projectId,
3085
+ milestoneIds: repair.milestoneIds,
3086
+ transactionIds: repair.transactionIds,
3087
+ }
3088
+ : 'taskIds' in repair
3089
+ ? {
3090
+ projectId: repair.projectId,
3091
+ taskIds: repair.taskIds,
3092
+ transactionIds: repair.transactionIds,
3093
+ }
3094
+ : {
3095
+ projectId: repair.projectId,
3096
+ entityIds: repair.entityIds,
3097
+ },
3098
+ } : {}),
3099
+ reasonCodes: status.observation.reasonCodes,
3100
+ },
3101
+ };
3102
+ }
3103
+ if (!status.adoption) {
3104
+ return {
3105
+ scope: 'project',
3106
+ projectId: status.projectId,
3107
+ status: 'observation-blocked',
3108
+ action: 'blocked',
3109
+ observation: status.observation,
3110
+ navigation: {
3111
+ kind: 'blocked',
3112
+ reasonCodes: status.observation.reasonCodes,
3113
+ availableRepairTransitionId: null,
3114
+ },
3115
+ };
3116
+ }
2476
3117
  const adoptionInfo = {
2477
3118
  status: status.adoption.status,
2478
3119
  safe: status.adoption.safe,
@@ -2490,19 +3131,130 @@ export class WorkflowService {
2490
3131
  status: status.adoption.status,
2491
3132
  action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
2492
3133
  adoption: adoptionInfo,
3134
+ observation: status.observation,
3135
+ navigation: status.adoption.safe
3136
+ ? {
3137
+ kind: 'human-gate',
3138
+ transitionId: null,
3139
+ cataloged: false,
3140
+ command: 'state adoption-apply',
3141
+ bindings: {
3142
+ projectId: status.projectId,
3143
+ confirmationCode: status.adoption.confirmationCode,
3144
+ confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
3145
+ actorRequired: true,
3146
+ },
3147
+ }
3148
+ : {
3149
+ kind: 'blocked',
3150
+ transitionId: null,
3151
+ availableRepairTransitionId: null,
3152
+ reasonCodes: ['ADOPTION_MISSING'],
3153
+ blockers: status.adoption.blockers,
3154
+ },
2493
3155
  };
2494
3156
  }
2495
- const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
3157
+ const withAdoption = (value) => ({
3158
+ ...value,
3159
+ adoption: adoptionInfo,
3160
+ observation: status.observation,
3161
+ });
2496
3162
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
3163
+ const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
2497
3164
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
2498
- const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
3165
+ const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
3166
+ const requestedTask = taskId === null
3167
+ ? null
3168
+ : status.tasks.find((task) => task.id === taskId) ?? null;
3169
+ if (taskId !== null && !requestedTask) {
3170
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3171
+ }
3172
+ if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById, tasksById)) {
3173
+ const requestedMilestone = requestedTask.milestoneId
3174
+ ? milestonesById.get(requestedTask.milestoneId) ?? null
3175
+ : null;
3176
+ const dependency = requestedMilestone
3177
+ ? assessMilestoneTaskDependencies(requestedMilestone, requestedTask.id, (candidateTaskId) => {
3178
+ const candidate = tasksById.get(candidateTaskId);
3179
+ if (!candidate)
3180
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
3181
+ return candidate;
3182
+ })
3183
+ : null;
3184
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
3185
+ taskId: requestedTask.id,
3186
+ taskStatus: requestedTask.status,
3187
+ milestoneId: requestedTask.milestoneId,
3188
+ ...(dependency ? { dependency } : {}),
3189
+ });
3190
+ }
3191
+ const activeTask = requestedTask
3192
+ ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
2499
3193
  if (activeTask) {
2500
- const activePlanRiskAudit = activeTask.planHash
2501
- ? readCurrentPlanRiskAudit(this.store, activeTask)
3194
+ const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3195
+ if (dependencyRecovery.eligible) {
3196
+ return withAdoption({
3197
+ scope: 'task',
3198
+ id: activeTask.id,
3199
+ status: activeTask.status,
3200
+ action: 'update dependency-provenance-recover',
3201
+ revision: activeTask.revision,
3202
+ dependencyProvenanceRecovery: dependencyRecovery,
3203
+ });
3204
+ }
3205
+ const activeTaskMilestone = activeTask.milestoneId
3206
+ ? milestonesById.get(activeTask.milestoneId) ?? null
2502
3207
  : null;
3208
+ const milestoneCancellationSafe = Boolean(activeTaskMilestone
3209
+ && !activeTask.steps.some((step) => step.status === 'in_progress')
3210
+ && status.observation.leases.length === 0
3211
+ && status.observation.coreTaskOperations.length === 0
3212
+ && status.observation.pendingProjectTransactions.length === 0
3213
+ && status.observation.pendingMilestoneTransactions.length === 0
3214
+ && status.observation.pendingTaskTransactions.length === 0
3215
+ && status.observation.corruptTaskTransactions.length === 0);
3216
+ const withMilestoneCancellation = (value) => (milestoneCancellationSafe
3217
+ ? {
3218
+ ...value,
3219
+ milestoneCancellationOption: {
3220
+ command: 'milestone cancel',
3221
+ milestoneId: activeTaskMilestone.id,
3222
+ expectedRevision: activeTaskMilestone.revision,
3223
+ reasonRequired: true,
3224
+ taskDisposition: 'retained-historical',
3225
+ },
3226
+ }
3227
+ : value);
3228
+ let activePlanRiskAudit = null;
3229
+ let planRiskAuditRecovery = null;
3230
+ if (activeTask.planHash) {
3231
+ try {
3232
+ activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
3233
+ }
3234
+ catch (error) {
3235
+ if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
3236
+ throw error;
3237
+ planRiskAuditRecovery = {
3238
+ scope: 'task',
3239
+ id: activeTask.id,
3240
+ status: activeTask.status,
3241
+ action: 'task plan-risk-audit',
3242
+ revision: activeTask.revision,
3243
+ planRiskAuditRecovery: {
3244
+ state: 'required',
3245
+ reason: error.message,
3246
+ currentBriefHash: activeTask.briefHash,
3247
+ currentPlanHash: activeTask.planHash,
3248
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
3249
+ knowledgeMapHash: activeTask.knowledgeMapHash,
3250
+ },
3251
+ };
3252
+ }
3253
+ }
2503
3254
  const taskNext = activeTask.status === 'planning'
2504
3255
  ? null
2505
- : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
3256
+ : planRiskAuditRecovery
3257
+ ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
2506
3258
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
2507
3259
  // strict review are executable while context refresh correctly remains forbidden.
2508
3260
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -2511,15 +3263,32 @@ export class WorkflowService {
2511
3263
  if (taskNext?.action === 'task step-complete'
2512
3264
  || taskNext?.action === 'task step-review'
2513
3265
  || taskNext?.action === 'task remediation-mode-recover'
3266
+ || taskNext?.action === 'task plan-integrity-recover'
3267
+ || taskNext?.blockedAction === 'task plan-integrity-recover'
2514
3268
  || hardStop
2515
3269
  || terminalCorrectiveDecision === 'split-required'
2516
3270
  || terminalCorrectiveDecision === 'stop-escalate') {
2517
- return withAdoption(taskNext);
3271
+ return withAdoption(withMilestoneCancellation(taskNext));
2518
3272
  }
2519
3273
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
2520
- return withAdoption(taskNext);
3274
+ return withAdoption(withMilestoneCancellation(taskNext));
3275
+ }
3276
+ let knowledgeMap;
3277
+ try {
3278
+ knowledgeMap = inspectCurrentKnowledgeMap();
3279
+ }
3280
+ catch (error) {
3281
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3282
+ throw error;
3283
+ return withAdoption(withMilestoneCancellation({
3284
+ scope: 'project-memory',
3285
+ status: 'missing',
3286
+ action: 'project-memory scan',
3287
+ stalenessOriginTaskId: activeTask.id,
3288
+ authorizesTask: false,
3289
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
3290
+ }));
2521
3291
  }
2522
- const knowledgeMap = this.knowledgeMapStatus(repository);
2523
3292
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
2524
3293
  const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
2525
3294
  const delegatedApprovalOptions = action === 'project-memory approve'
@@ -2535,7 +3304,17 @@ export class WorkflowService {
2535
3304
  const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
2536
3305
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2537
3306
  : [];
2538
- return withAdoption({
3307
+ if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
3308
+ const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
3309
+ if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
3310
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
3311
+ taskId: requestedTask.id,
3312
+ blockingTaskId: refreshOwner.task.id,
3313
+ requiredAction: 'Run repository next and its exact task context-refresh action first.',
3314
+ });
3315
+ }
3316
+ }
3317
+ return withAdoption(withMilestoneCancellation({
2539
3318
  scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2540
3319
  status: knowledgeMap.status,
2541
3320
  action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
@@ -2567,14 +3346,14 @@ export class WorkflowService {
2567
3346
  },
2568
3347
  }
2569
3348
  : {}),
2570
- });
3349
+ }));
2571
3350
  }
2572
3351
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
2573
3352
  if (taskCanRebindKnowledge(activeTask) &&
2574
3353
  activeTask.planHash &&
2575
3354
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
2576
3355
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
2577
- return withAdoption({
3356
+ return withAdoption(withMilestoneCancellation({
2578
3357
  scope: 'task',
2579
3358
  id: activeTask.id,
2580
3359
  status: activeTask.status,
@@ -2582,12 +3361,69 @@ export class WorkflowService {
2582
3361
  revision: activeTask.revision,
2583
3362
  knowledgeMapRevision: knowledgeMap.revision,
2584
3363
  knowledgeMapHash,
2585
- });
3364
+ }));
2586
3365
  }
2587
- return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
3366
+ return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)));
3367
+ }
3368
+ let repositoryKnowledgeMap;
3369
+ try {
3370
+ repositoryKnowledgeMap = inspectCurrentKnowledgeMap();
3371
+ }
3372
+ catch (error) {
3373
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3374
+ throw error;
3375
+ return withAdoption({
3376
+ scope: 'project-memory',
3377
+ status: 'missing',
3378
+ action: 'project-memory scan',
3379
+ authorizesMilestone: false,
3380
+ provenanceNotice: 'Project Knowledge must be current before Discovery or Milestone authorization.',
3381
+ });
3382
+ }
3383
+ if (repositoryKnowledgeMap.status !== 'active' || !repositoryKnowledgeMap.approval) {
3384
+ const action = repositoryKnowledgeMap.status === 'stale'
3385
+ ? 'project-memory reconcile'
3386
+ : 'project-memory approve';
3387
+ const delegatedApprovalOptions = action === 'project-memory approve'
3388
+ ? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', repositoryKnowledgeMap)
3389
+ : [];
3390
+ return withAdoption({
3391
+ scope: 'project-memory',
3392
+ status: repositoryKnowledgeMap.status,
3393
+ action,
3394
+ revision: repositoryKnowledgeMap.revision,
3395
+ authorizesMilestone: false,
3396
+ provenanceNotice: 'Project Knowledge must be reconciled and approved before Discovery or Milestone authorization.',
3397
+ ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3398
+ });
2588
3399
  }
2589
3400
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2590
3401
  if (activeMilestone) {
3402
+ const initialAssembly = status.milestoneInitialAssembly
3403
+ .find((candidate) => candidate.milestoneId === activeMilestone.id) ?? null;
3404
+ if (initialAssembly) {
3405
+ const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
3406
+ if (activeDiscovery) {
3407
+ return withAdoption({
3408
+ scope: 'discovery',
3409
+ id: activeDiscovery.id,
3410
+ status: activeDiscovery.status,
3411
+ action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
3412
+ readiness: assessDiscovery(activeDiscovery),
3413
+ milestoneInitialAssembly: initialAssembly,
3414
+ });
3415
+ }
3416
+ return withAdoption({
3417
+ scope: 'milestone',
3418
+ id: activeMilestone.id,
3419
+ status: activeMilestone.status,
3420
+ action: 'milestone initial-assembly',
3421
+ revision: activeMilestone.revision,
3422
+ linkedTaskIds: initialAssembly.linkedTaskIds,
3423
+ executionAvailable: initialAssembly.executionAvailable,
3424
+ milestoneInitialAssembly: initialAssembly,
3425
+ });
3426
+ }
2591
3427
  if (activeMilestone.status === 'awaiting_final_acceptance') {
2592
3428
  const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
2593
3429
  const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
@@ -2638,6 +3474,19 @@ export class WorkflowService {
2638
3474
  status: activeMilestone.status,
2639
3475
  action: nextForMilestone(activeMilestone),
2640
3476
  revision: activeMilestone.revision,
3477
+ ...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
3478
+ ? {
3479
+ milestoneAuthorizationOptions: [
3480
+ { action: 'milestone authorize', mode: 'direct-human' },
3481
+ {
3482
+ action: 'milestone autonomy-prepare',
3483
+ mode: 'delegated',
3484
+ requiresExactHumanConfirmation: true,
3485
+ maximumDurationHours: 72,
3486
+ },
3487
+ ],
3488
+ }
3489
+ : {}),
2641
3490
  ...(activeMilestone.status === 'active'
2642
3491
  ? {
2643
3492
  completionPolicy: {
@@ -2649,7 +3498,21 @@ export class WorkflowService {
2649
3498
  .filter((membership) => membership.disposition === 'required')
2650
3499
  .map((membership) => {
2651
3500
  const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
2652
- return { taskId: membership.taskId, status: task?.status ?? 'missing' };
3501
+ const dependency = assessMilestoneTaskDependencies(activeMilestone, membership.taskId, (candidateTaskId) => {
3502
+ const candidate = status.tasks.find((taskCandidate) => taskCandidate.id === candidateTaskId);
3503
+ if (!candidate)
3504
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
3505
+ return candidate;
3506
+ });
3507
+ return {
3508
+ taskId: membership.taskId,
3509
+ status: task?.status ?? 'missing',
3510
+ dependencyState: dependency.dependencyState,
3511
+ dependsOnTaskIds: dependency.dependsOnTaskIds,
3512
+ unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
3513
+ runnable: Boolean(task && !['merged', 'cancelled'].includes(task.status) && dependency.runnable),
3514
+ dependencyBlockReason: dependency.blockReason,
3515
+ };
2653
3516
  }),
2654
3517
  },
2655
3518
  }
@@ -2772,7 +3635,21 @@ export class WorkflowService {
2772
3635
  }
2773
3636
  }
2774
3637
  }
2775
- nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
3638
+ currentMilestoneDependencyBinding(milestone, taskId) {
3639
+ assertMilestoneTaskRunnable(milestone, taskId, (candidateTaskId) => this.store.readTask(milestone.projectId, candidateTaskId));
3640
+ if (!milestone.planHash) {
3641
+ throw new WorkflowError('STATE_CORRUPT', `Active Milestone ${milestone.id} has no Plan hash.`, {
3642
+ milestoneId: milestone.id,
3643
+ taskId,
3644
+ });
3645
+ }
3646
+ return {
3647
+ milestoneRevision: milestone.revision,
3648
+ membershipRevision: milestone.membershipRevision,
3649
+ milestonePlanHash: milestone.planHash,
3650
+ };
3651
+ }
3652
+ nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
2776
3653
  const transition = task.status === 'awaiting_execution_authorization'
2777
3654
  ? 'task.execution_authorize'
2778
3655
  : task.status === 'awaiting_final_acceptance'
@@ -2794,8 +3671,73 @@ export class WorkflowService {
2794
3671
  throw error;
2795
3672
  }
2796
3673
  }
2797
- const next = nextForTask(task);
3674
+ let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
3675
+ ? guardedCorrectivePosture
3676
+ : null;
3677
+ if (!remediationModeRecovery && planRiskAudit && guardedCorrectivePosture === null) {
3678
+ try {
3679
+ catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
3680
+ }
3681
+ catch (error) {
3682
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
3683
+ if (!remediationModeRecovery)
3684
+ throw error;
3685
+ }
3686
+ }
3687
+ const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
3688
+ let mechanicalFeasibilityOverlay = {};
3689
+ if (task.status === 'awaiting_execution_authorization' && task.planHash) {
3690
+ try {
3691
+ const taskRoot = this.store.taskRoot(projectId, task.id);
3692
+ const briefHash = this.store.hashArtifact(taskRoot, 'brief.md');
3693
+ const planHash = this.store.hashArtifact(taskRoot, 'plan.md');
3694
+ if (task.briefHash !== briefHash || task.planHash !== planHash) {
3695
+ throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
3696
+ taskId: task.id,
3697
+ stateBriefHash: task.briefHash,
3698
+ artifactBriefHash: briefHash,
3699
+ statePlanHash: task.planHash,
3700
+ artifactPlanHash: planHash,
3701
+ });
3702
+ }
3703
+ const mechanicalFeasibility = evaluateMechanicalFeasibility(repositoryRoot, task, briefHash, planHash);
3704
+ mechanicalFeasibilityOverlay = mechanicalFeasibility.decision === 'blocked'
3705
+ ? {
3706
+ action: 'task plan-set',
3707
+ blockedAction: 'task authorize',
3708
+ mechanicalFeasibility,
3709
+ mechanicalFeasibilityGate: {
3710
+ state: 'blocked',
3711
+ diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
3712
+ requiredAction: 'Replace the current Plan through task plan-set, then request fresh execution authorization.',
3713
+ },
3714
+ }
3715
+ : {
3716
+ mechanicalFeasibility,
3717
+ mechanicalFeasibilityGate: {
3718
+ state: 'clear',
3719
+ meaning: 'No supported analyzer proved infeasibility; semantic sufficiency is not asserted.',
3720
+ },
3721
+ };
3722
+ }
3723
+ catch (error) {
3724
+ mechanicalFeasibilityOverlay = {
3725
+ action: 'doctor',
3726
+ blockedAction: 'task authorize',
3727
+ mechanicalFeasibilityGate: {
3728
+ state: 'unavailable',
3729
+ diagnosticCode: error instanceof WorkflowError ? error.code : 'UNKNOWN_ERROR',
3730
+ reason: error instanceof Error ? error.message : String(error),
3731
+ requiredAction: 'Restore the exact Plan/source authority before execution authorization.',
3732
+ },
3733
+ };
3734
+ }
3735
+ }
2798
3736
  const handoffPosture = readTaskC1Posture(this.store, task);
3737
+ const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
3738
+ const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
3739
+ ? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
3740
+ : null;
2799
3741
  const strictStepReviewStep = planRiskAudit
2800
3742
  ? task.steps.find((step) => {
2801
3743
  if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
@@ -2806,6 +3748,11 @@ export class WorkflowService {
2806
3748
  return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
2807
3749
  }) ?? null
2808
3750
  : null;
3751
+ const strictReviewInfrastructureFailures = strictStepReviewStep
3752
+ ? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
3753
+ : 0;
3754
+ const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
3755
+ || (task.status === 'blocked' && task.review.status === 'unverified');
2809
3756
  const handoffOverlay = handoffPosture.state === 'pending'
2810
3757
  ? {
2811
3758
  action: 'task claim',
@@ -2814,6 +3761,15 @@ export class WorkflowService {
2814
3761
  state: 'pending',
2815
3762
  targetActor: handoffPosture.targetActor,
2816
3763
  eventId: handoffPosture.latestEvent.eventId,
3764
+ credentialRecovery: {
3765
+ action: 'task handoff-replace',
3766
+ when: 'claim credential is unavailable',
3767
+ requiredActor: handoffPosture.latestEvent.actor,
3768
+ targetActor: handoffPosture.targetActor,
3769
+ grantId: handoffPosture.latestEvent.grantId,
3770
+ expectedRevision: task.revision,
3771
+ invalidatesPriorCredential: true,
3772
+ },
2817
3773
  },
2818
3774
  }
2819
3775
  : handoffPosture.state === 'claimed'
@@ -2836,7 +3792,97 @@ export class WorkflowService {
2836
3792
  },
2837
3793
  }
2838
3794
  : {};
3795
+ const correctiveReplanOverlay = (posture) => {
3796
+ const evaluation = evaluateTaskCorrectiveReplanAvailability(task, posture);
3797
+ const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
3798
+ const command = handoffPosture.state === 'claimed'
3799
+ ? 'task corrective-yield'
3800
+ : writerLease
3801
+ ? 'doctor'
3802
+ : 'task corrective-replan-prepare';
3803
+ if (evaluation.allowed && handoffPosture.state === 'none' && writerLease) {
3804
+ const navigation = correctiveReplanNavigation(evaluation);
3805
+ return {
3806
+ action: command,
3807
+ blockedAction: 'task corrective-replan-prepare',
3808
+ stepId: posture.stepId,
3809
+ navigation: {
3810
+ kind: 'blocked',
3811
+ transitionId: navigation.transitionId,
3812
+ semanticFingerprint: navigation.semanticFingerprint,
3813
+ command: navigation.command,
3814
+ blockingGuardIds: ['task.corrective-replan.no-writer-lease'],
3815
+ pendingInputGuardIds: navigation.pendingInputGuardIds,
3816
+ reasonCode: 'ACTIVE_WRITER_LEASE',
3817
+ },
3818
+ correctiveDecisionGate: {
3819
+ attemptCount: posture.attemptCount,
3820
+ decision: posture.correctiveDecision?.decision ?? null,
3821
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3822
+ requiredAction: 'The active non-C1 writer lease must expire and be explicitly repaired before corrective replan preparation.',
3823
+ writerLease: {
3824
+ owner: writerLease.owner,
3825
+ expiresAt: writerLease.expiresAt,
3826
+ },
3827
+ },
3828
+ };
3829
+ }
3830
+ return evaluation.allowed
3831
+ ? {
3832
+ action: command,
3833
+ blockedAction: 'task plan-set',
3834
+ stepId: posture.stepId,
3835
+ navigation: { ...correctiveReplanNavigation(evaluation), command },
3836
+ correctiveDecisionGate: {
3837
+ attemptCount: posture.attemptCount,
3838
+ decision: posture.correctiveDecision?.decision ?? null,
3839
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3840
+ requiredAction: handoffPosture.state === 'claimed'
3841
+ ? 'Yield the exact C1 claim and writer lease before preparing replacement authority.'
3842
+ : 'Prepare a state-bound candidate, confirm the Human gate, then execute its exact receipt.',
3843
+ },
3844
+ }
3845
+ : {
3846
+ action: 'doctor',
3847
+ blockedAction: 'task plan-set',
3848
+ stepId: posture.stepId,
3849
+ navigation: correctiveReplanBlockedNavigation(evaluation),
3850
+ };
3851
+ };
2839
3852
  let guardedCorrectiveOverlay = {};
3853
+ if (planIntegrityAssessment?.applicable) {
3854
+ const requiredActor = handoffPosture.state === 'claimed'
3855
+ ? handoffPosture.claimant
3856
+ : defaultPlanIntegrityRecoveryActor(task.id);
3857
+ guardedCorrectiveOverlay = planIntegrityAssessment.eligible
3858
+ ? {
3859
+ action: 'task plan-integrity-recover',
3860
+ blockedAction: 'task run',
3861
+ stepId: planIntegrityAssessment.stepId,
3862
+ planIntegrityRecovery: {
3863
+ state: 'eligible',
3864
+ reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
3865
+ expectedRevision: task.revision,
3866
+ requiredActor,
3867
+ evidence: planIntegrityAssessment.evidence,
3868
+ boundedEffect: 'record-replan-required-decision',
3869
+ preservesWorktree: true,
3870
+ },
3871
+ }
3872
+ : {
3873
+ action: 'doctor',
3874
+ blockedAction: 'task plan-integrity-recover',
3875
+ stepId: planIntegrityAssessment.stepId,
3876
+ planIntegrityRecovery: {
3877
+ state: 'blocked',
3878
+ reasonCode: 'PLAN_INTEGRITY_RECOVERY_BLOCKED',
3879
+ expectedRevision: task.revision,
3880
+ requiredActor,
3881
+ blockers: planIntegrityAssessment.blockers,
3882
+ evidence: planIntegrityAssessment.evidence,
3883
+ },
3884
+ };
3885
+ }
2840
3886
  if (remediationModeRecovery) {
2841
3887
  guardedCorrectiveOverlay = {
2842
3888
  action: 'task remediation-mode-recover',
@@ -2854,7 +3900,12 @@ export class WorkflowService {
2854
3900
  },
2855
3901
  };
2856
3902
  }
2857
- if (!remediationModeRecovery && !strictStepReviewStep && guardedCorrectivePosture) {
3903
+ if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
3904
+ guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
3905
+ }
3906
+ if (!remediationModeRecovery
3907
+ && guardedCorrectivePosture
3908
+ && (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
2858
3909
  switch (guardedCorrectivePosture.posture) {
2859
3910
  case 'decision-required':
2860
3911
  if (guardedCorrectivePosture.correctiveDecision
@@ -2929,28 +3980,35 @@ export class WorkflowService {
2929
3980
  };
2930
3981
  break;
2931
3982
  case 'replan-required':
2932
- guardedCorrectiveOverlay = {
2933
- action: 'task plan-set',
2934
- stepId: guardedCorrectivePosture.stepId,
2935
- correctiveDecisionGate: {
2936
- attemptCount: guardedCorrectivePosture.attemptCount,
2937
- decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2938
- decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2939
- requiredAction: 'Record an actual new Plan posture before another remediation run.',
2940
- },
2941
- };
3983
+ guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
2942
3984
  break;
2943
3985
  case 'split-required':
2944
3986
  {
2945
3987
  const replacementDiscoveries = this.store.listDiscoveries(projectId)
2946
3988
  .filter((discovery) => discovery.status === 'ready_to_materialize');
2947
- const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
2948
3989
  const replacementMilestone = task.milestoneId
2949
3990
  ? this.store.readMilestone(projectId, task.milestoneId)
2950
3991
  : null;
3992
+ const incidentDependencies = replacementMilestone
3993
+ ? replacementMilestone.memberships.flatMap((membership) => {
3994
+ if (!Array.isArray(membership.dependsOnTaskIds))
3995
+ return [];
3996
+ return [
3997
+ ...(membership.taskId === task.id
3998
+ ? membership.dependsOnTaskIds.map((predecessorTaskId) => ({
3999
+ predecessorTaskId,
4000
+ consumerTaskId: membership.taskId,
4001
+ }))
4002
+ : []),
4003
+ ...(membership.dependsOnTaskIds.includes(task.id)
4004
+ ? [{ predecessorTaskId: task.id, consumerTaskId: membership.taskId }]
4005
+ : []),
4006
+ ];
4007
+ })
4008
+ : [];
2951
4009
  guardedCorrectiveOverlay = {
2952
- action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
2953
- ...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
4010
+ action: 'doctor',
4011
+ blockedAction: 'task replacement-materialize',
2954
4012
  stepId: guardedCorrectivePosture.stepId,
2955
4013
  correctiveDecisionGate: {
2956
4014
  attemptCount: guardedCorrectivePosture.attemptCount,
@@ -2964,29 +4022,17 @@ export class WorkflowService {
2964
4022
  requiredDiscoveryStatus: 'ready_to_materialize',
2965
4023
  oldMembershipDisposition: 'cancelled',
2966
4024
  replacementMembershipDisposition: 'required',
2967
- transaction: 'journal-recoverable',
4025
+ transaction: 'disabled',
4026
+ retainedImplementation: 'journal-recoverable-1-to-1',
4027
+ diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
4028
+ structuralReplacementAvailable: false,
4029
+ incidentDependencies,
2968
4030
  discoveryCandidates: replacementDiscoveries.map((discovery) => ({
2969
4031
  id: discovery.id,
2970
4032
  revision: discovery.revision,
2971
4033
  goal: discovery.goal,
2972
4034
  })),
2973
- ...(replacementDiscovery && replacementMilestone
2974
- ? {
2975
- exactTransition: {
2976
- command: 'task replacement-materialize',
2977
- taskId: task.id,
2978
- expectedTaskRevision: task.revision,
2979
- discoveryId: replacementDiscovery.id,
2980
- expectedDiscoveryRevision: replacementDiscovery.revision,
2981
- milestoneId: replacementMilestone.id,
2982
- expectedMilestoneRevision: replacementMilestone.revision,
2983
- },
2984
- }
2985
- : {
2986
- requiredAction: replacementDiscoveries.length === 0
2987
- ? 'Create and complete one thin replacement Discovery, then run next again.'
2988
- : 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
2989
- }),
4035
+ requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
2990
4036
  },
2991
4037
  };
2992
4038
  }
@@ -3022,6 +4068,30 @@ export class WorkflowService {
3022
4068
  strictStepReview: {
3023
4069
  reviewRequired: true,
3024
4070
  completionCommit: strictStepReviewStep.evidence.commitSha,
4071
+ infrastructureFailures: strictReviewInfrastructureFailures,
4072
+ maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
4073
+ recommendedMode: 'external-sealed',
4074
+ externalSealedReview: {
4075
+ packetCommand: 'task step-review-packet',
4076
+ recordCommand: 'task step-review-record',
4077
+ reviewerMustUseSeparateChat: true,
4078
+ repositorySealRequired: true,
4079
+ },
4080
+ },
4081
+ }
4082
+ : {}),
4083
+ ...(strictTaskReviewRequired
4084
+ ? {
4085
+ strictTaskReview: {
4086
+ reviewRequired: true,
4087
+ localInfrastructureFailureObserved: task.review.status === 'unverified',
4088
+ recommendedMode: 'external-sealed',
4089
+ externalSealedReview: {
4090
+ packetCommand: 'task review-packet',
4091
+ recordCommand: 'task review-sealed-record',
4092
+ reviewerMustUseSeparateChat: true,
4093
+ repositorySealRequired: true,
4094
+ },
3025
4095
  },
3026
4096
  }
3027
4097
  : {}),
@@ -3048,16 +4118,41 @@ export class WorkflowService {
3048
4118
  }
3049
4119
  : {}),
3050
4120
  ...guardedCorrectiveOverlay,
4121
+ ...mechanicalFeasibilityOverlay,
3051
4122
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3052
4123
  };
3053
4124
  if (handoffPosture.state === 'pending') {
3054
- return {
4125
+ return this.withTaskNavigationContracts(task, {
3055
4126
  ...result,
3056
4127
  action: 'task claim',
3057
4128
  blockedAction: next.action,
3058
- };
4129
+ });
3059
4130
  }
3060
- return result;
4131
+ const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
4132
+ .includes(String(result.action))
4133
+ ? {
4134
+ ...result,
4135
+ taskPlanContract: this.taskPlanContract(projectId, task),
4136
+ }
4137
+ : result;
4138
+ return this.withTaskNavigationContracts(task, navigableResult);
4139
+ }
4140
+ taskPlanContract(projectId, task) {
4141
+ const map = this.store.readKnowledgeMap(projectId);
4142
+ return {
4143
+ requiredRequirementIds: [...task.requirements],
4144
+ requiredAcceptanceIds: [...task.acceptance],
4145
+ currentRisks: [...task.risks],
4146
+ exactInclusionRequired: true,
4147
+ briefHash: task.briefHash,
4148
+ knowledgeMap: knowledgeSummary(map),
4149
+ planRiskAuditContract: {
4150
+ required: true,
4151
+ option: '--risk-audit-file',
4152
+ atomicWithPlanSet: true,
4153
+ plannerAuditorSeparationRequired: true,
4154
+ },
4155
+ };
3061
4156
  }
3062
4157
  assertTaskContextChangeAllowed(task) {
3063
4158
  const posture = this.taskTerminalCorrectivePosture(task);
@@ -3136,6 +4231,23 @@ export class WorkflowService {
3136
4231
  policyHash: grant.policyHash,
3137
4232
  }));
3138
4233
  }
4234
+ findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
4235
+ const tasksById = new Map(tasks.map((task) => [task.id, task]));
4236
+ for (const task of tasks) {
4237
+ if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
4238
+ || !task.planHash
4239
+ || !taskCanRebindKnowledge(task)
4240
+ || this.taskHasTerminalCorrectiveDecision(task))
4241
+ continue;
4242
+ const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
4243
+ if (!assessment.eligible)
4244
+ continue;
4245
+ const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
4246
+ if (options.length > 0)
4247
+ return { task, assessment, options };
4248
+ }
4249
+ return null;
4250
+ }
3139
4251
  assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
3140
4252
  const grant = this.store.readDelegation(projectId, grantId);
3141
4253
  if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
@@ -3161,7 +4273,7 @@ export class WorkflowService {
3161
4273
  if (policy.scope.kind === 'milestone')
3162
4274
  this.store.readMilestone(projectId, policy.scope.id);
3163
4275
  }
3164
- assertExecutionAuthorizationFresh(task) {
4276
+ assertExecutionAuthorizationFresh(task, repositoryRoot) {
3165
4277
  const root = this.store.taskRoot(task.projectId, task.id);
3166
4278
  const authorization = [...task.authorizations]
3167
4279
  .reverse()
@@ -3170,14 +4282,35 @@ export class WorkflowService {
3170
4282
  throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is missing.');
3171
4283
  const briefHash = this.store.hashArtifact(root, 'brief.md');
3172
4284
  const planHash = this.store.hashArtifact(root, 'plan.md');
3173
- if (authorization.briefHash !== briefHash || authorization.planHash !== planHash) {
4285
+ if (authorization.briefHash !== briefHash
4286
+ || authorization.planHash !== planHash
4287
+ || task.briefHash !== briefHash
4288
+ || task.planHash !== planHash) {
3174
4289
  throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is stale.', {
3175
4290
  authorizedBriefHash: authorization.briefHash,
3176
4291
  currentBriefHash: briefHash,
3177
4292
  authorizedPlanHash: authorization.planHash,
3178
4293
  currentPlanHash: planHash,
4294
+ stateBriefHash: task.briefHash,
4295
+ statePlanHash: task.planHash,
3179
4296
  });
3180
4297
  }
4298
+ const mechanicalFeasibility = authorization.mechanicalFeasibility;
4299
+ if (!mechanicalFeasibility)
4300
+ return authorization;
4301
+ const evidenceHash = hashMechanicalFeasibilityEvidence(mechanicalFeasibility);
4302
+ if (authorization.evidenceHash !== evidenceHash
4303
+ || authorization.headCommit !== mechanicalFeasibility.evaluatedHead) {
4304
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical-feasibility evidence does not match its authorization event.', {
4305
+ taskId: task.id,
4306
+ authorizedEvidenceHash: authorization.evidenceHash,
4307
+ currentEvidenceHash: evidenceHash,
4308
+ authorizedHeadCommit: authorization.headCommit,
4309
+ evaluatedHead: mechanicalFeasibility.evaluatedHead,
4310
+ });
4311
+ }
4312
+ assertMechanicalFeasibilityFresh(repositoryRoot, task, mechanicalFeasibility, briefHash, planHash);
4313
+ return authorization;
3181
4314
  }
3182
4315
  assertMilestoneAuthorizationFresh(milestone) {
3183
4316
  const authorization = [...milestone.authorizations]
@@ -3205,6 +4338,130 @@ export class WorkflowService {
3205
4338
  ? milestone
3206
4339
  : readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
3207
4340
  }
4341
+ listMilestonesObserved(projectId, milestones, posture) {
4342
+ if (!posture)
4343
+ return milestones;
4344
+ const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
4345
+ return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
4346
+ ? milestone
4347
+ : inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
4348
+ }
4349
+ taskGitNavigationOverride(task) {
4350
+ if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
4351
+ return null;
4352
+ }
4353
+ const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
4354
+ if (currentBase === task.baseCommit)
4355
+ return null;
4356
+ return {
4357
+ scope: 'task',
4358
+ id: task.id,
4359
+ status: task.status,
4360
+ action: 'task sync-base',
4361
+ revision: task.revision,
4362
+ baseSyncRequired: {
4363
+ baseBranch: task.baseBranch,
4364
+ recordedBaseCommit: task.baseCommit,
4365
+ currentBaseCommit: currentBase,
4366
+ },
4367
+ };
4368
+ }
4369
+ inspectDependencyProvenanceCandidate(snapshot, task) {
4370
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4371
+ const now = this.now().getTime();
4372
+ return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
4373
+ activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
4374
+ staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
4375
+ pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
4376
+ + snapshot.observation.pendingMilestoneTransactions.length
4377
+ + snapshot.observation.pendingTaskTransactions.length,
4378
+ corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
4379
+ coreOperationCount: snapshot.observation.coreTaskOperations.length,
4380
+ verifiedHistoricalStepCommits: verifiedHistoricalStepReviewCommits(this.store, task),
4381
+ versions: {
4382
+ runtime: versions.runtime,
4383
+ declared: versions.declared,
4384
+ locked: versions.locked,
4385
+ installed: versions.installed,
4386
+ currentBranch: versions.currentBranch,
4387
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4388
+ },
4389
+ });
4390
+ }
4391
+ withTaskNavigationContracts(task, next) {
4392
+ const action = typeof next.action === 'string' ? next.action : null;
4393
+ if (!action)
4394
+ return next;
4395
+ const policy = gitMutationPolicyForAction(task, action);
4396
+ const c1Posture = readTaskC1Posture(this.store, task);
4397
+ const claimedC1Posture = c1Posture.state === 'claimed';
4398
+ const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
4399
+ const activeLease = writerTokenContract
4400
+ ? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
4401
+ : null;
4402
+ const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
4403
+ const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
4404
+ ? {
4405
+ action: 'task writer-credential-replace',
4406
+ when: 'the active writer credential is unavailable',
4407
+ requiredActor: writerCredentialRecoveryActor,
4408
+ expectedRevision: task.revision,
4409
+ invalidatesPriorCredential: true,
4410
+ acquiresLeaseWhenAbsent: true,
4411
+ }
4412
+ : null;
4413
+ return {
4414
+ ...next,
4415
+ ...(policy ? { gitMutationPolicy: policy } : {}),
4416
+ ...(writerTokenContract ? { writerTokenContract } : {}),
4417
+ ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
4418
+ };
4419
+ }
4420
+ writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
4421
+ if (claimedActor)
4422
+ return claimedActor;
4423
+ if (activeLeaseOwner)
4424
+ return activeLeaseOwner;
4425
+ const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
4426
+ ? 'final_acceptance'
4427
+ : ['task step-complete', 'task submit'].includes(action)
4428
+ ? 'execution'
4429
+ : null;
4430
+ if (!authorizationKind)
4431
+ return null;
4432
+ return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
4433
+ }
4434
+ describeMilestoneInitialAssembly(projectId, milestone, tasks) {
4435
+ const assessment = assessMilestoneInitialAssembly({
4436
+ store: this.store,
4437
+ projectId,
4438
+ milestone,
4439
+ listTasks: () => tasks,
4440
+ });
4441
+ if (!assessment.eligible)
4442
+ return null;
4443
+ const options = [
4444
+ {
4445
+ action: 'linked-task-assembly',
4446
+ commands: ['discovery start', 'discovery materialize'],
4447
+ description: 'Start or materialize the next linked Task Discovery for this Milestone before closing the initial membership Plan.',
4448
+ },
4449
+ ];
4450
+ if (assessment.orphanTaskIds.length > 0) {
4451
+ options.push({
4452
+ action: 'milestone plan-set',
4453
+ commands: ['milestone plan-set'],
4454
+ description: 'Record one complete initial Milestone Plan that classifies every currently linked Task.',
4455
+ });
4456
+ }
4457
+ return {
4458
+ milestoneId: milestone.id,
4459
+ revision: milestone.revision,
4460
+ linkedTaskIds: assessment.orphanTaskIds,
4461
+ executionAvailable: false,
4462
+ options,
4463
+ };
4464
+ }
3208
4465
  prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
3209
4466
  validateMilestonePlan(plan);
3210
4467
  return prepareMilestoneScopeChangeCandidate({
@@ -3214,6 +4471,12 @@ export class WorkflowService {
3214
4471
  actor,
3215
4472
  });
3216
4473
  }
4474
+ publicWriterLeaseDiagnostic(lease) {
4475
+ return {
4476
+ ...toPublicWriterLease(lease, this.now()),
4477
+ tokenFingerprint: createHash('sha256').update(lease.token).digest('hex').slice(0, 16),
4478
+ };
4479
+ }
3217
4480
  proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
3218
4481
  const currentLease = context.locks.inspect(taskId);
3219
4482
  if (!currentLease) {
@@ -3303,9 +4566,12 @@ export class WorkflowService {
3303
4566
  storedKnowledgeMapHash: storedHash,
3304
4567
  });
3305
4568
  }
3306
- inspectAdoptionPreparation(identity, tasks, milestones) {
3307
- const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
3308
- const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
4569
+ inspectAdoptionPreparation(identity, tasks, milestones, observedLeases, observedPosture) {
4570
+ const leases = observedLeases
4571
+ ?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
4572
+ const currentPosture = observedPosture === undefined
4573
+ ? readCurrentAdoptionPosture(this.store, identity.projectId)
4574
+ : observedPosture;
3309
4575
  if (currentPosture) {
3310
4576
  assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
3311
4577
  }
@@ -3318,7 +4584,7 @@ export class WorkflowService {
3318
4584
  tasks,
3319
4585
  milestones,
3320
4586
  sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
3321
- blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
4587
+ blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, leases),
3322
4588
  now: this.now(),
3323
4589
  }, currentPosture);
3324
4590
  }
@@ -3765,12 +5031,25 @@ function validateMilestonePlan(plan) {
3765
5031
  if (plan.acceptance.length === 0 || plan.acceptance.some((value) => !value.trim())) {
3766
5032
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone acceptance must contain substantive entries.');
3767
5033
  }
3768
- if (plan.memberships.length === 0) {
5034
+ if (!Array.isArray(plan.memberships) || plan.memberships.length === 0) {
3769
5035
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone requires at least one Task membership.');
3770
5036
  }
3771
5037
  for (const membership of plan.memberships) {
3772
- if (!membership.taskId.trim())
5038
+ if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
5039
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.');
5040
+ }
5041
+ if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
3773
5042
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires Task ID.');
5043
+ }
5044
+ if (typeof membership.reason !== 'string') {
5045
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.');
5046
+ }
5047
+ if (!Array.isArray(membership.dependsOnTaskIds)) {
5048
+ throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.');
5049
+ }
5050
+ if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
5051
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.');
5052
+ }
3774
5053
  if (membership.disposition !== 'required' && !membership.reason.trim()) {
3775
5054
  throw new WorkflowError('INVALID_ARGUMENT', `Milestone ${membership.disposition} membership requires a reason.`);
3776
5055
  }
@@ -3800,7 +5079,7 @@ function taskCanRebindKnowledge(task) {
3800
5079
  'awaiting_final_acceptance',
3801
5080
  ].includes(task.status);
3802
5081
  }
3803
- function taskIsActionableFromRepositoryNext(task, milestonesById) {
5082
+ function taskIsActionableFromRepositoryNext(task, milestonesById, tasksById) {
3804
5083
  if (task.status === 'merged' || task.status === 'cancelled')
3805
5084
  return false;
3806
5085
  if (!task.milestoneId)
@@ -3808,10 +5087,18 @@ function taskIsActionableFromRepositoryNext(task, milestonesById) {
3808
5087
  const milestone = milestonesById.get(task.milestoneId);
3809
5088
  if (!milestone)
3810
5089
  return false;
3811
- if (taskHasStarted(task))
3812
- return !['accepted', 'cancelled'].includes(milestone.status);
3813
- const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
3814
- return milestone.status === 'active' && membership?.disposition === 'required';
5090
+ if (milestone.status !== 'active')
5091
+ return false;
5092
+ const assessment = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
5093
+ const candidate = tasksById.get(candidateTaskId);
5094
+ if (!candidate)
5095
+ throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
5096
+ return candidate;
5097
+ });
5098
+ return assessment.runnable;
5099
+ }
5100
+ function structuralReplacementIsDisabled() {
5101
+ return true;
3815
5102
  }
3816
5103
  function milestoneFinalAcceptanceGate(milestone, headCommit) {
3817
5104
  if (!milestone.resultHash || !milestone.evidenceHash) {
@@ -3860,13 +5147,13 @@ function nextForTask(task) {
3860
5147
  awaiting_execution_authorization: 'task authorize',
3861
5148
  ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
3862
5149
  in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
3863
- validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
5150
+ validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
3864
5151
  needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
3865
5152
  awaiting_final_acceptance: 'task accept',
3866
5153
  merging: 'task merge',
3867
5154
  ready_to_merge: 'task merge-confirm',
3868
5155
  merged: 'none',
3869
- blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
5156
+ blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
3870
5157
  cancelled: 'none',
3871
5158
  };
3872
5159
  const selectedStep = activeStep ?? failedStep ?? runnableStep;
@@ -3888,6 +5175,96 @@ function nextForTask(task) {
3888
5175
  : {}),
3889
5176
  };
3890
5177
  }
5178
+ function gitMutationPolicyForAction(task, action) {
5179
+ const metadataWrites = ['.git/index', '.git/index.lock', '.git/HEAD', '.git/refs', '.git/logs'];
5180
+ switch (action) {
5181
+ case 'task start':
5182
+ return {
5183
+ permission: 'git-metadata-write',
5184
+ requiredBeforeFirstCall: true,
5185
+ boundedToTransition: true,
5186
+ transition: 'task start',
5187
+ appliesWhen: { workspaceOwner: 'local' },
5188
+ metadataWrites,
5189
+ };
5190
+ case 'task step-complete':
5191
+ return {
5192
+ permission: 'git-metadata-write',
5193
+ requiredBeforeFirstCall: true,
5194
+ boundedToTransition: true,
5195
+ transition: 'task step-complete',
5196
+ metadataWrites,
5197
+ };
5198
+ case 'task merge':
5199
+ if (task.workspaceOwner !== 'local')
5200
+ return null;
5201
+ return {
5202
+ permission: 'git-metadata-write',
5203
+ requiredBeforeFirstCall: true,
5204
+ boundedToTransition: true,
5205
+ transition: 'task merge',
5206
+ metadataWrites,
5207
+ };
5208
+ case 'task sync-base':
5209
+ return {
5210
+ permission: 'git-metadata-write',
5211
+ requiredBeforeFirstCall: true,
5212
+ boundedToTransition: true,
5213
+ transition: 'task sync-base',
5214
+ metadataWrites,
5215
+ };
5216
+ default:
5217
+ return null;
5218
+ }
5219
+ }
5220
+ function writerTokenContractForAction(action, claimedC1Posture = false) {
5221
+ const alwaysLeaseBound = ['task step-complete', 'task submit', 'task merge', 'task merge-confirm'];
5222
+ const claimedC1LeaseBound = [
5223
+ 'task run',
5224
+ 'task step-review',
5225
+ 'task step-review-record',
5226
+ 'task review-record',
5227
+ 'task review-launch',
5228
+ 'task review-sealed-record',
5229
+ 'task corrective-decision-recover',
5230
+ 'task remediation-mode-recover',
5231
+ 'task plan-integrity-recover',
5232
+ 'task corrective-decision',
5233
+ 'task corrective-yield',
5234
+ 'task handoff',
5235
+ 'task handoff-prepare',
5236
+ 'task handback',
5237
+ 'task handback-create',
5238
+ ];
5239
+ if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
5240
+ return null;
5241
+ }
5242
+ return {
5243
+ required: true,
5244
+ option: '--writer-token',
5245
+ source: 'active-writer-lease',
5246
+ sensitive: true,
5247
+ renewalDisposition: 'reuse-presented-token',
5248
+ tokenReissuedOnRenewal: false,
5249
+ };
5250
+ }
5251
+ function observationPreflightBlockers(observation) {
5252
+ const messages = {
5253
+ PROJECT_REGISTRATION_MISSING: 'Workflow project registration is missing.',
5254
+ PROJECT_REPOSITORY_ROOT_UNBOUND: 'The current repository root is not bound to the Workflow project.',
5255
+ PROJECT_RECOVERY_REQUIRED: 'A project transaction requires explicit recovery.',
5256
+ MILESTONE_RECOVERY_REQUIRED: 'A Milestone transaction requires explicit recovery.',
5257
+ TASK_RECOVERY_REQUIRED: 'A Task corrective-replan transaction requires explicit recovery.',
5258
+ TASK_TRANSACTION_INTEGRITY_FAILED: 'A Task transaction journal failed integrity validation.',
5259
+ TASK_OPERATION_IN_PROGRESS: 'A Core Task operation is in progress.',
5260
+ STALE_TASK_OPERATION: 'A stale Core Task operation requires bounded repair.',
5261
+ ADOPTION_MISSING: 'Workflow lifecycle adoption is missing.',
5262
+ };
5263
+ return observation.reasonCodes.flatMap((reasonCode) => {
5264
+ const message = messages[reasonCode];
5265
+ return message ? [message] : [];
5266
+ });
5267
+ }
3891
5268
  function knowledgeSummary(map) {
3892
5269
  return {
3893
5270
  revision: map.revision,