codex-workflow-v2 2.0.0-alpha.7.2.1 → 2.0.0-beta.10

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 (164) hide show
  1. package/README.md +63 -7
  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 +23 -0
  5. package/dist/src/alpha6/handoff.js +221 -28
  6. package/dist/src/alpha6/handoff.js.map +1 -1
  7. package/dist/src/alpha6/milestone.d.ts +29 -0
  8. package/dist/src/alpha6/milestone.js +167 -1
  9. package/dist/src/alpha6/milestone.js.map +1 -1
  10. package/dist/src/alpha6/plan-risk.d.ts +1 -0
  11. package/dist/src/alpha6/plan-risk.js +15 -7
  12. package/dist/src/alpha6/plan-risk.js.map +1 -1
  13. package/dist/src/alpha6/remediation.js +18 -6
  14. package/dist/src/alpha6/remediation.js.map +1 -1
  15. package/dist/src/alpha6/review.d.ts +3 -0
  16. package/dist/src/alpha6/review.js +24 -0
  17. package/dist/src/alpha6/review.js.map +1 -1
  18. package/dist/src/beta1/project-transaction.d.ts +52 -0
  19. package/dist/src/beta1/project-transaction.js +297 -0
  20. package/dist/src/beta1/project-transaction.js.map +1 -0
  21. package/dist/src/cli.js +478 -32
  22. package/dist/src/cli.js.map +1 -1
  23. package/dist/src/contracts.d.ts +189 -5
  24. package/dist/src/contracts.js +1 -1
  25. package/dist/src/credential-output.d.ts +10 -0
  26. package/dist/src/credential-output.js +73 -0
  27. package/dist/src/credential-output.js.map +1 -0
  28. package/dist/src/dependency-provenance.d.ts +19 -0
  29. package/dist/src/dependency-provenance.js +185 -0
  30. package/dist/src/dependency-provenance.js.map +1 -0
  31. package/dist/src/diagnostics.d.ts +11 -0
  32. package/dist/src/diagnostics.js +54 -0
  33. package/dist/src/diagnostics.js.map +1 -1
  34. package/dist/src/errors.d.ts +1 -1
  35. package/dist/src/errors.js.map +1 -1
  36. package/dist/src/git.js +7 -9
  37. package/dist/src/git.js.map +1 -1
  38. package/dist/src/index.d.ts +1 -1
  39. package/dist/src/index.js +1 -1
  40. package/dist/src/index.js.map +1 -1
  41. package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
  42. package/dist/src/lifecycle/canonical-hash.js +24 -0
  43. package/dist/src/lifecycle/canonical-hash.js.map +1 -0
  44. package/dist/src/lifecycle/catalog.d.ts +26 -0
  45. package/dist/src/lifecycle/catalog.js +208 -0
  46. package/dist/src/lifecycle/catalog.js.map +1 -0
  47. package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
  48. package/dist/src/lifecycle/compiler-inspection.js +29 -0
  49. package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
  50. package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
  51. package/dist/src/lifecycle/core-static-readiness.js +130 -0
  52. package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
  53. package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
  54. package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
  55. package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
  56. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
  57. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
  58. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
  59. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
  60. package/dist/src/lifecycle/corrective-replan-credential-core.js +563 -0
  61. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
  62. package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
  63. package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
  64. package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
  65. package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
  66. package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
  67. package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
  68. package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
  69. package/dist/src/lifecycle/corrective-replan.js +548 -0
  70. package/dist/src/lifecycle/corrective-replan.js.map +1 -0
  71. package/dist/src/lifecycle/evaluator.d.ts +2 -0
  72. package/dist/src/lifecycle/evaluator.js +35 -0
  73. package/dist/src/lifecycle/evaluator.js.map +1 -0
  74. package/dist/src/lifecycle/fingerprint.d.ts +31 -0
  75. package/dist/src/lifecycle/fingerprint.js +388 -0
  76. package/dist/src/lifecycle/fingerprint.js.map +1 -0
  77. package/dist/src/lifecycle/immutable.d.ts +1 -0
  78. package/dist/src/lifecycle/immutable.js +10 -0
  79. package/dist/src/lifecycle/immutable.js.map +1 -0
  80. package/dist/src/lifecycle/implementation-table.d.ts +29 -0
  81. package/dist/src/lifecycle/implementation-table.js +87 -0
  82. package/dist/src/lifecycle/implementation-table.js.map +1 -0
  83. package/dist/src/lifecycle/index.d.ts +4 -0
  84. package/dist/src/lifecycle/index.js +5 -0
  85. package/dist/src/lifecycle/index.js.map +1 -0
  86. package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
  87. package/dist/src/lifecycle/schema-artifact.js +214 -0
  88. package/dist/src/lifecycle/schema-artifact.js.map +1 -0
  89. package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
  90. package/dist/src/lifecycle/semantic-registry.js +489 -0
  91. package/dist/src/lifecycle/semantic-registry.js.map +1 -0
  92. package/dist/src/lifecycle/types.d.ts +718 -0
  93. package/dist/src/lifecycle/types.js +4 -0
  94. package/dist/src/lifecycle/types.js.map +1 -0
  95. package/dist/src/observation.d.ts +20 -0
  96. package/dist/src/observation.js +171 -0
  97. package/dist/src/observation.js.map +1 -0
  98. package/dist/src/reviewer.d.ts +43 -1
  99. package/dist/src/reviewer.js +203 -50
  100. package/dist/src/reviewer.js.map +1 -1
  101. package/dist/src/state/corrective-replan-executor.d.ts +64 -0
  102. package/dist/src/state/corrective-replan-executor.js +608 -0
  103. package/dist/src/state/corrective-replan-executor.js.map +1 -0
  104. package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
  105. package/dist/src/state/corrective-replan-public-schema.js +43 -0
  106. package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
  107. package/dist/src/state/corrective-replan-public.d.ts +95 -0
  108. package/dist/src/state/corrective-replan-public.js +665 -0
  109. package/dist/src/state/corrective-replan-public.js.map +1 -0
  110. package/dist/src/state/corrective-replan-transaction.d.ts +160 -0
  111. package/dist/src/state/corrective-replan-transaction.js +730 -0
  112. package/dist/src/state/corrective-replan-transaction.js.map +1 -0
  113. package/dist/src/state/corrective-yield-executor.d.ts +31 -0
  114. package/dist/src/state/corrective-yield-executor.js +236 -0
  115. package/dist/src/state/corrective-yield-executor.js.map +1 -0
  116. package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
  117. package/dist/src/state/corrective-yield-transaction.js +591 -0
  118. package/dist/src/state/corrective-yield-transaction.js.map +1 -0
  119. package/dist/src/state/lock.d.ts +76 -3
  120. package/dist/src/state/lock.js +611 -30
  121. package/dist/src/state/lock.js.map +1 -1
  122. package/dist/src/state/store.d.ts +3 -0
  123. package/dist/src/state/store.js +65 -12
  124. package/dist/src/state/store.js.map +1 -1
  125. package/dist/src/transition-core.d.ts +67 -0
  126. package/dist/src/transition-core.js +318 -0
  127. package/dist/src/transition-core.js.map +1 -0
  128. package/dist/src/version.d.ts +1 -1
  129. package/dist/src/version.js +1 -1
  130. package/dist/src/version.js.map +1 -1
  131. package/dist/src/workflow.d.ts +93 -25
  132. package/dist/src/workflow.js +1712 -218
  133. package/dist/src/workflow.js.map +1 -1
  134. package/docs/autonomy-guardrails.md +43 -3
  135. package/docs/beta1-stabilization-brief.md +165 -0
  136. package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
  137. package/docs/delegated-approval.md +30 -4
  138. package/docs/development-flow.md +38 -6
  139. package/docs/lifecycle/state-machine-stabilization.md +641 -0
  140. package/docs/project-memory.md +7 -5
  141. package/docs/release.md +25 -3
  142. package/docs/split-required-recovery.md +54 -0
  143. package/docs/stable-release-defect-register.md +730 -0
  144. package/docs/updating-existing-project.md +59 -4
  145. package/docs/validation-report.md +85 -56
  146. package/package.json +6 -2
  147. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
  148. package/plugins/codex-workflow-gateway/references/protocol.md +80 -5
  149. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
  150. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +255 -3
  151. package/references/state-machine.md +4 -1
  152. package/roles/delivery-coordinator.md +39 -0
  153. package/schemas/adoption-posture-event.schema.json +1 -1
  154. package/schemas/context-envelope.schema.json +1 -1
  155. package/schemas/corrective-replan-credentials.private.schema.json +98 -0
  156. package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
  157. package/schemas/corrective-replan-public.schema.json +142 -0
  158. package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
  159. package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
  160. package/schemas/remediation-event.schema.json +11 -1
  161. package/schemas/task-handoff-event.schema.json +1 -1
  162. package/schemas/task.schema.json +25 -0
  163. package/schemas/transition-definition.schema.json +2609 -0
  164. package/schemas/transition-payloads.schema.json +140 -0
@@ -2,30 +2,40 @@ 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';
8
9
  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
- import { inspectLocalDependency } from './diagnostics.js';
12
- import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
12
+ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.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
16
  import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
- import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
- import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
17
+ import { assessMilestoneInitialAssembly, applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
18
+ import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
19
+ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
18
20
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
19
21
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
20
22
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
21
23
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
24
+ import { ensureDirectory } from './fs-utils.js';
22
25
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
23
26
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
24
- import { launchStrictStepReviewer } from './reviewer.js';
25
- import { WriterLockManager } from './state/lock.js';
27
+ import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
28
+ import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
26
29
  import { FileStateStore } from './state/store.js';
30
+ import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
31
+ import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
32
+ import { createCorrectiveYieldExecutor } from './state/corrective-yield-executor.js';
33
+ import { createCorrectiveReplanPublicController, } from './state/corrective-replan-public.js';
34
+ import { inspectWorkflowObservationBoundary, } from './observation.js';
27
35
  import { createEntityId } from './ulid.js';
28
- import { PACKAGE_VERSION } from './version.js';
36
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
37
+ import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
38
+ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
29
39
  export class WorkflowService {
30
40
  store;
31
41
  now;
@@ -33,11 +43,42 @@ export class WorkflowService {
33
43
  this.store = store;
34
44
  this.now = now;
35
45
  }
36
- context(repository) {
46
+ #mutationContext(repository) {
37
47
  const identity = resolveRepositoryIdentity(repository);
38
- const projectFile = path.join(this.store.projectRoot(identity.projectId), 'project.json');
48
+ const projectRoot = this.store.projectRoot(identity.projectId);
49
+ const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
50
+ const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
51
+ const taskTransactions = {
52
+ corrupt: [...replanTransactions.corrupt, ...yieldTransactions.corrupt],
53
+ pending: [...replanTransactions.pending, ...yieldTransactions.pending],
54
+ };
55
+ if (taskTransactions.corrupt.length > 0) {
56
+ throw new WorkflowError('STATE_CORRUPT', 'A Task transaction journal failed integrity validation.', {
57
+ transactions: taskTransactions.corrupt,
58
+ });
59
+ }
60
+ if (taskTransactions.pending.length > 0) {
61
+ throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'A Task transaction requires explicit recovery.', {
62
+ taskIds: [...new Set(taskTransactions.pending.map((transaction) => transaction.taskId))].sort(),
63
+ transactionIds: taskTransactions.pending.map((transaction) => transaction.transactionId).sort(),
64
+ });
65
+ }
66
+ const coreTaskOperations = listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root);
67
+ if (coreTaskOperations.length > 0) {
68
+ throw new WorkflowError('LOCKED', 'A Core Task operation owns the project mutation boundary.', {
69
+ taskIds: coreTaskOperations.map((operation) => operation.entityId).sort(),
70
+ operationIds: coreTaskOperations.map((operation) => operation.operationId).sort(),
71
+ });
72
+ }
73
+ recoverProjectTransactions(projectRoot, identity.projectId);
74
+ const projectFile = path.join(projectRoot, 'project.json');
39
75
  const projectAlreadyRegistered = existsSync(projectFile);
40
76
  if (!projectAlreadyRegistered) {
77
+ // The registration posture is a sidecar under the project root. A completely
78
+ // fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
79
+ // only that directory before writing the posture; project.json remains the
80
+ // final registration record written below.
81
+ ensureDirectory(projectRoot);
41
82
  initializeProjectRegistrationAdoption(this.store, {
42
83
  projectId: identity.projectId,
43
84
  repositoryIdentity: identity,
@@ -51,17 +92,103 @@ export class WorkflowService {
51
92
  return {
52
93
  identity,
53
94
  store: this.store,
54
- locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now),
95
+ locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root),
96
+ };
97
+ }
98
+ inspectWriterLease(repository, entityId) {
99
+ const identity = resolveRepositoryIdentity(repository);
100
+ const lease = inspectRawWriterLease(this.store.lockRoot(identity.projectId), entityId, this.store.root);
101
+ return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
102
+ }
103
+ repairStaleWriterLease(repository, entityId) {
104
+ const context = this.#mutationContext(repository);
105
+ const lease = context.locks.repairStale(entityId);
106
+ return {
107
+ ...this.publicWriterLeaseDiagnostic(lease),
108
+ repaired: true,
109
+ };
110
+ }
111
+ prepareCorrectiveReplan(repository, taskId, candidate) {
112
+ return this.correctiveReplanController(repository).prepare({ taskId, candidate });
113
+ }
114
+ confirmCorrectiveReplan(repository, taskId, candidate, gate, confirmationCode) {
115
+ return this.correctiveReplanController(repository).confirm({ taskId, candidate, gate, confirmationCode });
116
+ }
117
+ executeCorrectiveReplan(repository, taskId, candidate, credential) {
118
+ return this.correctiveReplanController(repository).execute({ taskId, candidate, credential });
119
+ }
120
+ recoverCorrectiveReplan(repository, taskId, transactionId) {
121
+ return this.correctiveReplanController(repository).recover({ taskId, transactionId });
122
+ }
123
+ executeCorrectiveYield(repository, taskId, expectedTaskRevision, actor, writerToken, reason) {
124
+ const identity = resolveRepositoryIdentity(repository);
125
+ return createCorrectiveYieldExecutor({
126
+ store: this.store,
127
+ projectId: identity.projectId,
128
+ now: this.now,
129
+ }).execute({
130
+ projectId: identity.projectId,
131
+ taskId,
132
+ expectedTaskRevision,
133
+ actor,
134
+ writerToken,
135
+ reason,
136
+ });
137
+ }
138
+ recoverCorrectiveYield(repository, taskId, transactionId) {
139
+ const identity = resolveRepositoryIdentity(repository);
140
+ return createCorrectiveYieldExecutor({
141
+ store: this.store,
142
+ projectId: identity.projectId,
143
+ now: this.now,
144
+ }).recover({ taskId, transactionId });
145
+ }
146
+ correctiveReplanController(repository) {
147
+ const identity = resolveRepositoryIdentity(repository);
148
+ return createCorrectiveReplanPublicController({
149
+ store: this.store,
150
+ projectId: identity.projectId,
151
+ repositoryRoot: identity.repositoryRoot,
152
+ now: this.now,
153
+ });
154
+ }
155
+ observationSnapshot(repository) {
156
+ const identity = resolveRepositoryIdentity(repository);
157
+ const boundary = inspectWorkflowObservationBoundary({
158
+ store: this.store,
159
+ identity,
160
+ now: this.now(),
161
+ });
162
+ if (boundary.observation.kind !== 'assessment') {
163
+ return {
164
+ ...boundary,
165
+ discoveries: [],
166
+ tasks: [],
167
+ milestones: [],
168
+ delegations: [],
169
+ adoption: null,
170
+ };
171
+ }
172
+ const tasks = this.store.listTasks(identity.projectId);
173
+ const rawMilestones = this.store.listMilestones(identity.projectId);
174
+ const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones, boundary.leases, boundary.currentAdoptionPosture);
175
+ return {
176
+ ...boundary,
177
+ discoveries: this.store.listDiscoveries(identity.projectId),
178
+ tasks,
179
+ milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
180
+ delegations: this.store.listDelegations(identity.projectId),
181
+ adoption,
55
182
  };
56
183
  }
57
184
  prepareDelegation(repository, input) {
58
- const { identity } = this.context(repository);
185
+ const { identity } = this.#mutationContext(repository);
59
186
  const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
60
187
  this.assertDelegationScopeExists(identity.projectId, gate.policy);
61
188
  return gate;
62
189
  }
63
190
  grantDelegation(repository, input, confirmationCode) {
64
- const { identity } = this.context(repository);
191
+ const { identity } = this.#mutationContext(repository);
65
192
  const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
66
193
  this.assertDelegationScopeExists(identity.projectId, gate.policy);
67
194
  if (confirmationCode.trim() !== gate.confirmationCode) {
@@ -97,7 +224,7 @@ export class WorkflowService {
97
224
  }, null);
98
225
  }
99
226
  revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
100
- const { identity } = this.context(repository);
227
+ const { identity } = this.#mutationContext(repository);
101
228
  const grant = this.store.readDelegation(identity.projectId, grantId);
102
229
  assertExpectedRevision(grant, expectedRevision);
103
230
  if (grant.status !== 'active') {
@@ -121,7 +248,7 @@ export class WorkflowService {
121
248
  startDiscovery(repository, input) {
122
249
  if (!input.goal.trim())
123
250
  throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
124
- const { identity } = this.context(repository);
251
+ const { identity } = this.#mutationContext(repository);
125
252
  const timestamp = this.now().toISOString();
126
253
  const initial = {
127
254
  schemaVersion: STATE_SCHEMA_VERSION,
@@ -149,7 +276,7 @@ export class WorkflowService {
149
276
  return this.store.writeDiscovery(initial, null);
150
277
  }
151
278
  updateDiscovery(repository, discoveryId, expectedRevision, patch) {
152
- const { identity } = this.context(repository);
279
+ const { identity } = this.#mutationContext(repository);
153
280
  const current = this.store.readDiscovery(identity.projectId, discoveryId);
154
281
  if (current.status === 'materialized' || current.status === 'abandoned') {
155
282
  throw new WorkflowError('TRANSITION_BLOCKED', `Discovery ${discoveryId} is ${current.status}.`);
@@ -173,7 +300,7 @@ export class WorkflowService {
173
300
  return this.store.writeDiscovery(updated, expectedRevision);
174
301
  }
175
302
  materializeTask(repository, discoveryId, expectedRevision, title, baseBranch = 'main', milestoneId = null) {
176
- const { identity } = this.context(repository);
303
+ const { identity } = this.#mutationContext(repository);
177
304
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
178
305
  assertExpectedRevision(discovery, expectedRevision);
179
306
  const readiness = assessDiscovery(discovery);
@@ -249,8 +376,259 @@ export class WorkflowService {
249
376
  const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
250
377
  return { discovery: savedDiscovery, task: savedTask, taskRoot };
251
378
  }
379
+ replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
380
+ const { identity, locks } = this.#mutationContext(repository);
381
+ const oldTask = this.store.readTask(identity.projectId, oldTaskId);
382
+ assertExpectedRevision(oldTask, expectedOldTaskRevision);
383
+ if (!oldTask.milestoneId) {
384
+ throw new WorkflowError('TRANSITION_BLOCKED', 'A split-required replacement requires a Milestone-linked Task.');
385
+ }
386
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, oldTask.milestoneId);
387
+ assertExpectedRevision(milestone, expectedMilestoneRevision);
388
+ assertMilestoneScopeChangeStatusAllowed(milestone);
389
+ const membership = milestone.memberships.find((candidate) => candidate.taskId === oldTask.id);
390
+ if (membership?.disposition !== 'required') {
391
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The terminal Task must still be a required Milestone member.', {
392
+ taskId: oldTask.id,
393
+ milestoneId: milestone.id,
394
+ disposition: membership?.disposition ?? null,
395
+ });
396
+ }
397
+ const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
398
+ if (!riskAudit)
399
+ throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
400
+ const posture = findCurrentGuardedRemediationPosture(this.store, oldTask, riskAudit);
401
+ if (posture?.posture !== 'split-required' || posture.correctiveDecision?.decision !== 'split-required') {
402
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task replacement is available only for the exact split-required posture.', {
403
+ taskId: oldTask.id,
404
+ posture: posture?.posture ?? null,
405
+ decision: posture?.correctiveDecision?.decision ?? null,
406
+ });
407
+ }
408
+ const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
409
+ assertExpectedRevision(discovery, expectedDiscoveryRevision);
410
+ const readiness = assessDiscovery(discovery);
411
+ if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
412
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Discovery is not ready to materialize.', {
413
+ discoveryId,
414
+ missing: readiness.missing,
415
+ });
416
+ }
417
+ const normalizedTitle = title.trim();
418
+ const normalizedActor = actor.trim();
419
+ if (!normalizedTitle)
420
+ throw new WorkflowError('INVALID_ARGUMENT', 'Replacement Task title is required.');
421
+ if (!normalizedActor)
422
+ throw new WorkflowError('INVALID_ARGUMENT', 'Replacement actor is required.');
423
+ let autonomyContractEventHash;
424
+ let activeAutonomy = null;
425
+ if (normalizedActor.startsWith('agent:')) {
426
+ activeAutonomy = requireActiveMilestoneAutonomy({
427
+ store: this.store,
428
+ projectId: identity.projectId,
429
+ milestone,
430
+ actor: normalizedActor,
431
+ now: this.now(),
432
+ });
433
+ autonomyContractEventHash = activeAutonomy.contract.eventHash;
434
+ }
435
+ const now = this.now();
436
+ const timestamp = now.toISOString();
437
+ const replacementTaskId = createEntityId('TASK', now.getTime());
438
+ const briefContent = renderBrief(discovery, normalizedTitle);
439
+ const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
440
+ const briefHash = sha256Hex(normalizedBriefContent);
441
+ const replacementTask = {
442
+ schemaVersion: STATE_SCHEMA_VERSION,
443
+ revision: 1,
444
+ id: replacementTaskId,
445
+ projectId: identity.projectId,
446
+ createdAt: timestamp,
447
+ updatedAt: timestamp,
448
+ kind: 'task',
449
+ status: 'planning',
450
+ title: normalizedTitle,
451
+ slug: slugify(normalizedTitle),
452
+ discoveryId,
453
+ milestoneId: milestone.id,
454
+ repositoryRoot: identity.repositoryRoot,
455
+ baseBranch: milestone.baseBranch,
456
+ baseCommit: null,
457
+ taskBranch: null,
458
+ workspaceOwner: null,
459
+ briefHash,
460
+ planHash: null,
461
+ planObjective: null,
462
+ resultHash: null,
463
+ evidenceHash: null,
464
+ requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
465
+ acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
466
+ risks: discovery.assumptions,
467
+ knowledgeImpact: null,
468
+ knowledgeImpactReason: null,
469
+ knowledgeTargets: [],
470
+ knowledgeMapRevision: null,
471
+ knowledgeMapHash: null,
472
+ graphUse: null,
473
+ steps: [],
474
+ authorizations: [],
475
+ knowledgeRebinds: [],
476
+ review: emptyReview(),
477
+ blockReason: null,
478
+ mergedCommit: null,
479
+ systemCommits: [],
480
+ replacementForTaskId: oldTask.id,
481
+ replacedByTaskId: null,
482
+ };
483
+ const cancelledOldTask = {
484
+ ...oldTask,
485
+ revision: oldTask.revision + 1,
486
+ updatedAt: timestamp,
487
+ status: 'cancelled',
488
+ blockReason: `Replaced after split-required by ${replacementTaskId}.`,
489
+ replacedByTaskId: replacementTaskId,
490
+ };
491
+ const consumedDiscovery = {
492
+ ...discovery,
493
+ revision: discovery.revision + 1,
494
+ updatedAt: timestamp,
495
+ status: 'materialized',
496
+ materializedEntityId: replacementTaskId,
497
+ };
498
+ const replacementReason = `Replacement for ${oldTask.id} after split-required decision ${posture.correctiveDecision.eventId}.`;
499
+ const plan = {
500
+ outcome: milestone.outcome,
501
+ successSignal: milestone.successSignal,
502
+ acceptance: milestone.acceptance,
503
+ checks: milestone.checks,
504
+ memberships: [
505
+ ...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
506
+ ? { ...candidate, disposition: 'cancelled', reason: replacementReason }
507
+ : candidate),
508
+ { taskId: replacementTaskId, disposition: 'required', reason: replacementReason },
509
+ ],
510
+ };
511
+ if (autonomyContractEventHash) {
512
+ const autonomy = requireActiveMilestoneAutonomy({
513
+ store: this.store,
514
+ projectId: identity.projectId,
515
+ milestone,
516
+ actor: normalizedActor,
517
+ now,
518
+ });
519
+ assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
520
+ }
521
+ const readCandidateTask = (taskId) => {
522
+ if (taskId === oldTask.id)
523
+ return cancelledOldTask;
524
+ if (taskId === replacementTaskId)
525
+ return replacementTask;
526
+ return this.store.readTask(identity.projectId, taskId);
527
+ };
528
+ const prepared = prepareMilestoneScopeChangeCandidate({
529
+ milestone,
530
+ plan,
531
+ readTask: readCandidateTask,
532
+ actor: normalizedActor,
533
+ ...(autonomyContractEventHash ? { autonomyContractEventHash } : {}),
534
+ });
535
+ const bundle = prepareMilestoneScopeChangeBundle({
536
+ store: this.store,
537
+ milestone,
538
+ prepared,
539
+ now,
540
+ readTask: readCandidateTask,
541
+ });
542
+ const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
543
+ const scannedKnowledgeMap = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, now);
544
+ const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
545
+ let refreshedKnowledgeMap = null;
546
+ if (activeAutonomy
547
+ && currentKnowledgeMap.status === 'active'
548
+ && currentKnowledgeMap.approval
549
+ && (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval)) {
550
+ const refreshAssessment = assessDelegatedKnowledgeRefresh(currentKnowledgeMap, inspectedKnowledgeMap, oldTask, oldTask.planHash
551
+ ? this.store.hashArtifact(this.store.taskRoot(identity.projectId, oldTask.id), 'plan.md')
552
+ : null);
553
+ if (refreshAssessment.eligible) {
554
+ if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
555
+ this.assertPlanBoundedSupportingSourceGrant(identity.projectId, activeAutonomy.grant.id, normalizedActor, oldTask);
556
+ }
557
+ const delegation = this.delegatedAuthorization(identity.projectId, activeAutonomy.grant.id, 'project_memory.approve', normalizedActor, inspectedKnowledgeMap, oldTask.id);
558
+ if (!delegation) {
559
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Knowledge refresh requires delegated approval evidence.');
560
+ }
561
+ const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
562
+ const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
563
+ if (!approved.approval) {
564
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement Knowledge refresh did not produce approval evidence.');
565
+ }
566
+ refreshedKnowledgeMap = {
567
+ ...approved,
568
+ revision: currentKnowledgeMap.revision + 2,
569
+ approval: {
570
+ ...approved.approval,
571
+ authorizationMode: 'delegated',
572
+ delegation,
573
+ },
574
+ };
575
+ }
576
+ }
577
+ const projectRoot = this.store.projectRoot(identity.projectId);
578
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
579
+ applyProjectTransaction({
580
+ projectRoot,
581
+ projectId: identity.projectId,
582
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
583
+ kind: 'split-required-task-replacement',
584
+ now,
585
+ targets: [
586
+ { path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')), content: `${JSON.stringify(consumedDiscovery, null, 2)}\n` },
587
+ { path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'brief.md')), content: normalizedBriefContent },
588
+ { path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'state.json')), content: `${JSON.stringify(replacementTask, null, 2)}\n` },
589
+ { path: relative(path.join(this.store.taskRoot(identity.projectId, oldTask.id), 'state.json')), content: `${JSON.stringify(cancelledOldTask, null, 2)}\n` },
590
+ { path: relative(path.join(this.store.lockRoot(identity.projectId), `${oldTask.id}.json`)), content: null },
591
+ ...(refreshedKnowledgeMap
592
+ ? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
593
+ : []),
594
+ { path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')), content: bundle.planContent },
595
+ { path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')), content: bundle.sidecarContent },
596
+ { path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')), content: bundle.stateContent },
597
+ ],
598
+ validate: () => {
599
+ const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
600
+ const savedOldTask = this.store.readTask(identity.projectId, oldTask.id);
601
+ const savedReplacement = this.store.readTask(identity.projectId, replacementTaskId);
602
+ const savedMilestone = this.store.readMilestone(identity.projectId, milestone.id);
603
+ if (savedDiscovery.materializedEntityId !== replacementTaskId
604
+ || savedOldTask.status !== 'cancelled'
605
+ || savedOldTask.replacedByTaskId !== replacementTaskId
606
+ || savedReplacement.replacementForTaskId !== oldTask.id) {
607
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction relationship validation failed.');
608
+ }
609
+ if (locks.inspect(oldTask.id)) {
610
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction retained authority for the cancelled Task.');
611
+ }
612
+ if (refreshedKnowledgeMap) {
613
+ const savedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
614
+ if (savedKnowledgeMap.revision !== refreshedKnowledgeMap.revision
615
+ || savedKnowledgeMap.status !== 'active'
616
+ || savedKnowledgeMap.approval?.authorizationMode !== 'delegated') {
617
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction Knowledge refresh validation failed.');
618
+ }
619
+ }
620
+ assertMilestoneMembershipIntegrity(savedMilestone, (taskId) => this.store.readTask(identity.projectId, taskId));
621
+ },
622
+ });
623
+ return {
624
+ oldTask: this.store.readTask(identity.projectId, oldTask.id),
625
+ replacementTask: this.store.readTask(identity.projectId, replacementTaskId),
626
+ discovery: this.store.readDiscovery(identity.projectId, discovery.id),
627
+ milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
628
+ };
629
+ }
252
630
  materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
253
- const { identity } = this.context(repository);
631
+ const { identity } = this.#mutationContext(repository);
254
632
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
255
633
  assertExpectedRevision(discovery, expectedRevision);
256
634
  const readiness = assessDiscovery(discovery);
@@ -298,7 +676,7 @@ export class WorkflowService {
298
676
  };
299
677
  }
300
678
  setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
301
- const { identity } = this.context(repository);
679
+ const { identity } = this.#mutationContext(repository);
302
680
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
303
681
  assertExpectedRevision(milestone, expectedRevision);
304
682
  if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
@@ -370,7 +748,7 @@ export class WorkflowService {
370
748
  return saved;
371
749
  }
372
750
  prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
373
- const { identity } = this.context(repository);
751
+ const { identity } = this.#mutationContext(repository);
374
752
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
375
753
  assertExpectedRevision(milestone, expectedRevision);
376
754
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -387,7 +765,7 @@ export class WorkflowService {
387
765
  };
388
766
  }
389
767
  applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
390
- const { identity } = this.context(repository);
768
+ const { identity } = this.#mutationContext(repository);
391
769
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
392
770
  assertExpectedRevision(milestone, expectedRevision);
393
771
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -410,9 +788,10 @@ export class WorkflowService {
410
788
  });
411
789
  }
412
790
  prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
413
- const { identity } = this.context(repository);
791
+ const { identity } = this.#mutationContext(repository);
414
792
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
415
793
  assertExpectedRevision(milestone, expectedRevision);
794
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
416
795
  return prepareMilestoneAutonomyContract({
417
796
  projectId: identity.projectId,
418
797
  milestone,
@@ -423,9 +802,10 @@ export class WorkflowService {
423
802
  });
424
803
  }
425
804
  grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
426
- const { identity } = this.context(repository);
805
+ const { identity } = this.#mutationContext(repository);
427
806
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
428
807
  assertExpectedRevision(milestone, expectedRevision);
808
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
429
809
  return applyMilestoneAutonomyContract({
430
810
  store: this.store,
431
811
  projectId: identity.projectId,
@@ -438,7 +818,7 @@ export class WorkflowService {
438
818
  });
439
819
  }
440
820
  evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
441
- const { identity } = this.context(repository);
821
+ const { identity } = this.#mutationContext(repository);
442
822
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
443
823
  assertExpectedRevision(milestone, expectedRevision);
444
824
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -468,12 +848,13 @@ export class WorkflowService {
468
848
  });
469
849
  }
470
850
  authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
471
- const { identity } = this.context(repository);
851
+ const { identity } = this.#mutationContext(repository);
472
852
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
473
853
  assertExpectedRevision(milestone, expectedRevision);
474
854
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
475
855
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
476
856
  }
857
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
477
858
  const root = this.store.milestoneRoot(identity.projectId, milestoneId);
478
859
  const planHash = this.store.hashArtifact(root, 'plan.json');
479
860
  if (planHash !== milestone.planHash) {
@@ -497,7 +878,7 @@ export class WorkflowService {
497
878
  return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
498
879
  }
499
880
  validateMilestone(repository, milestoneId, expectedRevision) {
500
- const { identity } = this.context(repository);
881
+ const { identity } = this.#mutationContext(repository);
501
882
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
502
883
  assertExpectedRevision(milestone, expectedRevision);
503
884
  if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
@@ -560,7 +941,7 @@ export class WorkflowService {
560
941
  return saved;
561
942
  }
562
943
  acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
563
- const { identity } = this.context(repository);
944
+ const { identity } = this.#mutationContext(repository);
564
945
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
565
946
  assertExpectedRevision(milestone, expectedRevision);
566
947
  if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
@@ -599,7 +980,7 @@ export class WorkflowService {
599
980
  resultHash,
600
981
  evidenceHash,
601
982
  headCommit: currentHead,
602
- reason: `Confirmed with ${gate.confirmationCode}.`,
983
+ reason: 'Milestone final acceptance confirmed against the current validated state.',
603
984
  authorizationMode: delegation ? 'delegated' : 'human',
604
985
  ...(delegation ? { delegation } : {}),
605
986
  };
@@ -615,7 +996,7 @@ export class WorkflowService {
615
996
  cancelMilestone(repository, milestoneId, expectedRevision, reason) {
616
997
  if (!reason.trim())
617
998
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
618
- const { identity } = this.context(repository);
999
+ const { identity } = this.#mutationContext(repository);
619
1000
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
620
1001
  assertExpectedRevision(milestone, expectedRevision);
621
1002
  if (['accepted', 'cancelled'].includes(milestone.status)) {
@@ -625,21 +1006,59 @@ export class WorkflowService {
625
1006
  }
626
1007
  setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
627
1008
  validatePlan(plan);
628
- const { identity } = this.context(repository);
1009
+ const { identity } = this.#mutationContext(repository);
629
1010
  const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
630
1011
  this.validateKnowledgeTargets(knowledgeMap, plan);
631
1012
  const task = this.store.readTask(identity.projectId, taskId);
632
1013
  assertExpectedRevision(task, expectedRevision);
633
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
634
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
635
- }
1014
+ const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
636
1015
  const currentPlanCorrectiveDecisions = task.planHash
637
- ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
1016
+ ? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
638
1017
  : [];
639
1018
  const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
640
- const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
1019
+ const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => {
1020
+ if (event.decision !== 'continue-fix')
1021
+ return false;
1022
+ const passedBoundary = latestPassedVerifiedStepReviewEvent(this.store, task, event.stepId);
1023
+ return !passedBoundary || passedBoundary.recordedAt <= event.recordedAt;
1024
+ });
641
1025
  const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
642
- const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
1026
+ const currentPlanRiskAudit = task.status === 'needs_fix' || currentReplanDecisions.length > 0
1027
+ ? readCurrentPlanRiskAudit(this.store, task)
1028
+ : null;
1029
+ const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
1030
+ ? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
1031
+ : null;
1032
+ const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
1033
+ ? classifiedCorrectivePosture
1034
+ : currentPlanRiskAudit
1035
+ ? identifyCorrectiveReplanPosture(task, currentPlanRiskAudit.reviewRequiredStepIds, correctiveDecisionEvents, readRemediationEvents(this.store, task))
1036
+ : null;
1037
+ const correctiveReplanAvailability = correctiveReplanPosture?.posture === 'replan-required'
1038
+ ? evaluateTaskCorrectiveReplanAvailability(task, correctiveReplanPosture)
1039
+ : null;
1040
+ if (currentReplanDecisions.length > 0) {
1041
+ if (!correctiveReplanAvailability?.allowed) {
1042
+ throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} is blocked for Task ${task.id}.`, {
1043
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1044
+ posture: correctiveReplanPosture?.posture ?? null,
1045
+ evaluationPhase: correctiveReplanAvailability?.phase ?? 'availability',
1046
+ blockingGuardIds: correctiveReplanAvailability?.blockingGuardIds ?? [],
1047
+ pendingGuardIds: correctiveReplanAvailability?.pendingGuardIds ?? [],
1048
+ guardResults: correctiveReplanAvailability?.guardResults ?? [],
1049
+ });
1050
+ }
1051
+ throw new WorkflowError('TRANSITION_BLOCKED', `Legacy task plan-set cannot execute ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID}.`, {
1052
+ taskId: task.id,
1053
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1054
+ requiredAction: readTaskC1Posture(this.store, task).state === 'claimed'
1055
+ ? 'Run task corrective-yield first, then use task corrective-replan-prepare.'
1056
+ : 'Use task corrective-replan-prepare, confirm, and execute.',
1057
+ });
1058
+ }
1059
+ else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
1060
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
1061
+ }
643
1062
  if (currentHardBlockingDecisions.length > 0) {
644
1063
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
645
1064
  taskId: task.id,
@@ -669,9 +1088,6 @@ export class WorkflowService {
669
1088
  })),
670
1089
  });
671
1090
  }
672
- const currentPlanRiskAudit = task.status === 'needs_fix'
673
- ? readCurrentPlanRiskAudit(this.store, task)
674
- : null;
675
1091
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
676
1092
  if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
677
1093
  validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
@@ -714,7 +1130,7 @@ export class WorkflowService {
714
1130
  }
715
1131
  }
716
1132
  const root = this.store.taskRoot(identity.projectId, taskId);
717
- const stagedPlan = this.store.stageArtifact(root, 'plan.md', renderPlan(task, plan));
1133
+ const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
718
1134
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
719
1135
  ? { ...authorization, decision: 'superseded' }
720
1136
  : authorization);
@@ -731,12 +1147,17 @@ export class WorkflowService {
731
1147
  plannedSteps.every((step) => step.status === 'completed')) {
732
1148
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
733
1149
  }
734
- if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
735
- 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.`, {
736
- taskId: task.id,
737
- requiredStepIds: currentReplanRequiredStepIds,
738
- plannedStepIds: plannedSteps.map((step) => step.id),
739
- });
1150
+ if (correctiveReplanPosture?.posture === 'replan-required') {
1151
+ const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
1152
+ if (!executionEvaluation.allowed) {
1153
+ throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
1154
+ transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
1155
+ evaluationPhase: executionEvaluation.phase,
1156
+ blockingGuardIds: executionEvaluation.blockingGuardIds,
1157
+ pendingGuardIds: executionEvaluation.pendingGuardIds,
1158
+ guardResults: executionEvaluation.guardResults,
1159
+ });
1160
+ }
740
1161
  }
741
1162
  if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
742
1163
  throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
@@ -744,14 +1165,10 @@ export class WorkflowService {
744
1165
  stepId: guardedRemediationGate.stepId,
745
1166
  });
746
1167
  }
747
- if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
748
- assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
749
- appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
750
- }
751
1168
  const candidate = {
752
1169
  ...task,
753
1170
  status: 'awaiting_execution_authorization',
754
- planHash: stagedPlan.hash,
1171
+ planHash: preparedPlan.planHash,
755
1172
  planObjective: plan.objective,
756
1173
  requirements: plan.requirements,
757
1174
  acceptance: plan.acceptance,
@@ -771,6 +1188,18 @@ export class WorkflowService {
771
1188
  };
772
1189
  if (planRiskAudit)
773
1190
  validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
1191
+ const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
1192
+ if (stagedPlan.hash !== preparedPlan.planHash) {
1193
+ throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
1194
+ taskId: task.id,
1195
+ preparedPlanHash: preparedPlan.planHash,
1196
+ stagedPlanHash: stagedPlan.hash,
1197
+ });
1198
+ }
1199
+ if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
1200
+ assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
1201
+ appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, preparedPlan.planHash, this.now());
1202
+ }
774
1203
  const saved = this.store.writeTask(candidate, expectedRevision);
775
1204
  this.store.publishArtifact(stagedPlan);
776
1205
  if (planRiskAudit)
@@ -781,9 +1210,10 @@ export class WorkflowService {
781
1210
  return saved;
782
1211
  }
783
1212
  rebindTaskKnowledge(repository, taskId, expectedRevision) {
784
- const { identity } = this.context(repository);
1213
+ const { identity } = this.#mutationContext(repository);
785
1214
  const task = this.store.readTask(identity.projectId, taskId);
786
1215
  assertExpectedRevision(task, expectedRevision);
1216
+ this.assertTaskContextChangeAllowed(task);
787
1217
  if (!taskCanRebindKnowledge(task)) {
788
1218
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while Task is ${task.status}.`);
789
1219
  }
@@ -865,11 +1295,12 @@ export class WorkflowService {
865
1295
  if (!delegationGrantId) {
866
1296
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
867
1297
  }
868
- const { identity } = this.context(repository);
1298
+ const { identity } = this.#mutationContext(repository);
869
1299
  const task = this.store.readTask(identity.projectId, taskId);
870
1300
  const currentMap = this.store.readKnowledgeMap(identity.projectId);
871
1301
  assertExpectedRevision(task, expectedTaskRevision);
872
1302
  assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
1303
+ this.assertTaskContextChangeAllowed(task);
873
1304
  if (!taskCanRebindKnowledge(task)) {
874
1305
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
875
1306
  }
@@ -887,48 +1318,75 @@ export class WorkflowService {
887
1318
  // Validate every delegated transition before the first write so a narrow grant cannot
888
1319
  // leave the composite operation half-applied.
889
1320
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
890
- let knowledgeMap = currentMap;
891
- if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
892
- if (currentMap.status !== 'active' || !currentMap.approval) {
893
- throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
894
- }
895
- assertDelegatedKnowledgeRefresh(refreshAssessment);
896
- if (inspectedMap.status !== 'stale') {
897
- throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
898
- }
899
- if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
900
- this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
901
- }
902
- this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
903
- knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
904
- transitions.push('project-memory reconcile');
905
- knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
906
- transitions.push('project-memory approve');
907
- }
908
- const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
909
- if (task.knowledgeMapRevision === knowledgeMap.revision
910
- && task.knowledgeMapHash === knowledgeMapHash) {
911
- throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
912
- }
913
- let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
914
- if (currentPlanRiskAudit) {
915
- appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
916
- }
917
- transitions.push('task knowledge-rebind');
918
- refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
919
- ? 'Delegated Plan-bounded supporting-source context refresh.'
920
- : 'Delegated content-only context refresh.', delegationGrantId);
921
- transitions.push('task execution authorize');
922
- return {
923
- mode: refreshAssessment.mode,
924
- transitions,
925
- addedSources: refreshAssessment.addedSources,
926
- knowledgeMap,
927
- task: refreshedTask,
928
- };
1321
+ const projectRoot = this.store.projectRoot(identity.projectId);
1322
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
1323
+ return runRollbackProjectTransaction({
1324
+ projectRoot,
1325
+ projectId: identity.projectId,
1326
+ transactionId: `tx-${createEntityId('TASK', this.now().getTime()).toLowerCase()}`,
1327
+ kind: 'delegated-task-context-refresh',
1328
+ now: this.now(),
1329
+ targetPaths: [
1330
+ relative(path.join(projectRoot, 'knowledge-map.json')),
1331
+ relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'state.json')),
1332
+ relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan.md')),
1333
+ relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan-risk-audits.jsonl')),
1334
+ ],
1335
+ mutate: () => {
1336
+ let knowledgeMap = currentMap;
1337
+ if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
1338
+ if (currentMap.status !== 'active' || !currentMap.approval) {
1339
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
1340
+ }
1341
+ assertDelegatedKnowledgeRefresh(refreshAssessment);
1342
+ if (inspectedMap.status !== 'stale') {
1343
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
1344
+ }
1345
+ if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
1346
+ this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
1347
+ }
1348
+ this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
1349
+ knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision, true);
1350
+ transitions.push('project-memory reconcile');
1351
+ knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
1352
+ transitions.push('project-memory approve');
1353
+ }
1354
+ const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
1355
+ if (task.knowledgeMapRevision === knowledgeMap.revision
1356
+ && task.knowledgeMapHash === knowledgeMapHash) {
1357
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
1358
+ }
1359
+ let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
1360
+ if (currentPlanRiskAudit) {
1361
+ appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
1362
+ }
1363
+ transitions.push('task knowledge-rebind');
1364
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
1365
+ ? 'Delegated Plan-bounded supporting-source context refresh.'
1366
+ : 'Delegated content-only context refresh.', delegationGrantId);
1367
+ transitions.push('task execution authorize');
1368
+ return {
1369
+ mode: refreshAssessment.mode,
1370
+ transitions,
1371
+ addedSources: refreshAssessment.addedSources,
1372
+ knowledgeMap,
1373
+ task: refreshedTask,
1374
+ };
1375
+ },
1376
+ validate: (result) => {
1377
+ const currentTask = this.store.readTask(identity.projectId, task.id);
1378
+ if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
1379
+ throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit an authorized Task.');
1380
+ }
1381
+ const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
1382
+ if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
1383
+ throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction Knowledge Map revision mismatch.');
1384
+ }
1385
+ },
1386
+ });
929
1387
  }
930
1388
  authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
931
- const { identity } = this.context(repository);
1389
+ const { identity } = this.#mutationContext(repository);
932
1390
  this.readCurrentAdoptionPostureStrict(identity.projectId);
933
1391
  const task = this.store.readTask(identity.projectId, taskId);
934
1392
  assertExpectedRevision(task, expectedRevision);
@@ -958,7 +1416,7 @@ export class WorkflowService {
958
1416
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
959
1417
  }
960
1418
  recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
961
- const { identity } = this.context(repository);
1419
+ const { identity } = this.#mutationContext(repository);
962
1420
  this.readCurrentAdoptionPostureStrict(identity.projectId);
963
1421
  const task = this.store.readTask(identity.projectId, taskId);
964
1422
  assertExpectedRevision(task, expectedRevision);
@@ -966,7 +1424,7 @@ export class WorkflowService {
966
1424
  return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
967
1425
  }
968
1426
  showTaskHandoff(repository, taskId) {
969
- const { identity } = this.context(repository);
1427
+ const { identity } = this.#mutationContext(repository);
970
1428
  const task = this.store.readTask(identity.projectId, taskId);
971
1429
  return {
972
1430
  task,
@@ -974,8 +1432,8 @@ export class WorkflowService {
974
1432
  claimTokenAvailable: false,
975
1433
  };
976
1434
  }
977
- handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
978
- const context = this.context(repository);
1435
+ handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
1436
+ const context = this.#mutationContext(repository);
979
1437
  const task = this.store.readTask(context.identity.projectId, taskId);
980
1438
  assertExpectedRevision(task, expectedRevision);
981
1439
  const normalizedActor = actor.trim();
@@ -1033,6 +1491,7 @@ export class WorkflowService {
1033
1491
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1034
1492
  grantId,
1035
1493
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
1494
+ replacePending,
1036
1495
  }, this.now());
1037
1496
  releaseLease();
1038
1497
  return { task, event: prepared.event, bundle: prepared.bundle };
@@ -1044,15 +1503,15 @@ export class WorkflowService {
1044
1503
  }
1045
1504
  }
1046
1505
  claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
1047
- const context = this.context(repository);
1506
+ const context = this.#mutationContext(repository);
1048
1507
  const task = this.store.readTask(context.identity.projectId, taskId);
1049
1508
  assertExpectedRevision(task, expectedRevision);
1050
1509
  const normalizedActor = actor.trim();
1051
1510
  if (!normalizedActor) {
1052
1511
  throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
1053
1512
  }
1054
- const pendingHandoff = readTaskC1Posture(this.store, task);
1055
- if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
1513
+ const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now());
1514
+ if (pendingHandoff.latestEvent.grantId) {
1056
1515
  this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
1057
1516
  }
1058
1517
  const existingLease = context.locks.inspect(taskId);
@@ -1085,7 +1544,14 @@ export class WorkflowService {
1085
1544
  }
1086
1545
  try {
1087
1546
  const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1088
- return { task, event, lease };
1547
+ return {
1548
+ task,
1549
+ event,
1550
+ lease: toPublicWriterLease(lease, this.now()),
1551
+ writerCredential: acquiredHere
1552
+ ? { disposition: 'issued', token: lease.token }
1553
+ : { disposition: 'renewed' },
1554
+ };
1089
1555
  }
1090
1556
  catch (error) {
1091
1557
  if (acquiredHere)
@@ -1093,8 +1559,45 @@ export class WorkflowService {
1093
1559
  throw error;
1094
1560
  }
1095
1561
  }
1562
+ replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
1563
+ const context = this.#mutationContext(repository);
1564
+ const task = this.store.readTask(context.identity.projectId, taskId);
1565
+ assertExpectedRevision(task, expectedRevision);
1566
+ const normalizedActor = actor.trim();
1567
+ if (!normalizedActor)
1568
+ throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
1569
+ const posture = readTaskC1Posture(this.store, task);
1570
+ const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
1571
+ const action = String(navigation.action);
1572
+ const currentLease = context.locks.inspect(taskId);
1573
+ const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
1574
+ const requiredActor = writerTokenContract
1575
+ ? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
1576
+ : null;
1577
+ if (!requiredActor || requiredActor !== normalizedActor) {
1578
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
1579
+ taskId,
1580
+ posture: posture.state,
1581
+ claimant: posture.state === 'claimed' ? posture.claimant : null,
1582
+ actor: normalizedActor,
1583
+ requiredActor,
1584
+ nextAction: action,
1585
+ });
1586
+ }
1587
+ const lease = currentLease
1588
+ ? context.locks.replaceCredential(taskId, normalizedActor)
1589
+ : context.locks.acquire(taskId, normalizedActor);
1590
+ if (posture.state === 'claimed') {
1591
+ appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
1592
+ }
1593
+ return {
1594
+ task,
1595
+ lease: toPublicWriterLease(lease, this.now()),
1596
+ writerCredential: { disposition: 'issued', token: lease.token },
1597
+ };
1598
+ }
1096
1599
  handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1097
- const context = this.context(repository);
1600
+ const context = this.#mutationContext(repository);
1098
1601
  const task = this.store.readTask(context.identity.projectId, taskId);
1099
1602
  assertExpectedRevision(task, expectedRevision);
1100
1603
  const lease = task.status === 'merged' || task.status === 'cancelled'
@@ -1106,7 +1609,7 @@ export class WorkflowService {
1106
1609
  return { task, event };
1107
1610
  }
1108
1611
  startTask(repository, taskId, expectedRevision, workspaceOwner) {
1109
- const { identity } = this.context(repository);
1612
+ const { identity } = this.#mutationContext(repository);
1110
1613
  const task = this.store.readTask(identity.projectId, taskId);
1111
1614
  assertExpectedRevision(task, expectedRevision);
1112
1615
  this.requireExecutionAdoptionPosture(identity, taskId);
@@ -1158,7 +1661,7 @@ export class WorkflowService {
1158
1661
  return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
1159
1662
  }
1160
1663
  runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1161
- const context = this.context(repository);
1664
+ const context = this.#mutationContext(repository);
1162
1665
  const task = this.store.readTask(context.identity.projectId, taskId);
1163
1666
  assertExpectedRevision(task, expectedRevision);
1164
1667
  this.requireExecutionAdoptionPosture(context.identity, taskId);
@@ -1187,6 +1690,7 @@ export class WorkflowService {
1187
1690
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
1188
1691
  }
1189
1692
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1693
+ let resumesCorrectiveReplan = false;
1190
1694
  if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1191
1695
  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.`, {
1192
1696
  taskId,
@@ -1201,20 +1705,21 @@ export class WorkflowService {
1201
1705
  && guardedPosture.correctiveDecision.planHash !== task.planHash
1202
1706
  ? guardedPosture.correctiveDecision
1203
1707
  : null;
1204
- const recovery = staleDecision
1708
+ const recovery = staleDecision?.decision === 'continue-fix'
1205
1709
  ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
1206
1710
  : null;
1207
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1711
+ const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1712
+ resumesCorrectiveReplan = gate.posture === 'replan-ready';
1208
1713
  }
1209
1714
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1210
1715
  const dirty = changedFiles(context.identity.repositoryRoot);
1211
- if (step.status === 'planned' && dirty.length > 0) {
1716
+ if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
1212
1717
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
1213
1718
  changedFiles: dirty,
1214
1719
  });
1215
1720
  }
1216
1721
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
1217
- if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
1722
+ if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
1218
1723
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
1219
1724
  changedFiles: dirtyOutsideStep,
1220
1725
  });
@@ -1272,7 +1777,8 @@ export class WorkflowService {
1272
1777
  }
1273
1778
  return {
1274
1779
  task: saved,
1275
- lease,
1780
+ lease: toPublicWriterLease(lease, this.now()),
1781
+ writerCredential: { disposition: acquiredHere ? 'issued' : 'renewed' },
1276
1782
  envelope: {
1277
1783
  protocolVersion: PROTOCOL_VERSION,
1278
1784
  packageVersion: PACKAGE_VERSION,
@@ -1308,14 +1814,14 @@ export class WorkflowService {
1308
1814
  taskId,
1309
1815
  stepId,
1310
1816
  expectedRevision: saved.revision,
1311
- writerToken: lease.token,
1817
+ writerToken: acquiredHere ? lease.token : null,
1312
1818
  knowledgeMap: knowledgeSummary(knowledgeMap),
1313
1819
  knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
1314
1820
  },
1315
1821
  };
1316
1822
  }
1317
1823
  taskContext(repository, taskId, role) {
1318
- const { identity } = this.context(repository);
1824
+ const { identity } = this.#mutationContext(repository);
1319
1825
  const task = this.store.readTask(identity.projectId, taskId);
1320
1826
  const historicalStrictReview = role === 'independent-reviewer'
1321
1827
  && task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
@@ -1394,7 +1900,7 @@ export class WorkflowService {
1394
1900
  };
1395
1901
  }
1396
1902
  completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
1397
- const context = this.context(repository);
1903
+ const context = this.#mutationContext(repository);
1398
1904
  const lease = context.locks.heartbeat(taskId, writerToken);
1399
1905
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1400
1906
  const task = this.store.readTask(context.identity.projectId, taskId);
@@ -1480,7 +1986,7 @@ export class WorkflowService {
1480
1986
  return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
1481
1987
  }
1482
1988
  reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
1483
- const context = this.context(repository);
1989
+ const context = this.#mutationContext(repository);
1484
1990
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1485
1991
  const task = this.store.readTask(context.identity.projectId, taskId);
1486
1992
  assertExpectedRevision(task, expectedRevision);
@@ -1536,10 +2042,18 @@ export class WorkflowService {
1536
2042
  resolution: currentCycle.resolution,
1537
2043
  });
1538
2044
  }
1539
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
2045
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2046
+ if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2047
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
2048
+ taskId: task.id,
2049
+ stepId,
2050
+ infrastructureFailures,
2051
+ requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
2052
+ });
2053
+ }
1540
2054
  }
1541
2055
  else if (currentCycle.resolution !== 'pending') {
1542
- if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
2056
+ if (currentCycle.resolution === 'failed') {
1543
2057
  if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1544
2058
  throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1545
2059
  taskId: task.id,
@@ -1547,7 +2061,7 @@ export class WorkflowService {
1547
2061
  resolution: currentCycle.resolution,
1548
2062
  });
1549
2063
  }
1550
- appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2064
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
1551
2065
  completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
1552
2066
  checksHash: currentCycle.reviewRecordedEvent.checksHash,
1553
2067
  reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
@@ -1565,7 +2079,7 @@ export class WorkflowService {
1565
2079
  reviewedCommit: launch.reviewedCommit,
1566
2080
  isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
1567
2081
  }, this.now());
1568
- if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
2082
+ if (recorded.resolution === 'failed') {
1569
2083
  const reviewRecordedEvent = recorded.reviewRecordedEvent;
1570
2084
  const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
1571
2085
  if (!reviewRecordedEvent || !reviewerAttestationEvent) {
@@ -1575,7 +2089,7 @@ export class WorkflowService {
1575
2089
  resolution: recorded.resolution,
1576
2090
  });
1577
2091
  }
1578
- appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2092
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
1579
2093
  completionCommit: reviewRecordedEvent.completionCommit,
1580
2094
  checksHash: reviewRecordedEvent.checksHash,
1581
2095
  reviewEventHash: reviewRecordedEvent.eventHash,
@@ -1584,8 +2098,132 @@ export class WorkflowService {
1584
2098
  }
1585
2099
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
1586
2100
  }
2101
+ prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
2102
+ const identity = resolveRepositoryIdentity(repository);
2103
+ const task = this.store.readTask(identity.projectId, taskId);
2104
+ assertExpectedRevision(task, expectedRevision);
2105
+ if (task.status !== 'in_progress' && task.status !== 'blocked') {
2106
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
2107
+ }
2108
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2109
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
2110
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
2111
+ }
2112
+ const step = requireStep(task, stepId);
2113
+ if (step.status !== 'in_progress' || !step.evidence) {
2114
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
2115
+ }
2116
+ validateTaskHistory(identity.repositoryRoot, task);
2117
+ if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
2118
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
2119
+ }
2120
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
2121
+ return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
2122
+ }
2123
+ recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
2124
+ const context = this.#mutationContext(repository);
2125
+ const task = this.store.readTask(context.identity.projectId, taskId);
2126
+ assertExpectedRevision(task, expectedRevision);
2127
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2128
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2129
+ const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
2130
+ let review;
2131
+ try {
2132
+ review = validateExternalStrictStepReviewInput(input, packet);
2133
+ }
2134
+ catch (error) {
2135
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
2136
+ }
2137
+ if (review.reviewer.trim() === actor.trim()) {
2138
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
2139
+ }
2140
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
2141
+ if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
2142
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
2143
+ taskId,
2144
+ stepId,
2145
+ resolution: currentCycle.resolution,
2146
+ });
2147
+ }
2148
+ const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
2149
+ if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
2150
+ throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
2151
+ taskId,
2152
+ stepId,
2153
+ infrastructureFailures,
2154
+ });
2155
+ }
2156
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
2157
+ const recorded = recordStrictStepReview(this.store, task, stepId, {
2158
+ review,
2159
+ reviewerOutputHash: strictReviewInputHash(review),
2160
+ reviewedCommit: packet.reviewedCommit,
2161
+ isolationResult: 'verified',
2162
+ }, this.now());
2163
+ if (recorded.resolution === 'failed') {
2164
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
2165
+ completionCommit: recorded.reviewRecordedEvent.completionCommit,
2166
+ checksHash: recorded.reviewRecordedEvent.checksHash,
2167
+ reviewEventHash: recorded.reviewRecordedEvent.eventHash,
2168
+ reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
2169
+ }, readCurrentPlanRiskAudit(this.store, task), this.now());
2170
+ }
2171
+ const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
2172
+ return {
2173
+ mode: 'external-sealed',
2174
+ packetHash: packet.packetHash,
2175
+ repositorySealHash: packet.repositorySealHash,
2176
+ reviewerThreadId: input.reviewerThreadId,
2177
+ task: saved,
2178
+ };
2179
+ }
2180
+ prepareExternalTaskReview(repository, taskId, expectedRevision) {
2181
+ const identity = resolveRepositoryIdentity(repository);
2182
+ const task = this.store.readTask(identity.projectId, taskId);
2183
+ assertExpectedRevision(task, expectedRevision);
2184
+ const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
2185
+ if (task.status !== 'validating' && !retryingUnverified) {
2186
+ throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
2187
+ }
2188
+ validateTaskHistory(identity.repositoryRoot, task);
2189
+ assertTaskBranch(identity.repositoryRoot, task);
2190
+ if (!isClean(identity.repositoryRoot)) {
2191
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
2192
+ }
2193
+ const taskRoot = this.store.taskRoot(task.projectId, task.id);
2194
+ if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
2195
+ throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
2196
+ }
2197
+ return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
2198
+ }
2199
+ recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
2200
+ const context = this.#mutationContext(repository);
2201
+ const task = this.store.readTask(context.identity.projectId, taskId);
2202
+ assertExpectedRevision(task, expectedRevision);
2203
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
2204
+ this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
2205
+ const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
2206
+ let review;
2207
+ try {
2208
+ review = validateExternalStrictTaskReviewInput(input, packet);
2209
+ }
2210
+ catch (error) {
2211
+ throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
2212
+ }
2213
+ if (review.reviewer.trim() === actor.trim()) {
2214
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
2215
+ }
2216
+ const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
2217
+ return {
2218
+ mode: 'external-sealed',
2219
+ packetHash: packet.packetHash,
2220
+ repositorySealHash: packet.repositorySealHash,
2221
+ reviewerThreadId: input.reviewerThreadId,
2222
+ task: saved,
2223
+ };
2224
+ }
1587
2225
  submitTask(repository, taskId, expectedRevision, writerToken, actor) {
1588
- const context = this.context(repository);
2226
+ const context = this.#mutationContext(repository);
1589
2227
  const lease = context.locks.heartbeat(taskId, writerToken);
1590
2228
  const task = this.store.readTask(context.identity.projectId, taskId);
1591
2229
  assertExpectedRevision(task, expectedRevision);
@@ -1617,11 +2255,11 @@ export class WorkflowService {
1617
2255
  return saved;
1618
2256
  }
1619
2257
  recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
1620
- const { identity } = this.context(repository);
2258
+ const { identity } = this.#mutationContext(repository);
1621
2259
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1622
2260
  const task = this.store.readTask(identity.projectId, taskId);
1623
2261
  assertExpectedRevision(task, expectedRevision);
1624
- this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
2262
+ this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
1625
2263
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1626
2264
  const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
1627
2265
  if (task.status !== 'validating' && !retryingUnverified) {
@@ -1659,7 +2297,7 @@ export class WorkflowService {
1659
2297
  return saved;
1660
2298
  }
1661
2299
  recoverStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1662
- const context = this.context(repository);
2300
+ const context = this.#mutationContext(repository);
1663
2301
  const task = this.store.readTask(context.identity.projectId, taskId);
1664
2302
  assertExpectedRevision(task, expectedRevision);
1665
2303
  this.requireExecutionAdoptionPosture(context.identity, taskId);
@@ -1671,18 +2309,18 @@ export class WorkflowService {
1671
2309
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1672
2310
  }
1673
2311
  recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1674
- const context = this.context(repository);
2312
+ const context = this.#mutationContext(repository);
1675
2313
  const task = this.store.readTask(context.identity.projectId, taskId);
1676
2314
  assertExpectedRevision(task, expectedRevision);
1677
2315
  this.requireExecutionAdoptionPosture(context.identity, taskId);
1678
2316
  return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1679
2317
  }
1680
2318
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1681
- const { identity } = this.context(repository);
2319
+ const { identity } = this.#mutationContext(repository);
1682
2320
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1683
2321
  const task = this.store.readTask(identity.projectId, taskId);
1684
2322
  assertExpectedRevision(task, expectedRevision);
1685
- this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
2323
+ this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
1686
2324
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1687
2325
  if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
1688
2326
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
@@ -1724,7 +2362,7 @@ export class WorkflowService {
1724
2362
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
1725
2363
  }
1726
2364
  setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
1727
- const { identity } = this.context(repository);
2365
+ const { identity } = this.#mutationContext(repository);
1728
2366
  const task = this.store.readTask(identity.projectId, taskId);
1729
2367
  assertExpectedRevision(task, expectedRevision);
1730
2368
  if (task.status !== 'validating' || task.review.status !== 'passed') {
@@ -1738,7 +2376,7 @@ export class WorkflowService {
1738
2376
  return saved;
1739
2377
  }
1740
2378
  acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
1741
- const { identity } = this.context(repository);
2379
+ const { identity } = this.#mutationContext(repository);
1742
2380
  const task = this.store.readTask(identity.projectId, taskId);
1743
2381
  assertExpectedRevision(task, expectedRevision);
1744
2382
  if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
@@ -1776,7 +2414,7 @@ export class WorkflowService {
1776
2414
  }, expectedRevision);
1777
2415
  }
1778
2416
  syncTaskBase(repository, taskId, expectedRevision) {
1779
- const { identity } = this.context(repository);
2417
+ const { identity } = this.#mutationContext(repository);
1780
2418
  const task = this.store.readTask(identity.projectId, taskId);
1781
2419
  assertExpectedRevision(task, expectedRevision);
1782
2420
  if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
@@ -1828,7 +2466,7 @@ export class WorkflowService {
1828
2466
  return saved;
1829
2467
  }
1830
2468
  mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
1831
- const context = this.context(repository);
2469
+ const context = this.#mutationContext(repository);
1832
2470
  const task = this.store.readTask(context.identity.projectId, taskId);
1833
2471
  assertExpectedRevision(task, expectedRevision);
1834
2472
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -1850,7 +2488,7 @@ export class WorkflowService {
1850
2488
  return saved;
1851
2489
  }
1852
2490
  confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
1853
- const context = this.context(repository);
2491
+ const context = this.#mutationContext(repository);
1854
2492
  const task = this.store.readTask(context.identity.projectId, taskId);
1855
2493
  assertExpectedRevision(task, expectedRevision);
1856
2494
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -1867,21 +2505,22 @@ export class WorkflowService {
1867
2505
  return saved;
1868
2506
  }
1869
2507
  status(repository) {
1870
- const { identity } = this.context(repository);
1871
- const tasks = this.store.listTasks(identity.projectId);
1872
- const rawMilestones = this.store.listMilestones(identity.projectId);
1873
- const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
2508
+ const snapshot = this.observationSnapshot(repository);
1874
2509
  return {
1875
- projectId: identity.projectId,
1876
- discoveries: this.store.listDiscoveries(identity.projectId),
1877
- tasks,
1878
- milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
1879
- delegations: this.store.listDelegations(identity.projectId),
1880
- adoption,
2510
+ projectId: snapshot.identity.projectId,
2511
+ discoveries: snapshot.discoveries,
2512
+ tasks: snapshot.tasks,
2513
+ milestones: snapshot.milestones,
2514
+ milestoneInitialAssembly: snapshot.milestones
2515
+ .map((milestone) => this.describeMilestoneInitialAssembly(snapshot.identity.projectId, milestone, snapshot.tasks))
2516
+ .filter((value) => value !== null),
2517
+ delegations: snapshot.delegations,
2518
+ adoption: snapshot.adoption,
2519
+ observation: snapshot.observation,
1881
2520
  };
1882
2521
  }
1883
2522
  recoverMilestoneTransactions(repository) {
1884
- const { identity } = this.context(repository);
2523
+ const { identity } = this.#mutationContext(repository);
1885
2524
  const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
1886
2525
  return {
1887
2526
  projectId: identity.projectId,
@@ -1889,11 +2528,11 @@ export class WorkflowService {
1889
2528
  };
1890
2529
  }
1891
2530
  prepareAdoption(repository) {
1892
- const { identity } = this.context(repository);
2531
+ const { identity } = this.#mutationContext(repository);
1893
2532
  return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
1894
2533
  }
1895
2534
  applyAdoption(repository, actor, confirmationCode) {
1896
- const context = this.context(repository);
2535
+ const context = this.#mutationContext(repository);
1897
2536
  const tasks = context.store.listTasks(context.identity.projectId);
1898
2537
  const milestones = context.store.listMilestones(context.identity.projectId);
1899
2538
  return applyAdoptionPosture(this.store, {
@@ -1910,53 +2549,186 @@ export class WorkflowService {
1910
2549
  }, actor, confirmationCode);
1911
2550
  }
1912
2551
  updatePreflight(repository) {
1913
- const context = this.context(repository);
1914
- const tasks = context.store.listTasks(context.identity.projectId);
1915
- const milestones = context.store.listMilestones(context.identity.projectId);
1916
- const dirty = changedFiles(context.identity.repositoryRoot);
2552
+ const snapshot = this.observationSnapshot(repository);
2553
+ const tasks = snapshot.tasks;
2554
+ const milestones = snapshot.milestones;
2555
+ const dirty = changedFiles(snapshot.identity.repositoryRoot);
1917
2556
  const runningSteps = tasks.flatMap((task) => task.steps
1918
2557
  .filter((step) => step.status === 'in_progress')
1919
2558
  .map((step) => ({ taskId: task.id, stepId: step.id })));
1920
- const leases = context.locks.list();
2559
+ const leases = snapshot.leases;
1921
2560
  const now = this.now().getTime();
1922
2561
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1923
2562
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2563
+ const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2564
+ const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2565
+ const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
2566
+ ...base,
2567
+ milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
2568
+ }));
2569
+ const dependencyPresent = inspectedVersions.declared !== null
2570
+ || inspectedVersions.locked !== null
2571
+ || inspectedVersions.installed !== null;
2572
+ const versionValues = dependencyPresent
2573
+ ? [
2574
+ inspectedVersions.declared,
2575
+ inspectedVersions.locked,
2576
+ inspectedVersions.installed,
2577
+ inspectedVersions.currentBranch,
2578
+ ...milestoneBases.map((base) => base.version),
2579
+ ]
2580
+ : [];
2581
+ const versionsAligned = versionValues.every((version) => version === PACKAGE_VERSION);
2582
+ const versionBlockers = dependencyPresent && !versionsAligned
2583
+ ? ['Workflow dependency versions diverge across declared, locked, installed, current-branch, or active Milestone base state.']
2584
+ : [];
1924
2585
  const blockers = [
1925
2586
  ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1926
2587
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
1927
2588
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1928
- ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
1929
- ];
1930
- const adoption = buildAdoptionPreparation({
1931
- projectId: context.identity.projectId,
1932
- repositoryIdentity: context.identity,
1933
- packageVersion: PACKAGE_VERSION,
1934
- protocolVersion: PROTOCOL_VERSION,
1935
- stateSchemaVersion: STATE_SCHEMA_VERSION,
1936
- tasks,
1937
- milestones,
1938
- sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
1939
- blockers,
1940
- now: this.now(),
1941
- }, readCurrentAdoptionPosture(this.store, context.identity.projectId));
2589
+ ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2590
+ ...versionBlockers,
2591
+ ...observationPreflightBlockers(snapshot.observation),
2592
+ ];
1942
2593
  return {
1943
2594
  safe: blockers.length === 0,
1944
2595
  packageVersion: PACKAGE_VERSION,
1945
2596
  protocolVersion: PROTOCOL_VERSION,
1946
2597
  stateSchemaVersion: STATE_SCHEMA_VERSION,
1947
- projectId: context.identity.projectId,
1948
- repositoryRoot: context.identity.repositoryRoot,
2598
+ projectId: snapshot.identity.projectId,
2599
+ repositoryRoot: snapshot.identity.repositoryRoot,
1949
2600
  clean: dirty.length === 0,
1950
2601
  changedFiles: dirty,
1951
2602
  runningSteps,
1952
- activeLeases,
1953
- staleLeases,
2603
+ activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2604
+ staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
1954
2605
  blockers,
1955
- adoption,
2606
+ workflowVersions: {
2607
+ runtime: inspectedVersions.runtime,
2608
+ declared: inspectedVersions.declared,
2609
+ locked: inspectedVersions.locked,
2610
+ installed: inspectedVersions.installed,
2611
+ currentBranch: inspectedVersions.currentBranch,
2612
+ milestoneBases,
2613
+ },
2614
+ dependencyTransport: {
2615
+ safe: versionBlockers.length === 0,
2616
+ requiredAction: versionBlockers.length === 0
2617
+ ? null
2618
+ : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
2619
+ },
2620
+ adoption: snapshot.adoption,
2621
+ observation: snapshot.observation,
2622
+ };
2623
+ }
2624
+ dependencyProvenanceRecoveryPreflight(repository, taskId) {
2625
+ const snapshot = this.observationSnapshot(repository);
2626
+ if (snapshot.observation.kind !== 'assessment') {
2627
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency provenance recovery requires a coherent observation boundary.', {
2628
+ observation: snapshot.observation,
2629
+ });
2630
+ }
2631
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
2632
+ if (!task)
2633
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
2634
+ return this.inspectDependencyProvenanceCandidate(snapshot, task);
2635
+ }
2636
+ recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
2637
+ const normalizedActor = actor.trim();
2638
+ const normalizedReason = reason.trim();
2639
+ if (!normalizedActor)
2640
+ throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires an actor.');
2641
+ if (!normalizedReason)
2642
+ throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires a reason.');
2643
+ const context = this.#mutationContext(repository);
2644
+ const current = context.store.readTask(context.identity.projectId, taskId);
2645
+ const currentHead = headCommit(context.identity.repositoryRoot);
2646
+ const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead && record.packageVersion === PACKAGE_VERSION);
2647
+ if (prior && current.systemCommits.includes(currentHead))
2648
+ return current;
2649
+ assertExpectedRevision(current, expectedRevision);
2650
+ const snapshot = this.observationSnapshot(repository);
2651
+ const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
2652
+ if (!task)
2653
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
2654
+ const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task);
2655
+ if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
2656
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
2657
+ taskId,
2658
+ blockers: preflight.blockers,
2659
+ });
2660
+ }
2661
+ return context.store.writeTask({
2662
+ ...task,
2663
+ systemCommits: [...task.systemCommits, preflight.commitSha],
2664
+ dependencyProvenanceRecoveries: [
2665
+ ...(task.dependencyProvenanceRecoveries ?? []),
2666
+ {
2667
+ commitSha: preflight.commitSha,
2668
+ parentCommitSha: preflight.parentCommitSha,
2669
+ packageName: PACKAGE_NAME,
2670
+ packageVersion: PACKAGE_VERSION,
2671
+ files: ['package-lock.json', 'package.json'],
2672
+ actor: normalizedActor,
2673
+ reason: normalizedReason,
2674
+ recordedAt: this.now().toISOString(),
2675
+ retainedHistoricalCommits: preflight.retainedHistoricalCommits,
2676
+ },
2677
+ ],
2678
+ }, expectedRevision);
2679
+ }
2680
+ milestoneProgress(repository, milestoneId) {
2681
+ const snapshot = this.observationSnapshot(repository);
2682
+ if (snapshot.observation.kind !== 'assessment') {
2683
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone progress requires a coherent read-only observation boundary.', {
2684
+ observation: snapshot.observation,
2685
+ });
2686
+ }
2687
+ const milestone = snapshot.milestones.find((candidate) => candidate.id === milestoneId);
2688
+ if (!milestone)
2689
+ throw new WorkflowError('NOT_FOUND', `Milestone ${milestoneId} was not found in this repository.`);
2690
+ const tasksById = new Map(snapshot.tasks.map((task) => [task.id, task]));
2691
+ const tasks = milestone.memberships.map((membership, index) => {
2692
+ const task = tasksById.get(membership.taskId);
2693
+ if (!task)
2694
+ throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
2695
+ const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
2696
+ return {
2697
+ ordinal: `T${String(index + 1).padStart(2, '0')}`,
2698
+ taskId: task.id,
2699
+ title: task.title,
2700
+ status: task.status,
2701
+ revision: task.revision,
2702
+ disposition: membership.disposition,
2703
+ membershipReason: membership.reason,
2704
+ replacementForTaskId: task.replacementForTaskId ?? null,
2705
+ replacedByTaskId: task.replacedByTaskId ?? null,
2706
+ stepsCompleted,
2707
+ stepsTotal: task.steps.length,
2708
+ steps: task.steps.map((step, stepIndex) => ({
2709
+ ordinal: `S${String(stepIndex + 1).padStart(2, '0')}`,
2710
+ stepId: step.id,
2711
+ title: step.title,
2712
+ status: step.status,
2713
+ })),
2714
+ };
2715
+ });
2716
+ const required = tasks.filter((task) => task.disposition === 'required');
2717
+ return {
2718
+ readOnly: true,
2719
+ projectId: snapshot.identity.projectId,
2720
+ milestoneId: milestone.id,
2721
+ milestoneTitle: milestone.title,
2722
+ milestoneStatus: milestone.status,
2723
+ milestoneRevision: milestone.revision,
2724
+ membershipRevision: milestone.membershipRevision,
2725
+ tasksCompleted: required.filter((task) => task.status === 'merged').length,
2726
+ tasksRequired: required.length,
2727
+ tasks,
1956
2728
  };
1957
2729
  }
1958
2730
  alpha6StrictReviewRescuePreflight(repository) {
1959
- const context = this.context(repository);
2731
+ const context = this.#mutationContext(repository);
1960
2732
  const dependency = inspectLocalDependency(context.identity.repositoryRoot);
1961
2733
  const tasks = context.store.listTasks(context.identity.projectId);
1962
2734
  const leases = context.locks.list();
@@ -2051,15 +2823,15 @@ export class WorkflowService {
2051
2823
  projectId: context.identity.projectId,
2052
2824
  repositoryRoot: context.identity.repositoryRoot,
2053
2825
  clean: dirty.length === 0,
2054
- activeLeases,
2055
- staleLeases,
2826
+ activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2827
+ staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2056
2828
  candidate,
2057
2829
  blockers,
2058
2830
  requiredActions,
2059
2831
  };
2060
2832
  }
2061
2833
  scanKnowledgeMap(repository) {
2062
- const { identity } = this.context(repository);
2834
+ const { identity } = this.#mutationContext(repository);
2063
2835
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2064
2836
  try {
2065
2837
  const current = this.store.readKnowledgeMap(identity.projectId);
@@ -2072,11 +2844,11 @@ export class WorkflowService {
2072
2844
  }
2073
2845
  }
2074
2846
  showKnowledgeMap(repository) {
2075
- const { identity } = this.context(repository);
2847
+ const { identity } = this.#mutationContext(repository);
2076
2848
  return this.store.readKnowledgeMap(identity.projectId);
2077
2849
  }
2078
2850
  knowledgeMapStatus(repository) {
2079
- const { identity } = this.context(repository);
2851
+ const { identity } = this.#mutationContext(repository);
2080
2852
  const current = this.store.readKnowledgeMap(identity.projectId);
2081
2853
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2082
2854
  return inspectKnowledgeMap(current, scanned);
@@ -2085,7 +2857,7 @@ export class WorkflowService {
2085
2857
  return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
2086
2858
  }
2087
2859
  approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
2088
- const { identity } = this.context(repository);
2860
+ const { identity } = this.#mutationContext(repository);
2089
2861
  const current = this.store.readKnowledgeMap(identity.projectId);
2090
2862
  assertExpectedRevision(current, expectedRevision);
2091
2863
  const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
@@ -2101,25 +2873,36 @@ export class WorkflowService {
2101
2873
  };
2102
2874
  return this.store.writeKnowledgeMap(approved, expectedRevision);
2103
2875
  }
2104
- reconcileKnowledgeMap(repository, expectedRevision) {
2105
- const { identity } = this.context(repository);
2876
+ reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
2877
+ const { identity } = this.#mutationContext(repository);
2106
2878
  const current = this.store.readKnowledgeMap(identity.projectId);
2107
2879
  assertExpectedRevision(current, expectedRevision);
2108
2880
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2881
+ if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2882
+ const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
2883
+ const inspected = inspectKnowledgeMap(current, scanned);
2884
+ const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
2885
+ if (refreshOwner) {
2886
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2887
+ taskId: refreshOwner.task.id,
2888
+ requiredAction: 'Run the exact task context-refresh action returned by next.',
2889
+ });
2890
+ }
2891
+ }
2109
2892
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
2110
2893
  }
2111
2894
  graphRefreshRequest(repository, graphKind, mode = 'moderate') {
2112
- const { identity } = this.context(repository);
2895
+ const { identity } = this.#mutationContext(repository);
2113
2896
  return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2114
2897
  }
2115
2898
  graphStatus(repository, graphKind, mode = 'moderate') {
2116
- const { identity } = this.context(repository);
2899
+ const { identity } = this.#mutationContext(repository);
2117
2900
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2118
2901
  const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
2119
2902
  return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
2120
2903
  }
2121
2904
  bindGraph(repository, expectedRevision, request, evidence) {
2122
- const { identity } = this.context(repository);
2905
+ const { identity } = this.#mutationContext(repository);
2123
2906
  const currentRequest = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, request.graphKind, request.mode, this.now());
2124
2907
  validateGraphRequestCurrent(request, currentRequest);
2125
2908
  const existing = this.readGraphBindingOrNull(identity.projectId, request.graphKind);
@@ -2127,13 +2910,78 @@ export class WorkflowService {
2127
2910
  return this.store.writeGraphBinding(binding, expectedRevision);
2128
2911
  }
2129
2912
  recordGraphFallback(repository, graphKind, mode, expectedRevision, reason) {
2130
- const { identity } = this.context(repository);
2913
+ const { identity } = this.#mutationContext(repository);
2131
2914
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2132
2915
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
2133
2916
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
2134
2917
  }
2135
- next(repository) {
2918
+ next(repository, taskId = null) {
2136
2919
  const status = this.status(repository);
2920
+ if (status.observation.kind === 'blocked') {
2921
+ return {
2922
+ scope: 'project',
2923
+ projectId: status.projectId,
2924
+ status: 'observation-blocked',
2925
+ action: 'blocked',
2926
+ observation: status.observation,
2927
+ navigation: {
2928
+ kind: 'blocked',
2929
+ reasonCodes: status.observation.reasonCodes,
2930
+ availableRepairTransitionId: null,
2931
+ },
2932
+ };
2933
+ }
2934
+ if (status.observation.kind === 'repair') {
2935
+ const repair = status.observation.repair;
2936
+ return {
2937
+ scope: 'project',
2938
+ projectId: status.projectId,
2939
+ status: 'repair-required',
2940
+ action: repair?.command ?? 'blocked',
2941
+ observation: status.observation,
2942
+ navigation: {
2943
+ kind: repair ? 'repair' : 'blocked',
2944
+ transitionId: null,
2945
+ availableRepairTransitionId: null,
2946
+ ...(repair ? {
2947
+ command: repair.command,
2948
+ cataloged: repair.cataloged,
2949
+ ...('available' in repair ? { available: repair.available } : {}),
2950
+ bindings: repair.command === 'milestone recover'
2951
+ ? {
2952
+ projectId: repair.projectId,
2953
+ milestoneIds: repair.milestoneIds,
2954
+ transactionIds: repair.transactionIds,
2955
+ }
2956
+ : 'taskIds' in repair
2957
+ ? {
2958
+ projectId: repair.projectId,
2959
+ taskIds: repair.taskIds,
2960
+ transactionIds: repair.transactionIds,
2961
+ }
2962
+ : {
2963
+ projectId: repair.projectId,
2964
+ entityIds: repair.entityIds,
2965
+ },
2966
+ } : {}),
2967
+ reasonCodes: status.observation.reasonCodes,
2968
+ },
2969
+ };
2970
+ }
2971
+ if (!status.adoption) {
2972
+ return {
2973
+ scope: 'project',
2974
+ projectId: status.projectId,
2975
+ status: 'observation-blocked',
2976
+ action: 'blocked',
2977
+ observation: status.observation,
2978
+ navigation: {
2979
+ kind: 'blocked',
2980
+ reasonCodes: status.observation.reasonCodes,
2981
+ availableRepairTransitionId: null,
2982
+ },
2983
+ };
2984
+ }
2137
2985
  const adoptionInfo = {
2138
2986
  status: status.adoption.status,
2139
2987
  safe: status.adoption.safe,
@@ -2151,33 +2999,149 @@ export class WorkflowService {
2151
2999
  status: status.adoption.status,
2152
3000
  action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
2153
3001
  adoption: adoptionInfo,
3002
+ observation: status.observation,
3003
+ navigation: status.adoption.safe
3004
+ ? {
3005
+ kind: 'human-gate',
3006
+ transitionId: null,
3007
+ cataloged: false,
3008
+ command: 'state adoption-apply',
3009
+ bindings: {
3010
+ projectId: status.projectId,
3011
+ confirmationCode: status.adoption.confirmationCode,
3012
+ confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
3013
+ actorRequired: true,
3014
+ },
3015
+ }
3016
+ : {
3017
+ kind: 'blocked',
3018
+ transitionId: null,
3019
+ availableRepairTransitionId: null,
3020
+ reasonCodes: ['ADOPTION_MISSING'],
3021
+ blockers: status.adoption.blockers,
3022
+ },
2154
3023
  };
2155
3024
  }
2156
- const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
3025
+ const withAdoption = (value) => ({
3026
+ ...value,
3027
+ adoption: adoptionInfo,
3028
+ observation: status.observation,
3029
+ });
2157
3030
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
3031
+ const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
2158
3032
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
2159
- const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
3033
+ const requestedTask = taskId === null
3034
+ ? null
3035
+ : status.tasks.find((task) => task.id === taskId) ?? null;
3036
+ if (taskId !== null && !requestedTask) {
3037
+ throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
3038
+ }
3039
+ if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
3040
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
3041
+ taskId: requestedTask.id,
3042
+ taskStatus: requestedTask.status,
3043
+ milestoneId: requestedTask.milestoneId,
3044
+ });
3045
+ }
3046
+ const activeTask = requestedTask
3047
+ ?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2160
3048
  if (activeTask) {
2161
- const activePlanRiskAudit = activeTask.planHash
2162
- ? readCurrentPlanRiskAudit(this.store, activeTask)
3049
+ const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
3050
+ if (dependencyRecovery.eligible) {
3051
+ return withAdoption({
3052
+ scope: 'task',
3053
+ id: activeTask.id,
3054
+ status: activeTask.status,
3055
+ action: 'update dependency-provenance-recover',
3056
+ revision: activeTask.revision,
3057
+ dependencyProvenanceRecovery: dependencyRecovery,
3058
+ });
3059
+ }
3060
+ const activeTaskMilestone = activeTask.milestoneId
3061
+ ? milestonesById.get(activeTask.milestoneId) ?? null
2163
3062
  : null;
3063
+ const milestoneCancellationSafe = Boolean(activeTaskMilestone
3064
+ && !activeTask.steps.some((step) => step.status === 'in_progress')
3065
+ && status.observation.leases.length === 0
3066
+ && status.observation.coreTaskOperations.length === 0
3067
+ && status.observation.pendingProjectTransactions.length === 0
3068
+ && status.observation.pendingMilestoneTransactions.length === 0
3069
+ && status.observation.pendingTaskTransactions.length === 0
3070
+ && status.observation.corruptTaskTransactions.length === 0);
3071
+ const withMilestoneCancellation = (value) => (milestoneCancellationSafe
3072
+ ? {
3073
+ ...value,
3074
+ milestoneCancellationOption: {
3075
+ command: 'milestone cancel',
3076
+ milestoneId: activeTaskMilestone.id,
3077
+ expectedRevision: activeTaskMilestone.revision,
3078
+ reasonRequired: true,
3079
+ taskDisposition: 'retained-historical',
3080
+ },
3081
+ }
3082
+ : value);
3083
+ let activePlanRiskAudit = null;
3084
+ let planRiskAuditRecovery = null;
3085
+ if (activeTask.planHash) {
3086
+ try {
3087
+ activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
3088
+ }
3089
+ catch (error) {
3090
+ if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
3091
+ throw error;
3092
+ planRiskAuditRecovery = {
3093
+ scope: 'task',
3094
+ id: activeTask.id,
3095
+ status: activeTask.status,
3096
+ action: 'task plan-risk-audit',
3097
+ revision: activeTask.revision,
3098
+ planRiskAuditRecovery: {
3099
+ state: 'required',
3100
+ reason: error.message,
3101
+ currentBriefHash: activeTask.briefHash,
3102
+ currentPlanHash: activeTask.planHash,
3103
+ knowledgeMapRevision: activeTask.knowledgeMapRevision,
3104
+ knowledgeMapHash: activeTask.knowledgeMapHash,
3105
+ },
3106
+ };
3107
+ }
3108
+ }
2164
3109
  const taskNext = activeTask.status === 'planning'
2165
3110
  ? null
2166
- : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
3111
+ : planRiskAuditRecovery
3112
+ ?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2167
3113
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
2168
3114
  // strict review are executable while context refresh correctly remains forbidden.
2169
3115
  const hardStop = taskNext?.action === 'task corrective-decision'
2170
3116
  && taskNext.correctiveDecisionGate?.hardStop === true;
3117
+ const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
2171
3118
  if (taskNext?.action === 'task step-complete'
2172
3119
  || taskNext?.action === 'task step-review'
2173
3120
  || taskNext?.action === 'task remediation-mode-recover'
2174
- || hardStop) {
2175
- return withAdoption(taskNext);
3121
+ || hardStop
3122
+ || terminalCorrectiveDecision === 'split-required'
3123
+ || terminalCorrectiveDecision === 'stop-escalate') {
3124
+ return withAdoption(withMilestoneCancellation(taskNext));
2176
3125
  }
2177
3126
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
2178
- return withAdoption(taskNext);
3127
+ return withAdoption(withMilestoneCancellation(taskNext));
3128
+ }
3129
+ let knowledgeMap;
3130
+ try {
3131
+ knowledgeMap = inspectCurrentKnowledgeMap();
3132
+ }
3133
+ catch (error) {
3134
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3135
+ throw error;
3136
+ return withAdoption(withMilestoneCancellation({
3137
+ scope: 'project-memory',
3138
+ status: 'missing',
3139
+ action: 'project-memory scan',
3140
+ stalenessOriginTaskId: activeTask.id,
3141
+ authorizesTask: false,
3142
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
3143
+ }));
2179
3144
  }
2180
- const knowledgeMap = this.knowledgeMapStatus(repository);
2181
3145
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
2182
3146
  const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
2183
3147
  const delegatedApprovalOptions = action === 'project-memory approve'
@@ -2193,12 +3157,28 @@ export class WorkflowService {
2193
3157
  const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
2194
3158
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2195
3159
  : [];
2196
- return withAdoption({
2197
- scope: 'project-memory',
3160
+ if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
3161
+ const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
3162
+ if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
3163
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
3164
+ taskId: requestedTask.id,
3165
+ blockingTaskId: refreshOwner.task.id,
3166
+ requiredAction: 'Run repository next and its exact task context-refresh action first.',
3167
+ });
3168
+ }
3169
+ }
3170
+ return withAdoption(withMilestoneCancellation({
3171
+ scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2198
3172
  status: knowledgeMap.status,
2199
- action,
3173
+ action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
2200
3174
  revision: knowledgeMap.revision,
2201
- taskId: activeTask.id,
3175
+ ...(contextRefreshOptions.length > 0
3176
+ ? { taskId: activeTask.id }
3177
+ : {
3178
+ stalenessOriginTaskId: activeTask.id,
3179
+ authorizesTask: false,
3180
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
3181
+ }),
2202
3182
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2203
3183
  ...(contextRefreshOptions.length > 0
2204
3184
  ? {
@@ -2219,14 +3199,14 @@ export class WorkflowService {
2219
3199
  },
2220
3200
  }
2221
3201
  : {}),
2222
- });
3202
+ }));
2223
3203
  }
2224
3204
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
2225
3205
  if (taskCanRebindKnowledge(activeTask) &&
2226
3206
  activeTask.planHash &&
2227
3207
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
2228
3208
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
2229
- return withAdoption({
3209
+ return withAdoption(withMilestoneCancellation({
2230
3210
  scope: 'task',
2231
3211
  id: activeTask.id,
2232
3212
  status: activeTask.status,
@@ -2234,12 +3214,69 @@ export class WorkflowService {
2234
3214
  revision: activeTask.revision,
2235
3215
  knowledgeMapRevision: knowledgeMap.revision,
2236
3216
  knowledgeMapHash,
2237
- });
3217
+ }));
2238
3218
  }
2239
- return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
3219
+ return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
3220
+ }
3221
+ let repositoryKnowledgeMap;
3222
+ try {
3223
+ repositoryKnowledgeMap = inspectCurrentKnowledgeMap();
3224
+ }
3225
+ catch (error) {
3226
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
3227
+ throw error;
3228
+ return withAdoption({
3229
+ scope: 'project-memory',
3230
+ status: 'missing',
3231
+ action: 'project-memory scan',
3232
+ authorizesMilestone: false,
3233
+ provenanceNotice: 'Project Knowledge must be current before Discovery or Milestone authorization.',
3234
+ });
3235
+ }
3236
+ if (repositoryKnowledgeMap.status !== 'active' || !repositoryKnowledgeMap.approval) {
3237
+ const action = repositoryKnowledgeMap.status === 'stale'
3238
+ ? 'project-memory reconcile'
3239
+ : 'project-memory approve';
3240
+ const delegatedApprovalOptions = action === 'project-memory approve'
3241
+ ? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', repositoryKnowledgeMap)
3242
+ : [];
3243
+ return withAdoption({
3244
+ scope: 'project-memory',
3245
+ status: repositoryKnowledgeMap.status,
3246
+ action,
3247
+ revision: repositoryKnowledgeMap.revision,
3248
+ authorizesMilestone: false,
3249
+ provenanceNotice: 'Project Knowledge must be reconciled and approved before Discovery or Milestone authorization.',
3250
+ ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3251
+ });
2240
3252
  }
2241
3253
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2242
3254
  if (activeMilestone) {
3255
+ const initialAssembly = status.milestoneInitialAssembly
3256
+ .find((candidate) => candidate.milestoneId === activeMilestone.id) ?? null;
3257
+ if (initialAssembly) {
3258
+ const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
3259
+ if (activeDiscovery) {
3260
+ return withAdoption({
3261
+ scope: 'discovery',
3262
+ id: activeDiscovery.id,
3263
+ status: activeDiscovery.status,
3264
+ action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
3265
+ readiness: assessDiscovery(activeDiscovery),
3266
+ milestoneInitialAssembly: initialAssembly,
3267
+ });
3268
+ }
3269
+ return withAdoption({
3270
+ scope: 'milestone',
3271
+ id: activeMilestone.id,
3272
+ status: activeMilestone.status,
3273
+ action: 'milestone initial-assembly',
3274
+ revision: activeMilestone.revision,
3275
+ linkedTaskIds: initialAssembly.linkedTaskIds,
3276
+ executionAvailable: initialAssembly.executionAvailable,
3277
+ milestoneInitialAssembly: initialAssembly,
3278
+ });
3279
+ }
2243
3280
  if (activeMilestone.status === 'awaiting_final_acceptance') {
2244
3281
  const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
2245
3282
  const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
@@ -2290,6 +3327,19 @@ export class WorkflowService {
2290
3327
  status: activeMilestone.status,
2291
3328
  action: nextForMilestone(activeMilestone),
2292
3329
  revision: activeMilestone.revision,
3330
+ ...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
3331
+ ? {
3332
+ milestoneAuthorizationOptions: [
3333
+ { action: 'milestone authorize', mode: 'direct-human' },
3334
+ {
3335
+ action: 'milestone autonomy-prepare',
3336
+ mode: 'delegated',
3337
+ requiresExactHumanConfirmation: true,
3338
+ maximumDurationHours: 72,
3339
+ },
3340
+ ],
3341
+ }
3342
+ : {}),
2293
3343
  ...(activeMilestone.status === 'active'
2294
3344
  ? {
2295
3345
  completionPolicy: {
@@ -2446,7 +3496,20 @@ export class WorkflowService {
2446
3496
  throw error;
2447
3497
  }
2448
3498
  }
2449
- const next = nextForTask(task);
3499
+ let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
3500
+ ? guardedCorrectivePosture
3501
+ : null;
3502
+ if (!remediationModeRecovery && planRiskAudit && guardedCorrectivePosture === null) {
3503
+ try {
3504
+ catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
3505
+ }
3506
+ catch (error) {
3507
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
3508
+ if (!remediationModeRecovery)
3509
+ throw error;
3510
+ }
3511
+ }
3512
+ const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
2450
3513
  const handoffPosture = readTaskC1Posture(this.store, task);
2451
3514
  const strictStepReviewStep = planRiskAudit
2452
3515
  ? task.steps.find((step) => {
@@ -2458,6 +3521,11 @@ export class WorkflowService {
2458
3521
  return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
2459
3522
  }) ?? null
2460
3523
  : null;
3524
+ const strictReviewInfrastructureFailures = strictStepReviewStep
3525
+ ? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
3526
+ : 0;
3527
+ const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
3528
+ || (task.status === 'blocked' && task.review.status === 'unverified');
2461
3529
  const handoffOverlay = handoffPosture.state === 'pending'
2462
3530
  ? {
2463
3531
  action: 'task claim',
@@ -2466,6 +3534,15 @@ export class WorkflowService {
2466
3534
  state: 'pending',
2467
3535
  targetActor: handoffPosture.targetActor,
2468
3536
  eventId: handoffPosture.latestEvent.eventId,
3537
+ credentialRecovery: {
3538
+ action: 'task handoff-replace',
3539
+ when: 'claim credential is unavailable',
3540
+ requiredActor: handoffPosture.latestEvent.actor,
3541
+ targetActor: handoffPosture.targetActor,
3542
+ grantId: handoffPosture.latestEvent.grantId,
3543
+ expectedRevision: task.revision,
3544
+ invalidatesPriorCredential: true,
3545
+ },
2469
3546
  },
2470
3547
  }
2471
3548
  : handoffPosture.state === 'claimed'
@@ -2488,6 +3565,63 @@ export class WorkflowService {
2488
3565
  },
2489
3566
  }
2490
3567
  : {};
3568
+ const correctiveReplanOverlay = (posture) => {
3569
+ const evaluation = evaluateTaskCorrectiveReplanAvailability(task, posture);
3570
+ const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
3571
+ const command = handoffPosture.state === 'claimed'
3572
+ ? 'task corrective-yield'
3573
+ : writerLease
3574
+ ? 'doctor'
3575
+ : 'task corrective-replan-prepare';
3576
+ if (evaluation.allowed && handoffPosture.state === 'none' && writerLease) {
3577
+ const navigation = correctiveReplanNavigation(evaluation);
3578
+ return {
3579
+ action: command,
3580
+ blockedAction: 'task corrective-replan-prepare',
3581
+ stepId: posture.stepId,
3582
+ navigation: {
3583
+ kind: 'blocked',
3584
+ transitionId: navigation.transitionId,
3585
+ semanticFingerprint: navigation.semanticFingerprint,
3586
+ command: navigation.command,
3587
+ blockingGuardIds: ['task.corrective-replan.no-writer-lease'],
3588
+ pendingInputGuardIds: navigation.pendingInputGuardIds,
3589
+ reasonCode: 'ACTIVE_WRITER_LEASE',
3590
+ },
3591
+ correctiveDecisionGate: {
3592
+ attemptCount: posture.attemptCount,
3593
+ decision: posture.correctiveDecision?.decision ?? null,
3594
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3595
+ requiredAction: 'The active non-C1 writer lease must expire and be explicitly repaired before corrective replan preparation.',
3596
+ writerLease: {
3597
+ owner: writerLease.owner,
3598
+ expiresAt: writerLease.expiresAt,
3599
+ },
3600
+ },
3601
+ };
3602
+ }
3603
+ return evaluation.allowed
3604
+ ? {
3605
+ action: command,
3606
+ blockedAction: 'task plan-set',
3607
+ stepId: posture.stepId,
3608
+ navigation: { ...correctiveReplanNavigation(evaluation), command },
3609
+ correctiveDecisionGate: {
3610
+ attemptCount: posture.attemptCount,
3611
+ decision: posture.correctiveDecision?.decision ?? null,
3612
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3613
+ requiredAction: handoffPosture.state === 'claimed'
3614
+ ? 'Yield the exact C1 claim and writer lease before preparing replacement authority.'
3615
+ : 'Prepare a state-bound candidate, confirm the Human gate, then execute its exact receipt.',
3616
+ },
3617
+ }
3618
+ : {
3619
+ action: 'doctor',
3620
+ blockedAction: 'task plan-set',
3621
+ stepId: posture.stepId,
3622
+ navigation: correctiveReplanBlockedNavigation(evaluation),
3623
+ };
3624
+ };
2491
3625
  let guardedCorrectiveOverlay = {};
2492
3626
  if (remediationModeRecovery) {
2493
3627
  guardedCorrectiveOverlay = {
@@ -2506,7 +3640,12 @@ export class WorkflowService {
2506
3640
  },
2507
3641
  };
2508
3642
  }
2509
- if (!remediationModeRecovery && !strictStepReviewStep && guardedCorrectivePosture) {
3643
+ if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
3644
+ guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
3645
+ }
3646
+ if (!remediationModeRecovery
3647
+ && guardedCorrectivePosture
3648
+ && (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
2510
3649
  switch (guardedCorrectivePosture.posture) {
2511
3650
  case 'decision-required':
2512
3651
  if (guardedCorrectivePosture.correctiveDecision
@@ -2581,18 +3720,59 @@ export class WorkflowService {
2581
3720
  };
2582
3721
  break;
2583
3722
  case 'replan-required':
2584
- guardedCorrectiveOverlay = {
2585
- action: 'task plan-set',
2586
- stepId: guardedCorrectivePosture.stepId,
2587
- correctiveDecisionGate: {
2588
- attemptCount: guardedCorrectivePosture.attemptCount,
2589
- decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2590
- decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2591
- requiredAction: 'Record an actual new Plan posture before another remediation run.',
2592
- },
2593
- };
3723
+ guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
2594
3724
  break;
2595
3725
  case 'split-required':
3726
+ {
3727
+ const replacementDiscoveries = this.store.listDiscoveries(projectId)
3728
+ .filter((discovery) => discovery.status === 'ready_to_materialize');
3729
+ const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
3730
+ const replacementMilestone = task.milestoneId
3731
+ ? this.store.readMilestone(projectId, task.milestoneId)
3732
+ : null;
3733
+ guardedCorrectiveOverlay = {
3734
+ action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
3735
+ ...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
3736
+ stepId: guardedCorrectivePosture.stepId,
3737
+ correctiveDecisionGate: {
3738
+ attemptCount: guardedCorrectivePosture.attemptCount,
3739
+ decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
3740
+ decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
3741
+ humanApprovalRequired: false,
3742
+ },
3743
+ replacementGate: {
3744
+ milestoneId: task.milestoneId,
3745
+ oldTaskId: task.id,
3746
+ requiredDiscoveryStatus: 'ready_to_materialize',
3747
+ oldMembershipDisposition: 'cancelled',
3748
+ replacementMembershipDisposition: 'required',
3749
+ transaction: 'journal-recoverable',
3750
+ discoveryCandidates: replacementDiscoveries.map((discovery) => ({
3751
+ id: discovery.id,
3752
+ revision: discovery.revision,
3753
+ goal: discovery.goal,
3754
+ })),
3755
+ ...(replacementDiscovery && replacementMilestone
3756
+ ? {
3757
+ exactTransition: {
3758
+ command: 'task replacement-materialize',
3759
+ taskId: task.id,
3760
+ expectedTaskRevision: task.revision,
3761
+ discoveryId: replacementDiscovery.id,
3762
+ expectedDiscoveryRevision: replacementDiscovery.revision,
3763
+ milestoneId: replacementMilestone.id,
3764
+ expectedMilestoneRevision: replacementMilestone.revision,
3765
+ },
3766
+ }
3767
+ : {
3768
+ requiredAction: replacementDiscoveries.length === 0
3769
+ ? 'Create and complete one thin replacement Discovery, then run next again.'
3770
+ : 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
3771
+ }),
3772
+ },
3773
+ };
3774
+ }
3775
+ break;
2596
3776
  case 'stop-escalate':
2597
3777
  guardedCorrectiveOverlay = {
2598
3778
  action: 'doctor',
@@ -2624,6 +3804,30 @@ export class WorkflowService {
2624
3804
  strictStepReview: {
2625
3805
  reviewRequired: true,
2626
3806
  completionCommit: strictStepReviewStep.evidence.commitSha,
3807
+ infrastructureFailures: strictReviewInfrastructureFailures,
3808
+ maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
3809
+ recommendedMode: 'external-sealed',
3810
+ externalSealedReview: {
3811
+ packetCommand: 'task step-review-packet',
3812
+ recordCommand: 'task step-review-record',
3813
+ reviewerMustUseSeparateChat: true,
3814
+ repositorySealRequired: true,
3815
+ },
3816
+ },
3817
+ }
3818
+ : {}),
3819
+ ...(strictTaskReviewRequired
3820
+ ? {
3821
+ strictTaskReview: {
3822
+ reviewRequired: true,
3823
+ localInfrastructureFailureObserved: task.review.status === 'unverified',
3824
+ recommendedMode: 'external-sealed',
3825
+ externalSealedReview: {
3826
+ packetCommand: 'task review-packet',
3827
+ recordCommand: 'task review-sealed-record',
3828
+ reviewerMustUseSeparateChat: true,
3829
+ repositorySealRequired: true,
3830
+ },
2627
3831
  },
2628
3832
  }
2629
3833
  : {}),
@@ -2653,13 +3857,65 @@ export class WorkflowService {
2653
3857
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2654
3858
  };
2655
3859
  if (handoffPosture.state === 'pending') {
2656
- return {
3860
+ return this.withTaskNavigationContracts(task, {
2657
3861
  ...result,
2658
3862
  action: 'task claim',
2659
3863
  blockedAction: next.action,
2660
- };
3864
+ });
2661
3865
  }
2662
- return result;
3866
+ const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
3867
+ .includes(String(result.action))
3868
+ ? {
3869
+ ...result,
3870
+ taskPlanContract: this.taskPlanContract(projectId, task),
3871
+ }
3872
+ : result;
3873
+ return this.withTaskNavigationContracts(task, navigableResult);
3874
+ }
3875
+ taskPlanContract(projectId, task) {
3876
+ const map = this.store.readKnowledgeMap(projectId);
3877
+ return {
3878
+ requiredRequirementIds: [...task.requirements],
3879
+ requiredAcceptanceIds: [...task.acceptance],
3880
+ currentRisks: [...task.risks],
3881
+ exactInclusionRequired: true,
3882
+ briefHash: task.briefHash,
3883
+ knowledgeMap: knowledgeSummary(map),
3884
+ planRiskAuditContract: {
3885
+ required: true,
3886
+ option: '--risk-audit-file',
3887
+ atomicWithPlanSet: true,
3888
+ plannerAuditorSeparationRequired: true,
3889
+ },
3890
+ };
3891
+ }
3892
+ assertTaskContextChangeAllowed(task) {
3893
+ const posture = this.taskTerminalCorrectivePosture(task);
3894
+ if (!posture)
3895
+ return;
3896
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
3897
+ taskId: task.id,
3898
+ stepId: posture.stepId,
3899
+ decision: posture.posture,
3900
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3901
+ requiredAction: posture.posture === 'split-required'
3902
+ ? 'Materialize an atomic replacement Task through the exact replacement route.'
3903
+ : 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
3904
+ });
3905
+ }
3906
+ taskHasTerminalCorrectiveDecision(task) {
3907
+ return this.taskTerminalCorrectivePosture(task) !== null;
3908
+ }
3909
+ taskTerminalCorrectivePosture(task) {
3910
+ if (!task.planHash)
3911
+ return null;
3912
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
3913
+ if (!planRiskAudit)
3914
+ return null;
3915
+ const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
3916
+ return posture?.posture === 'split-required' || posture?.posture === 'stop-escalate'
3917
+ ? posture
3918
+ : null;
2663
3919
  }
2664
3920
  delegatedApprovalOptions(projectId, transition, target) {
2665
3921
  return this.store.listDelegations(projectId)
@@ -2710,6 +3966,22 @@ export class WorkflowService {
2710
3966
  policyHash: grant.policyHash,
2711
3967
  }));
2712
3968
  }
3969
+ findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
3970
+ for (const task of tasks) {
3971
+ if (!taskIsActionableFromRepositoryNext(task, milestonesById)
3972
+ || !task.planHash
3973
+ || !taskCanRebindKnowledge(task)
3974
+ || this.taskHasTerminalCorrectiveDecision(task))
3975
+ continue;
3976
+ const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
3977
+ if (!assessment.eligible)
3978
+ continue;
3979
+ const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
3980
+ if (options.length > 0)
3981
+ return { task, assessment, options };
3982
+ }
3983
+ return null;
3984
+ }
2713
3985
  assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
2714
3986
  const grant = this.store.readDelegation(projectId, grantId);
2715
3987
  if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
@@ -2779,6 +4051,130 @@ export class WorkflowService {
2779
4051
  ? milestone
2780
4052
  : readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
2781
4053
  }
4054
+ listMilestonesObserved(projectId, milestones, posture) {
4055
+ if (!posture)
4056
+ return milestones;
4057
+ const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
4058
+ return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
4059
+ ? milestone
4060
+ : inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
4061
+ }
4062
+ taskGitNavigationOverride(task) {
4063
+ if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
4064
+ return null;
4065
+ }
4066
+ const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
4067
+ if (currentBase === task.baseCommit)
4068
+ return null;
4069
+ return {
4070
+ scope: 'task',
4071
+ id: task.id,
4072
+ status: task.status,
4073
+ action: 'task sync-base',
4074
+ revision: task.revision,
4075
+ baseSyncRequired: {
4076
+ baseBranch: task.baseBranch,
4077
+ recordedBaseCommit: task.baseCommit,
4078
+ currentBaseCommit: currentBase,
4079
+ },
4080
+ };
4081
+ }
4082
+ inspectDependencyProvenanceCandidate(snapshot, task) {
4083
+ const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4084
+ const now = this.now().getTime();
4085
+ return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
4086
+ activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
4087
+ staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
4088
+ pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
4089
+ + snapshot.observation.pendingMilestoneTransactions.length
4090
+ + snapshot.observation.pendingTaskTransactions.length,
4091
+ corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
4092
+ coreOperationCount: snapshot.observation.coreTaskOperations.length,
4093
+ verifiedHistoricalStepCommits: verifiedHistoricalStepReviewCommits(this.store, task),
4094
+ versions: {
4095
+ runtime: versions.runtime,
4096
+ declared: versions.declared,
4097
+ locked: versions.locked,
4098
+ installed: versions.installed,
4099
+ currentBranch: versions.currentBranch,
4100
+ baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
4101
+ },
4102
+ });
4103
+ }
4104
+ withTaskNavigationContracts(task, next) {
4105
+ const action = typeof next.action === 'string' ? next.action : null;
4106
+ if (!action)
4107
+ return next;
4108
+ const policy = gitMutationPolicyForAction(task, action);
4109
+ const c1Posture = readTaskC1Posture(this.store, task);
4110
+ const claimedC1Posture = c1Posture.state === 'claimed';
4111
+ const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
4112
+ const activeLease = writerTokenContract
4113
+ ? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
4114
+ : null;
4115
+ const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
4116
+ const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
4117
+ ? {
4118
+ action: 'task writer-credential-replace',
4119
+ when: 'the active writer credential is unavailable',
4120
+ requiredActor: writerCredentialRecoveryActor,
4121
+ expectedRevision: task.revision,
4122
+ invalidatesPriorCredential: true,
4123
+ acquiresLeaseWhenAbsent: true,
4124
+ }
4125
+ : null;
4126
+ return {
4127
+ ...next,
4128
+ ...(policy ? { gitMutationPolicy: policy } : {}),
4129
+ ...(writerTokenContract ? { writerTokenContract } : {}),
4130
+ ...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
4131
+ };
4132
+ }
4133
+ writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
4134
+ if (claimedActor)
4135
+ return claimedActor;
4136
+ if (activeLeaseOwner)
4137
+ return activeLeaseOwner;
4138
+ const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
4139
+ ? 'final_acceptance'
4140
+ : ['task step-complete', 'task submit'].includes(action)
4141
+ ? 'execution'
4142
+ : null;
4143
+ if (!authorizationKind)
4144
+ return null;
4145
+ return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
4146
+ }
4147
+ describeMilestoneInitialAssembly(projectId, milestone, tasks) {
4148
+ const assessment = assessMilestoneInitialAssembly({
4149
+ store: this.store,
4150
+ projectId,
4151
+ milestone,
4152
+ listTasks: () => tasks,
4153
+ });
4154
+ if (!assessment.eligible)
4155
+ return null;
4156
+ const options = [
4157
+ {
4158
+ action: 'linked-task-assembly',
4159
+ commands: ['discovery start', 'discovery materialize'],
4160
+ description: 'Start or materialize the next linked Task Discovery for this Milestone before closing the initial membership Plan.',
4161
+ },
4162
+ ];
4163
+ if (assessment.orphanTaskIds.length > 0) {
4164
+ options.push({
4165
+ action: 'milestone plan-set',
4166
+ commands: ['milestone plan-set'],
4167
+ description: 'Record one complete initial Milestone Plan that classifies every currently linked Task.',
4168
+ });
4169
+ }
4170
+ return {
4171
+ milestoneId: milestone.id,
4172
+ revision: milestone.revision,
4173
+ linkedTaskIds: assessment.orphanTaskIds,
4174
+ executionAvailable: false,
4175
+ options,
4176
+ };
4177
+ }
2782
4178
  prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
2783
4179
  validateMilestonePlan(plan);
2784
4180
  return prepareMilestoneScopeChangeCandidate({
@@ -2788,6 +4184,12 @@ export class WorkflowService {
2788
4184
  actor,
2789
4185
  });
2790
4186
  }
4187
+ publicWriterLeaseDiagnostic(lease) {
4188
+ return {
4189
+ ...toPublicWriterLease(lease, this.now()),
4190
+ tokenFingerprint: createHash('sha256').update(lease.token).digest('hex').slice(0, 16),
4191
+ };
4192
+ }
2791
4193
  proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
2792
4194
  const currentLease = context.locks.inspect(taskId);
2793
4195
  if (!currentLease) {
@@ -2877,9 +4279,12 @@ export class WorkflowService {
2877
4279
  storedKnowledgeMapHash: storedHash,
2878
4280
  });
2879
4281
  }
2880
- inspectAdoptionPreparation(identity, tasks, milestones) {
2881
- const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
2882
- const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
4282
+ inspectAdoptionPreparation(identity, tasks, milestones, observedLeases, observedPosture) {
4283
+ const leases = observedLeases
4284
+ ?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
4285
+ const currentPosture = observedPosture === undefined
4286
+ ? readCurrentAdoptionPosture(this.store, identity.projectId)
4287
+ : observedPosture;
2883
4288
  if (currentPosture) {
2884
4289
  assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
2885
4290
  }
@@ -2892,7 +4297,7 @@ export class WorkflowService {
2892
4297
  tasks,
2893
4298
  milestones,
2894
4299
  sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
2895
- blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
4300
+ blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, leases),
2896
4301
  now: this.now(),
2897
4302
  }, currentPosture);
2898
4303
  }
@@ -3434,13 +4839,13 @@ function nextForTask(task) {
3434
4839
  awaiting_execution_authorization: 'task authorize',
3435
4840
  ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
3436
4841
  in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
3437
- validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
4842
+ validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
3438
4843
  needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
3439
4844
  awaiting_final_acceptance: 'task accept',
3440
4845
  merging: 'task merge',
3441
4846
  ready_to_merge: 'task merge-confirm',
3442
4847
  merged: 'none',
3443
- blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
4848
+ blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
3444
4849
  cancelled: 'none',
3445
4850
  };
3446
4851
  const selectedStep = activeStep ?? failedStep ?? runnableStep;
@@ -3462,6 +4867,95 @@ function nextForTask(task) {
3462
4867
  : {}),
3463
4868
  };
3464
4869
  }
4870
+ function gitMutationPolicyForAction(task, action) {
4871
+ const metadataWrites = ['.git/index', '.git/index.lock', '.git/HEAD', '.git/refs', '.git/logs'];
4872
+ switch (action) {
4873
+ case 'task start':
4874
+ return {
4875
+ permission: 'git-metadata-write',
4876
+ requiredBeforeFirstCall: true,
4877
+ boundedToTransition: true,
4878
+ transition: 'task start',
4879
+ appliesWhen: { workspaceOwner: 'local' },
4880
+ metadataWrites,
4881
+ };
4882
+ case 'task step-complete':
4883
+ return {
4884
+ permission: 'git-metadata-write',
4885
+ requiredBeforeFirstCall: true,
4886
+ boundedToTransition: true,
4887
+ transition: 'task step-complete',
4888
+ metadataWrites,
4889
+ };
4890
+ case 'task merge':
4891
+ if (task.workspaceOwner !== 'local')
4892
+ return null;
4893
+ return {
4894
+ permission: 'git-metadata-write',
4895
+ requiredBeforeFirstCall: true,
4896
+ boundedToTransition: true,
4897
+ transition: 'task merge',
4898
+ metadataWrites,
4899
+ };
4900
+ case 'task sync-base':
4901
+ return {
4902
+ permission: 'git-metadata-write',
4903
+ requiredBeforeFirstCall: true,
4904
+ boundedToTransition: true,
4905
+ transition: 'task sync-base',
4906
+ metadataWrites,
4907
+ };
4908
+ default:
4909
+ return null;
4910
+ }
4911
+ }
4912
+ function writerTokenContractForAction(action, claimedC1Posture = false) {
4913
+ const alwaysLeaseBound = ['task step-complete', 'task submit', 'task merge', 'task merge-confirm'];
4914
+ const claimedC1LeaseBound = [
4915
+ 'task run',
4916
+ 'task step-review',
4917
+ 'task step-review-record',
4918
+ 'task review-record',
4919
+ 'task review-launch',
4920
+ 'task review-sealed-record',
4921
+ 'task corrective-decision-recover',
4922
+ 'task remediation-mode-recover',
4923
+ 'task corrective-decision',
4924
+ 'task corrective-yield',
4925
+ 'task handoff',
4926
+ 'task handoff-prepare',
4927
+ 'task handback',
4928
+ 'task handback-create',
4929
+ ];
4930
+ if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
4931
+ return null;
4932
+ }
4933
+ return {
4934
+ required: true,
4935
+ option: '--writer-token',
4936
+ source: 'active-writer-lease',
4937
+ sensitive: true,
4938
+ renewalDisposition: 'reuse-presented-token',
4939
+ tokenReissuedOnRenewal: false,
4940
+ };
4941
+ }
4942
+ function observationPreflightBlockers(observation) {
4943
+ const messages = {
4944
+ PROJECT_REGISTRATION_MISSING: 'Workflow project registration is missing.',
4945
+ PROJECT_REPOSITORY_ROOT_UNBOUND: 'The current repository root is not bound to the Workflow project.',
4946
+ PROJECT_RECOVERY_REQUIRED: 'A project transaction requires explicit recovery.',
4947
+ MILESTONE_RECOVERY_REQUIRED: 'A Milestone transaction requires explicit recovery.',
4948
+ TASK_RECOVERY_REQUIRED: 'A Task corrective-replan transaction requires explicit recovery.',
4949
+ TASK_TRANSACTION_INTEGRITY_FAILED: 'A Task transaction journal failed integrity validation.',
4950
+ TASK_OPERATION_IN_PROGRESS: 'A Core Task operation is in progress.',
4951
+ STALE_TASK_OPERATION: 'A stale Core Task operation requires bounded repair.',
4952
+ ADOPTION_MISSING: 'Workflow lifecycle adoption is missing.',
4953
+ };
4954
+ return observation.reasonCodes.flatMap((reasonCode) => {
4955
+ const message = messages[reasonCode];
4956
+ return message ? [message] : [];
4957
+ });
4958
+ }
3465
4959
  function knowledgeSummary(map) {
3466
4960
  return {
3467
4961
  revision: map.revision,