codex-workflow-v2 2.0.0-beta.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 (156) hide show
  1. package/README.md +52 -4
  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 +12 -0
  8. package/dist/src/alpha6/milestone.js +113 -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/cli.js +473 -45
  19. package/dist/src/cli.js.map +1 -1
  20. package/dist/src/contracts.d.ts +169 -5
  21. package/dist/src/contracts.js +1 -1
  22. package/dist/src/credential-output.d.ts +10 -0
  23. package/dist/src/credential-output.js +73 -0
  24. package/dist/src/credential-output.js.map +1 -0
  25. package/dist/src/dependency-provenance.d.ts +19 -0
  26. package/dist/src/dependency-provenance.js +185 -0
  27. package/dist/src/dependency-provenance.js.map +1 -0
  28. package/dist/src/errors.d.ts +1 -1
  29. package/dist/src/errors.js.map +1 -1
  30. package/dist/src/git.js +5 -4
  31. package/dist/src/git.js.map +1 -1
  32. package/dist/src/index.d.ts +1 -1
  33. package/dist/src/index.js +1 -1
  34. package/dist/src/index.js.map +1 -1
  35. package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
  36. package/dist/src/lifecycle/canonical-hash.js +24 -0
  37. package/dist/src/lifecycle/canonical-hash.js.map +1 -0
  38. package/dist/src/lifecycle/catalog.d.ts +26 -0
  39. package/dist/src/lifecycle/catalog.js +208 -0
  40. package/dist/src/lifecycle/catalog.js.map +1 -0
  41. package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
  42. package/dist/src/lifecycle/compiler-inspection.js +29 -0
  43. package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
  44. package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
  45. package/dist/src/lifecycle/core-static-readiness.js +130 -0
  46. package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
  47. package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
  48. package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
  49. package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
  50. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
  51. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
  52. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
  53. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
  54. package/dist/src/lifecycle/corrective-replan-credential-core.js +563 -0
  55. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
  56. package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
  57. package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
  58. package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
  59. package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
  60. package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
  61. package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
  62. package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
  63. package/dist/src/lifecycle/corrective-replan.js +548 -0
  64. package/dist/src/lifecycle/corrective-replan.js.map +1 -0
  65. package/dist/src/lifecycle/evaluator.d.ts +2 -0
  66. package/dist/src/lifecycle/evaluator.js +35 -0
  67. package/dist/src/lifecycle/evaluator.js.map +1 -0
  68. package/dist/src/lifecycle/fingerprint.d.ts +31 -0
  69. package/dist/src/lifecycle/fingerprint.js +388 -0
  70. package/dist/src/lifecycle/fingerprint.js.map +1 -0
  71. package/dist/src/lifecycle/immutable.d.ts +1 -0
  72. package/dist/src/lifecycle/immutable.js +10 -0
  73. package/dist/src/lifecycle/immutable.js.map +1 -0
  74. package/dist/src/lifecycle/implementation-table.d.ts +29 -0
  75. package/dist/src/lifecycle/implementation-table.js +87 -0
  76. package/dist/src/lifecycle/implementation-table.js.map +1 -0
  77. package/dist/src/lifecycle/index.d.ts +4 -0
  78. package/dist/src/lifecycle/index.js +5 -0
  79. package/dist/src/lifecycle/index.js.map +1 -0
  80. package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
  81. package/dist/src/lifecycle/schema-artifact.js +214 -0
  82. package/dist/src/lifecycle/schema-artifact.js.map +1 -0
  83. package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
  84. package/dist/src/lifecycle/semantic-registry.js +489 -0
  85. package/dist/src/lifecycle/semantic-registry.js.map +1 -0
  86. package/dist/src/lifecycle/types.d.ts +718 -0
  87. package/dist/src/lifecycle/types.js +4 -0
  88. package/dist/src/lifecycle/types.js.map +1 -0
  89. package/dist/src/observation.d.ts +20 -0
  90. package/dist/src/observation.js +171 -0
  91. package/dist/src/observation.js.map +1 -0
  92. package/dist/src/reviewer.d.ts +43 -1
  93. package/dist/src/reviewer.js +203 -50
  94. package/dist/src/reviewer.js.map +1 -1
  95. package/dist/src/state/corrective-replan-executor.d.ts +64 -0
  96. package/dist/src/state/corrective-replan-executor.js +608 -0
  97. package/dist/src/state/corrective-replan-executor.js.map +1 -0
  98. package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
  99. package/dist/src/state/corrective-replan-public-schema.js +43 -0
  100. package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
  101. package/dist/src/state/corrective-replan-public.d.ts +95 -0
  102. package/dist/src/state/corrective-replan-public.js +665 -0
  103. package/dist/src/state/corrective-replan-public.js.map +1 -0
  104. package/dist/src/state/corrective-replan-transaction.d.ts +160 -0
  105. package/dist/src/state/corrective-replan-transaction.js +730 -0
  106. package/dist/src/state/corrective-replan-transaction.js.map +1 -0
  107. package/dist/src/state/corrective-yield-executor.d.ts +31 -0
  108. package/dist/src/state/corrective-yield-executor.js +236 -0
  109. package/dist/src/state/corrective-yield-executor.js.map +1 -0
  110. package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
  111. package/dist/src/state/corrective-yield-transaction.js +591 -0
  112. package/dist/src/state/corrective-yield-transaction.js.map +1 -0
  113. package/dist/src/state/lock.d.ts +76 -3
  114. package/dist/src/state/lock.js +611 -30
  115. package/dist/src/state/lock.js.map +1 -1
  116. package/dist/src/state/store.d.ts +3 -0
  117. package/dist/src/state/store.js +65 -12
  118. package/dist/src/state/store.js.map +1 -1
  119. package/dist/src/transition-core.d.ts +67 -0
  120. package/dist/src/transition-core.js +318 -0
  121. package/dist/src/transition-core.js.map +1 -0
  122. package/dist/src/version.d.ts +1 -1
  123. package/dist/src/version.js +1 -1
  124. package/dist/src/version.js.map +1 -1
  125. package/dist/src/workflow.d.ts +83 -24
  126. package/dist/src/workflow.js +1255 -187
  127. package/dist/src/workflow.js.map +1 -1
  128. package/docs/autonomy-guardrails.md +40 -1
  129. package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
  130. package/docs/delegated-approval.md +26 -1
  131. package/docs/development-flow.md +32 -2
  132. package/docs/lifecycle/state-machine-stabilization.md +641 -0
  133. package/docs/release.md +17 -2
  134. package/docs/split-required-recovery.md +5 -4
  135. package/docs/stable-release-defect-register.md +465 -3
  136. package/docs/updating-existing-project.md +59 -4
  137. package/docs/validation-report.md +85 -56
  138. package/package.json +5 -1
  139. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
  140. package/plugins/codex-workflow-gateway/references/protocol.md +74 -3
  141. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
  142. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +246 -1
  143. package/references/state-machine.md +4 -1
  144. package/roles/delivery-coordinator.md +39 -0
  145. package/schemas/adoption-posture-event.schema.json +1 -1
  146. package/schemas/context-envelope.schema.json +1 -1
  147. package/schemas/corrective-replan-credentials.private.schema.json +98 -0
  148. package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
  149. package/schemas/corrective-replan-public.schema.json +142 -0
  150. package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
  151. package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
  152. package/schemas/remediation-event.schema.json +11 -1
  153. package/schemas/task-handoff-event.schema.json +1 -1
  154. package/schemas/task.schema.json +24 -1
  155. package/schemas/transition-definition.schema.json +2609 -0
  156. package/schemas/transition-payloads.schema.json +140 -0
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
5
+ import { inspectDependencyProvenanceRecovery } from './dependency-provenance.js';
5
6
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
6
7
  import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
7
8
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
@@ -9,24 +10,32 @@ import { pathAllowed, validatePlan } from './domain/validation.js';
9
10
  import { WorkflowError } from './errors.js';
10
11
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
11
12
  import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
12
- import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
+ import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
14
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
14
15
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
15
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, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
+ import { assessMilestoneInitialAssembly, applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
18
  import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
18
- import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
19
+ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
19
20
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
20
21
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
21
22
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
22
23
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
24
+ import { ensureDirectory } from './fs-utils.js';
23
25
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
24
26
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
25
- import { launchStrictStepReviewer } from './reviewer.js';
26
- 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';
27
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';
28
35
  import { createEntityId } from './ulid.js';
29
- 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;
30
39
  export class WorkflowService {
31
40
  store;
32
41
  now;
@@ -34,13 +43,42 @@ export class WorkflowService {
34
43
  this.store = store;
35
44
  this.now = now;
36
45
  }
37
- context(repository) {
46
+ #mutationContext(repository) {
38
47
  const identity = resolveRepositoryIdentity(repository);
39
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
+ }
40
73
  recoverProjectTransactions(projectRoot, identity.projectId);
41
74
  const projectFile = path.join(projectRoot, 'project.json');
42
75
  const projectAlreadyRegistered = existsSync(projectFile);
43
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);
44
82
  initializeProjectRegistrationAdoption(this.store, {
45
83
  projectId: identity.projectId,
46
84
  repositoryIdentity: identity,
@@ -54,17 +92,103 @@ export class WorkflowService {
54
92
  return {
55
93
  identity,
56
94
  store: this.store,
57
- 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,
58
182
  };
59
183
  }
60
184
  prepareDelegation(repository, input) {
61
- const { identity } = this.context(repository);
185
+ const { identity } = this.#mutationContext(repository);
62
186
  const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
63
187
  this.assertDelegationScopeExists(identity.projectId, gate.policy);
64
188
  return gate;
65
189
  }
66
190
  grantDelegation(repository, input, confirmationCode) {
67
- const { identity } = this.context(repository);
191
+ const { identity } = this.#mutationContext(repository);
68
192
  const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
69
193
  this.assertDelegationScopeExists(identity.projectId, gate.policy);
70
194
  if (confirmationCode.trim() !== gate.confirmationCode) {
@@ -100,7 +224,7 @@ export class WorkflowService {
100
224
  }, null);
101
225
  }
102
226
  revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
103
- const { identity } = this.context(repository);
227
+ const { identity } = this.#mutationContext(repository);
104
228
  const grant = this.store.readDelegation(identity.projectId, grantId);
105
229
  assertExpectedRevision(grant, expectedRevision);
106
230
  if (grant.status !== 'active') {
@@ -124,7 +248,7 @@ export class WorkflowService {
124
248
  startDiscovery(repository, input) {
125
249
  if (!input.goal.trim())
126
250
  throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
127
- const { identity } = this.context(repository);
251
+ const { identity } = this.#mutationContext(repository);
128
252
  const timestamp = this.now().toISOString();
129
253
  const initial = {
130
254
  schemaVersion: STATE_SCHEMA_VERSION,
@@ -152,7 +276,7 @@ export class WorkflowService {
152
276
  return this.store.writeDiscovery(initial, null);
153
277
  }
154
278
  updateDiscovery(repository, discoveryId, expectedRevision, patch) {
155
- const { identity } = this.context(repository);
279
+ const { identity } = this.#mutationContext(repository);
156
280
  const current = this.store.readDiscovery(identity.projectId, discoveryId);
157
281
  if (current.status === 'materialized' || current.status === 'abandoned') {
158
282
  throw new WorkflowError('TRANSITION_BLOCKED', `Discovery ${discoveryId} is ${current.status}.`);
@@ -176,7 +300,7 @@ export class WorkflowService {
176
300
  return this.store.writeDiscovery(updated, expectedRevision);
177
301
  }
178
302
  materializeTask(repository, discoveryId, expectedRevision, title, baseBranch = 'main', milestoneId = null) {
179
- const { identity } = this.context(repository);
303
+ const { identity } = this.#mutationContext(repository);
180
304
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
181
305
  assertExpectedRevision(discovery, expectedRevision);
182
306
  const readiness = assessDiscovery(discovery);
@@ -253,7 +377,7 @@ export class WorkflowService {
253
377
  return { discovery: savedDiscovery, task: savedTask, taskRoot };
254
378
  }
255
379
  replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
256
- const { identity, locks } = this.context(repository);
380
+ const { identity, locks } = this.#mutationContext(repository);
257
381
  const oldTask = this.store.readTask(identity.projectId, oldTaskId);
258
382
  assertExpectedRevision(oldTask, expectedOldTaskRevision);
259
383
  if (!oldTask.milestoneId) {
@@ -504,7 +628,7 @@ export class WorkflowService {
504
628
  };
505
629
  }
506
630
  materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
507
- const { identity } = this.context(repository);
631
+ const { identity } = this.#mutationContext(repository);
508
632
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
509
633
  assertExpectedRevision(discovery, expectedRevision);
510
634
  const readiness = assessDiscovery(discovery);
@@ -552,7 +676,7 @@ export class WorkflowService {
552
676
  };
553
677
  }
554
678
  setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
555
- const { identity } = this.context(repository);
679
+ const { identity } = this.#mutationContext(repository);
556
680
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
557
681
  assertExpectedRevision(milestone, expectedRevision);
558
682
  if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
@@ -624,7 +748,7 @@ export class WorkflowService {
624
748
  return saved;
625
749
  }
626
750
  prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
627
- const { identity } = this.context(repository);
751
+ const { identity } = this.#mutationContext(repository);
628
752
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
629
753
  assertExpectedRevision(milestone, expectedRevision);
630
754
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -641,7 +765,7 @@ export class WorkflowService {
641
765
  };
642
766
  }
643
767
  applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
644
- const { identity } = this.context(repository);
768
+ const { identity } = this.#mutationContext(repository);
645
769
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
646
770
  assertExpectedRevision(milestone, expectedRevision);
647
771
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -664,9 +788,10 @@ export class WorkflowService {
664
788
  });
665
789
  }
666
790
  prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
667
- const { identity } = this.context(repository);
791
+ const { identity } = this.#mutationContext(repository);
668
792
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
669
793
  assertExpectedRevision(milestone, expectedRevision);
794
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
670
795
  return prepareMilestoneAutonomyContract({
671
796
  projectId: identity.projectId,
672
797
  milestone,
@@ -677,9 +802,10 @@ export class WorkflowService {
677
802
  });
678
803
  }
679
804
  grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
680
- const { identity } = this.context(repository);
805
+ const { identity } = this.#mutationContext(repository);
681
806
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
682
807
  assertExpectedRevision(milestone, expectedRevision);
808
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
683
809
  return applyMilestoneAutonomyContract({
684
810
  store: this.store,
685
811
  projectId: identity.projectId,
@@ -692,7 +818,7 @@ export class WorkflowService {
692
818
  });
693
819
  }
694
820
  evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
695
- const { identity } = this.context(repository);
821
+ const { identity } = this.#mutationContext(repository);
696
822
  const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
697
823
  assertExpectedRevision(milestone, expectedRevision);
698
824
  assertMilestoneScopeChangeStatusAllowed(milestone);
@@ -722,12 +848,13 @@ export class WorkflowService {
722
848
  });
723
849
  }
724
850
  authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
725
- const { identity } = this.context(repository);
851
+ const { identity } = this.#mutationContext(repository);
726
852
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
727
853
  assertExpectedRevision(milestone, expectedRevision);
728
854
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
729
855
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
730
856
  }
857
+ this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
731
858
  const root = this.store.milestoneRoot(identity.projectId, milestoneId);
732
859
  const planHash = this.store.hashArtifact(root, 'plan.json');
733
860
  if (planHash !== milestone.planHash) {
@@ -751,7 +878,7 @@ export class WorkflowService {
751
878
  return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
752
879
  }
753
880
  validateMilestone(repository, milestoneId, expectedRevision) {
754
- const { identity } = this.context(repository);
881
+ const { identity } = this.#mutationContext(repository);
755
882
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
756
883
  assertExpectedRevision(milestone, expectedRevision);
757
884
  if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
@@ -814,7 +941,7 @@ export class WorkflowService {
814
941
  return saved;
815
942
  }
816
943
  acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
817
- const { identity } = this.context(repository);
944
+ const { identity } = this.#mutationContext(repository);
818
945
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
819
946
  assertExpectedRevision(milestone, expectedRevision);
820
947
  if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
@@ -853,7 +980,7 @@ export class WorkflowService {
853
980
  resultHash,
854
981
  evidenceHash,
855
982
  headCommit: currentHead,
856
- reason: `Confirmed with ${gate.confirmationCode}.`,
983
+ reason: 'Milestone final acceptance confirmed against the current validated state.',
857
984
  authorizationMode: delegation ? 'delegated' : 'human',
858
985
  ...(delegation ? { delegation } : {}),
859
986
  };
@@ -869,7 +996,7 @@ export class WorkflowService {
869
996
  cancelMilestone(repository, milestoneId, expectedRevision, reason) {
870
997
  if (!reason.trim())
871
998
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
872
- const { identity } = this.context(repository);
999
+ const { identity } = this.#mutationContext(repository);
873
1000
  const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
874
1001
  assertExpectedRevision(milestone, expectedRevision);
875
1002
  if (['accepted', 'cancelled'].includes(milestone.status)) {
@@ -879,21 +1006,59 @@ export class WorkflowService {
879
1006
  }
880
1007
  setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
881
1008
  validatePlan(plan);
882
- const { identity } = this.context(repository);
1009
+ const { identity } = this.#mutationContext(repository);
883
1010
  const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
884
1011
  this.validateKnowledgeTargets(knowledgeMap, plan);
885
1012
  const task = this.store.readTask(identity.projectId, taskId);
886
1013
  assertExpectedRevision(task, expectedRevision);
887
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
888
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
889
- }
1014
+ const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
890
1015
  const currentPlanCorrectiveDecisions = task.planHash
891
- ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
1016
+ ? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
892
1017
  : [];
893
1018
  const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
894
- 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
+ });
895
1025
  const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
896
- 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
+ }
897
1062
  if (currentHardBlockingDecisions.length > 0) {
898
1063
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
899
1064
  taskId: task.id,
@@ -923,9 +1088,6 @@ export class WorkflowService {
923
1088
  })),
924
1089
  });
925
1090
  }
926
- const currentPlanRiskAudit = task.status === 'needs_fix'
927
- ? readCurrentPlanRiskAudit(this.store, task)
928
- : null;
929
1091
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
930
1092
  if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
931
1093
  validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
@@ -968,7 +1130,7 @@ export class WorkflowService {
968
1130
  }
969
1131
  }
970
1132
  const root = this.store.taskRoot(identity.projectId, taskId);
971
- const stagedPlan = this.store.stageArtifact(root, 'plan.md', renderPlan(task, plan));
1133
+ const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
972
1134
  const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
973
1135
  ? { ...authorization, decision: 'superseded' }
974
1136
  : authorization);
@@ -985,12 +1147,17 @@ export class WorkflowService {
985
1147
  plannedSteps.every((step) => step.status === 'completed')) {
986
1148
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
987
1149
  }
988
- if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
989
- throw new WorkflowError('TRANSITION_BLOCKED', `Replacement Plan under replan-required must retain every guarded Step targeted by the current corrective decision; removing or renaming one would reset remediation history.`, {
990
- taskId: task.id,
991
- requiredStepIds: currentReplanRequiredStepIds,
992
- plannedStepIds: plannedSteps.map((step) => step.id),
993
- });
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
+ }
994
1161
  }
995
1162
  if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
996
1163
  throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
@@ -998,14 +1165,10 @@ export class WorkflowService {
998
1165
  stepId: guardedRemediationGate.stepId,
999
1166
  });
1000
1167
  }
1001
- if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
1002
- assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
1003
- appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
1004
- }
1005
1168
  const candidate = {
1006
1169
  ...task,
1007
1170
  status: 'awaiting_execution_authorization',
1008
- planHash: stagedPlan.hash,
1171
+ planHash: preparedPlan.planHash,
1009
1172
  planObjective: plan.objective,
1010
1173
  requirements: plan.requirements,
1011
1174
  acceptance: plan.acceptance,
@@ -1025,6 +1188,18 @@ export class WorkflowService {
1025
1188
  };
1026
1189
  if (planRiskAudit)
1027
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
+ }
1028
1203
  const saved = this.store.writeTask(candidate, expectedRevision);
1029
1204
  this.store.publishArtifact(stagedPlan);
1030
1205
  if (planRiskAudit)
@@ -1035,7 +1210,7 @@ export class WorkflowService {
1035
1210
  return saved;
1036
1211
  }
1037
1212
  rebindTaskKnowledge(repository, taskId, expectedRevision) {
1038
- const { identity } = this.context(repository);
1213
+ const { identity } = this.#mutationContext(repository);
1039
1214
  const task = this.store.readTask(identity.projectId, taskId);
1040
1215
  assertExpectedRevision(task, expectedRevision);
1041
1216
  this.assertTaskContextChangeAllowed(task);
@@ -1120,7 +1295,7 @@ export class WorkflowService {
1120
1295
  if (!delegationGrantId) {
1121
1296
  throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
1122
1297
  }
1123
- const { identity } = this.context(repository);
1298
+ const { identity } = this.#mutationContext(repository);
1124
1299
  const task = this.store.readTask(identity.projectId, taskId);
1125
1300
  const currentMap = this.store.readKnowledgeMap(identity.projectId);
1126
1301
  assertExpectedRevision(task, expectedTaskRevision);
@@ -1211,7 +1386,7 @@ export class WorkflowService {
1211
1386
  });
1212
1387
  }
1213
1388
  authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
1214
- const { identity } = this.context(repository);
1389
+ const { identity } = this.#mutationContext(repository);
1215
1390
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1216
1391
  const task = this.store.readTask(identity.projectId, taskId);
1217
1392
  assertExpectedRevision(task, expectedRevision);
@@ -1241,7 +1416,7 @@ export class WorkflowService {
1241
1416
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
1242
1417
  }
1243
1418
  recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
1244
- const { identity } = this.context(repository);
1419
+ const { identity } = this.#mutationContext(repository);
1245
1420
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1246
1421
  const task = this.store.readTask(identity.projectId, taskId);
1247
1422
  assertExpectedRevision(task, expectedRevision);
@@ -1249,7 +1424,7 @@ export class WorkflowService {
1249
1424
  return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
1250
1425
  }
1251
1426
  showTaskHandoff(repository, taskId) {
1252
- const { identity } = this.context(repository);
1427
+ const { identity } = this.#mutationContext(repository);
1253
1428
  const task = this.store.readTask(identity.projectId, taskId);
1254
1429
  return {
1255
1430
  task,
@@ -1257,8 +1432,8 @@ export class WorkflowService {
1257
1432
  claimTokenAvailable: false,
1258
1433
  };
1259
1434
  }
1260
- handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
1261
- 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);
1262
1437
  const task = this.store.readTask(context.identity.projectId, taskId);
1263
1438
  assertExpectedRevision(task, expectedRevision);
1264
1439
  const normalizedActor = actor.trim();
@@ -1316,6 +1491,7 @@ export class WorkflowService {
1316
1491
  expectedNext: String(nextForTask(task).action ?? 'task show'),
1317
1492
  grantId,
1318
1493
  expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
1494
+ replacePending,
1319
1495
  }, this.now());
1320
1496
  releaseLease();
1321
1497
  return { task, event: prepared.event, bundle: prepared.bundle };
@@ -1327,15 +1503,15 @@ export class WorkflowService {
1327
1503
  }
1328
1504
  }
1329
1505
  claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
1330
- const context = this.context(repository);
1506
+ const context = this.#mutationContext(repository);
1331
1507
  const task = this.store.readTask(context.identity.projectId, taskId);
1332
1508
  assertExpectedRevision(task, expectedRevision);
1333
1509
  const normalizedActor = actor.trim();
1334
1510
  if (!normalizedActor) {
1335
1511
  throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
1336
1512
  }
1337
- const pendingHandoff = readTaskC1Posture(this.store, task);
1338
- if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
1513
+ const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now());
1514
+ if (pendingHandoff.latestEvent.grantId) {
1339
1515
  this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
1340
1516
  }
1341
1517
  const existingLease = context.locks.inspect(taskId);
@@ -1368,7 +1544,14 @@ export class WorkflowService {
1368
1544
  }
1369
1545
  try {
1370
1546
  const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1371
- 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
+ };
1372
1555
  }
1373
1556
  catch (error) {
1374
1557
  if (acquiredHere)
@@ -1376,8 +1559,45 @@ export class WorkflowService {
1376
1559
  throw error;
1377
1560
  }
1378
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
+ }
1379
1599
  handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1380
- const context = this.context(repository);
1600
+ const context = this.#mutationContext(repository);
1381
1601
  const task = this.store.readTask(context.identity.projectId, taskId);
1382
1602
  assertExpectedRevision(task, expectedRevision);
1383
1603
  const lease = task.status === 'merged' || task.status === 'cancelled'
@@ -1389,7 +1609,7 @@ export class WorkflowService {
1389
1609
  return { task, event };
1390
1610
  }
1391
1611
  startTask(repository, taskId, expectedRevision, workspaceOwner) {
1392
- const { identity } = this.context(repository);
1612
+ const { identity } = this.#mutationContext(repository);
1393
1613
  const task = this.store.readTask(identity.projectId, taskId);
1394
1614
  assertExpectedRevision(task, expectedRevision);
1395
1615
  this.requireExecutionAdoptionPosture(identity, taskId);
@@ -1441,7 +1661,7 @@ export class WorkflowService {
1441
1661
  return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
1442
1662
  }
1443
1663
  runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1444
- const context = this.context(repository);
1664
+ const context = this.#mutationContext(repository);
1445
1665
  const task = this.store.readTask(context.identity.projectId, taskId);
1446
1666
  assertExpectedRevision(task, expectedRevision);
1447
1667
  this.requireExecutionAdoptionPosture(context.identity, taskId);
@@ -1470,6 +1690,7 @@ export class WorkflowService {
1470
1690
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
1471
1691
  }
1472
1692
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1693
+ let resumesCorrectiveReplan = false;
1473
1694
  if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1474
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.`, {
1475
1696
  taskId,
@@ -1484,20 +1705,21 @@ export class WorkflowService {
1484
1705
  && guardedPosture.correctiveDecision.planHash !== task.planHash
1485
1706
  ? guardedPosture.correctiveDecision
1486
1707
  : null;
1487
- const recovery = staleDecision
1708
+ const recovery = staleDecision?.decision === 'continue-fix'
1488
1709
  ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
1489
1710
  : null;
1490
- 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';
1491
1713
  }
1492
1714
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1493
1715
  const dirty = changedFiles(context.identity.repositoryRoot);
1494
- if (step.status === 'planned' && dirty.length > 0) {
1716
+ if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
1495
1717
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
1496
1718
  changedFiles: dirty,
1497
1719
  });
1498
1720
  }
1499
1721
  const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
1500
- if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
1722
+ if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
1501
1723
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
1502
1724
  changedFiles: dirtyOutsideStep,
1503
1725
  });
@@ -1555,7 +1777,8 @@ export class WorkflowService {
1555
1777
  }
1556
1778
  return {
1557
1779
  task: saved,
1558
- lease,
1780
+ lease: toPublicWriterLease(lease, this.now()),
1781
+ writerCredential: { disposition: acquiredHere ? 'issued' : 'renewed' },
1559
1782
  envelope: {
1560
1783
  protocolVersion: PROTOCOL_VERSION,
1561
1784
  packageVersion: PACKAGE_VERSION,
@@ -1591,14 +1814,14 @@ export class WorkflowService {
1591
1814
  taskId,
1592
1815
  stepId,
1593
1816
  expectedRevision: saved.revision,
1594
- writerToken: lease.token,
1817
+ writerToken: acquiredHere ? lease.token : null,
1595
1818
  knowledgeMap: knowledgeSummary(knowledgeMap),
1596
1819
  knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
1597
1820
  },
1598
1821
  };
1599
1822
  }
1600
1823
  taskContext(repository, taskId, role) {
1601
- const { identity } = this.context(repository);
1824
+ const { identity } = this.#mutationContext(repository);
1602
1825
  const task = this.store.readTask(identity.projectId, taskId);
1603
1826
  const historicalStrictReview = role === 'independent-reviewer'
1604
1827
  && task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
@@ -1677,7 +1900,7 @@ export class WorkflowService {
1677
1900
  };
1678
1901
  }
1679
1902
  completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
1680
- const context = this.context(repository);
1903
+ const context = this.#mutationContext(repository);
1681
1904
  const lease = context.locks.heartbeat(taskId, writerToken);
1682
1905
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1683
1906
  const task = this.store.readTask(context.identity.projectId, taskId);
@@ -1763,7 +1986,7 @@ export class WorkflowService {
1763
1986
  return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
1764
1987
  }
1765
1988
  reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
1766
- const context = this.context(repository);
1989
+ const context = this.#mutationContext(repository);
1767
1990
  this.readCurrentAdoptionPostureStrict(context.identity.projectId);
1768
1991
  const task = this.store.readTask(context.identity.projectId, taskId);
1769
1992
  assertExpectedRevision(task, expectedRevision);
@@ -1819,10 +2042,18 @@ export class WorkflowService {
1819
2042
  resolution: currentCycle.resolution,
1820
2043
  });
1821
2044
  }
1822
- 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
+ }
1823
2054
  }
1824
2055
  else if (currentCycle.resolution !== 'pending') {
1825
- if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
2056
+ if (currentCycle.resolution === 'failed') {
1826
2057
  if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1827
2058
  throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1828
2059
  taskId: task.id,
@@ -1830,7 +2061,7 @@ export class WorkflowService {
1830
2061
  resolution: currentCycle.resolution,
1831
2062
  });
1832
2063
  }
1833
- appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2064
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
1834
2065
  completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
1835
2066
  checksHash: currentCycle.reviewRecordedEvent.checksHash,
1836
2067
  reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
@@ -1848,7 +2079,7 @@ export class WorkflowService {
1848
2079
  reviewedCommit: launch.reviewedCommit,
1849
2080
  isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
1850
2081
  }, this.now());
1851
- if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
2082
+ if (recorded.resolution === 'failed') {
1852
2083
  const reviewRecordedEvent = recorded.reviewRecordedEvent;
1853
2084
  const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
1854
2085
  if (!reviewRecordedEvent || !reviewerAttestationEvent) {
@@ -1858,7 +2089,7 @@ export class WorkflowService {
1858
2089
  resolution: recorded.resolution,
1859
2090
  });
1860
2091
  }
1861
- appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
2092
+ appendRemediationEvent(this.store, task, stepId, 'review-failed', {
1862
2093
  completionCommit: reviewRecordedEvent.completionCommit,
1863
2094
  checksHash: reviewRecordedEvent.checksHash,
1864
2095
  reviewEventHash: reviewRecordedEvent.eventHash,
@@ -1867,8 +2098,132 @@ export class WorkflowService {
1867
2098
  }
1868
2099
  return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
1869
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
+ }
1870
2225
  submitTask(repository, taskId, expectedRevision, writerToken, actor) {
1871
- const context = this.context(repository);
2226
+ const context = this.#mutationContext(repository);
1872
2227
  const lease = context.locks.heartbeat(taskId, writerToken);
1873
2228
  const task = this.store.readTask(context.identity.projectId, taskId);
1874
2229
  assertExpectedRevision(task, expectedRevision);
@@ -1900,11 +2255,11 @@ export class WorkflowService {
1900
2255
  return saved;
1901
2256
  }
1902
2257
  recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
1903
- const { identity } = this.context(repository);
2258
+ const { identity } = this.#mutationContext(repository);
1904
2259
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1905
2260
  const task = this.store.readTask(identity.projectId, taskId);
1906
2261
  assertExpectedRevision(task, expectedRevision);
1907
- this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
2262
+ this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
1908
2263
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1909
2264
  const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
1910
2265
  if (task.status !== 'validating' && !retryingUnverified) {
@@ -1942,7 +2297,7 @@ export class WorkflowService {
1942
2297
  return saved;
1943
2298
  }
1944
2299
  recoverStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1945
- const context = this.context(repository);
2300
+ const context = this.#mutationContext(repository);
1946
2301
  const task = this.store.readTask(context.identity.projectId, taskId);
1947
2302
  assertExpectedRevision(task, expectedRevision);
1948
2303
  this.requireExecutionAdoptionPosture(context.identity, taskId);
@@ -1954,18 +2309,18 @@ export class WorkflowService {
1954
2309
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1955
2310
  }
1956
2311
  recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1957
- const context = this.context(repository);
2312
+ const context = this.#mutationContext(repository);
1958
2313
  const task = this.store.readTask(context.identity.projectId, taskId);
1959
2314
  assertExpectedRevision(task, expectedRevision);
1960
2315
  this.requireExecutionAdoptionPosture(context.identity, taskId);
1961
2316
  return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1962
2317
  }
1963
2318
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1964
- const { identity } = this.context(repository);
2319
+ const { identity } = this.#mutationContext(repository);
1965
2320
  this.readCurrentAdoptionPostureStrict(identity.projectId);
1966
2321
  const task = this.store.readTask(identity.projectId, taskId);
1967
2322
  assertExpectedRevision(task, expectedRevision);
1968
- this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
2323
+ this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
1969
2324
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1970
2325
  if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
1971
2326
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
@@ -2007,7 +2362,7 @@ export class WorkflowService {
2007
2362
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
2008
2363
  }
2009
2364
  setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
2010
- const { identity } = this.context(repository);
2365
+ const { identity } = this.#mutationContext(repository);
2011
2366
  const task = this.store.readTask(identity.projectId, taskId);
2012
2367
  assertExpectedRevision(task, expectedRevision);
2013
2368
  if (task.status !== 'validating' || task.review.status !== 'passed') {
@@ -2021,7 +2376,7 @@ export class WorkflowService {
2021
2376
  return saved;
2022
2377
  }
2023
2378
  acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
2024
- const { identity } = this.context(repository);
2379
+ const { identity } = this.#mutationContext(repository);
2025
2380
  const task = this.store.readTask(identity.projectId, taskId);
2026
2381
  assertExpectedRevision(task, expectedRevision);
2027
2382
  if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
@@ -2059,7 +2414,7 @@ export class WorkflowService {
2059
2414
  }, expectedRevision);
2060
2415
  }
2061
2416
  syncTaskBase(repository, taskId, expectedRevision) {
2062
- const { identity } = this.context(repository);
2417
+ const { identity } = this.#mutationContext(repository);
2063
2418
  const task = this.store.readTask(identity.projectId, taskId);
2064
2419
  assertExpectedRevision(task, expectedRevision);
2065
2420
  if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
@@ -2111,7 +2466,7 @@ export class WorkflowService {
2111
2466
  return saved;
2112
2467
  }
2113
2468
  mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
2114
- const context = this.context(repository);
2469
+ const context = this.#mutationContext(repository);
2115
2470
  const task = this.store.readTask(context.identity.projectId, taskId);
2116
2471
  assertExpectedRevision(task, expectedRevision);
2117
2472
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -2133,7 +2488,7 @@ export class WorkflowService {
2133
2488
  return saved;
2134
2489
  }
2135
2490
  confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
2136
- const context = this.context(repository);
2491
+ const context = this.#mutationContext(repository);
2137
2492
  const task = this.store.readTask(context.identity.projectId, taskId);
2138
2493
  assertExpectedRevision(task, expectedRevision);
2139
2494
  const lease = context.locks.heartbeat(taskId, writerToken);
@@ -2150,21 +2505,22 @@ export class WorkflowService {
2150
2505
  return saved;
2151
2506
  }
2152
2507
  status(repository) {
2153
- const { identity } = this.context(repository);
2154
- const tasks = this.store.listTasks(identity.projectId);
2155
- const rawMilestones = this.store.listMilestones(identity.projectId);
2156
- const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
2508
+ const snapshot = this.observationSnapshot(repository);
2157
2509
  return {
2158
- projectId: identity.projectId,
2159
- discoveries: this.store.listDiscoveries(identity.projectId),
2160
- tasks,
2161
- milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
2162
- delegations: this.store.listDelegations(identity.projectId),
2163
- adoption,
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,
2164
2520
  };
2165
2521
  }
2166
2522
  recoverMilestoneTransactions(repository) {
2167
- const { identity } = this.context(repository);
2523
+ const { identity } = this.#mutationContext(repository);
2168
2524
  const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
2169
2525
  return {
2170
2526
  projectId: identity.projectId,
@@ -2172,11 +2528,11 @@ export class WorkflowService {
2172
2528
  };
2173
2529
  }
2174
2530
  prepareAdoption(repository) {
2175
- const { identity } = this.context(repository);
2531
+ const { identity } = this.#mutationContext(repository);
2176
2532
  return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
2177
2533
  }
2178
2534
  applyAdoption(repository, actor, confirmationCode) {
2179
- const context = this.context(repository);
2535
+ const context = this.#mutationContext(repository);
2180
2536
  const tasks = context.store.listTasks(context.identity.projectId);
2181
2537
  const milestones = context.store.listMilestones(context.identity.projectId);
2182
2538
  return applyAdoptionPosture(this.store, {
@@ -2193,19 +2549,19 @@ export class WorkflowService {
2193
2549
  }, actor, confirmationCode);
2194
2550
  }
2195
2551
  updatePreflight(repository) {
2196
- const context = this.context(repository);
2197
- const tasks = context.store.listTasks(context.identity.projectId);
2198
- const milestones = context.store.listMilestones(context.identity.projectId);
2199
- const dirty = changedFiles(context.identity.repositoryRoot);
2552
+ const snapshot = this.observationSnapshot(repository);
2553
+ const tasks = snapshot.tasks;
2554
+ const milestones = snapshot.milestones;
2555
+ const dirty = changedFiles(snapshot.identity.repositoryRoot);
2200
2556
  const runningSteps = tasks.flatMap((task) => task.steps
2201
2557
  .filter((step) => step.status === 'in_progress')
2202
2558
  .map((step) => ({ taskId: task.id, stepId: step.id })));
2203
- const leases = context.locks.list();
2559
+ const leases = snapshot.leases;
2204
2560
  const now = this.now().getTime();
2205
2561
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
2206
2562
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2207
2563
  const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2208
- const inspectedVersions = inspectWorkflowVersions(context.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2564
+ const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2209
2565
  const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
2210
2566
  ...base,
2211
2567
  milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
@@ -2232,31 +2588,20 @@ export class WorkflowService {
2232
2588
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
2233
2589
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2234
2590
  ...versionBlockers,
2591
+ ...observationPreflightBlockers(snapshot.observation),
2235
2592
  ];
2236
- const adoption = buildAdoptionPreparation({
2237
- projectId: context.identity.projectId,
2238
- repositoryIdentity: context.identity,
2239
- packageVersion: PACKAGE_VERSION,
2240
- protocolVersion: PROTOCOL_VERSION,
2241
- stateSchemaVersion: STATE_SCHEMA_VERSION,
2242
- tasks,
2243
- milestones,
2244
- sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
2245
- blockers,
2246
- now: this.now(),
2247
- }, readCurrentAdoptionPosture(this.store, context.identity.projectId));
2248
2593
  return {
2249
2594
  safe: blockers.length === 0,
2250
2595
  packageVersion: PACKAGE_VERSION,
2251
2596
  protocolVersion: PROTOCOL_VERSION,
2252
2597
  stateSchemaVersion: STATE_SCHEMA_VERSION,
2253
- projectId: context.identity.projectId,
2254
- repositoryRoot: context.identity.repositoryRoot,
2598
+ projectId: snapshot.identity.projectId,
2599
+ repositoryRoot: snapshot.identity.repositoryRoot,
2255
2600
  clean: dirty.length === 0,
2256
2601
  changedFiles: dirty,
2257
2602
  runningSteps,
2258
- activeLeases,
2259
- staleLeases,
2603
+ activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2604
+ staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2260
2605
  blockers,
2261
2606
  workflowVersions: {
2262
2607
  runtime: inspectedVersions.runtime,
@@ -2272,11 +2617,118 @@ export class WorkflowService {
2272
2617
  ? null
2273
2618
  : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
2274
2619
  },
2275
- adoption,
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,
2276
2728
  };
2277
2729
  }
2278
2730
  alpha6StrictReviewRescuePreflight(repository) {
2279
- const context = this.context(repository);
2731
+ const context = this.#mutationContext(repository);
2280
2732
  const dependency = inspectLocalDependency(context.identity.repositoryRoot);
2281
2733
  const tasks = context.store.listTasks(context.identity.projectId);
2282
2734
  const leases = context.locks.list();
@@ -2371,15 +2823,15 @@ export class WorkflowService {
2371
2823
  projectId: context.identity.projectId,
2372
2824
  repositoryRoot: context.identity.repositoryRoot,
2373
2825
  clean: dirty.length === 0,
2374
- activeLeases,
2375
- staleLeases,
2826
+ activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2827
+ staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
2376
2828
  candidate,
2377
2829
  blockers,
2378
2830
  requiredActions,
2379
2831
  };
2380
2832
  }
2381
2833
  scanKnowledgeMap(repository) {
2382
- const { identity } = this.context(repository);
2834
+ const { identity } = this.#mutationContext(repository);
2383
2835
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2384
2836
  try {
2385
2837
  const current = this.store.readKnowledgeMap(identity.projectId);
@@ -2392,11 +2844,11 @@ export class WorkflowService {
2392
2844
  }
2393
2845
  }
2394
2846
  showKnowledgeMap(repository) {
2395
- const { identity } = this.context(repository);
2847
+ const { identity } = this.#mutationContext(repository);
2396
2848
  return this.store.readKnowledgeMap(identity.projectId);
2397
2849
  }
2398
2850
  knowledgeMapStatus(repository) {
2399
- const { identity } = this.context(repository);
2851
+ const { identity } = this.#mutationContext(repository);
2400
2852
  const current = this.store.readKnowledgeMap(identity.projectId);
2401
2853
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2402
2854
  return inspectKnowledgeMap(current, scanned);
@@ -2405,7 +2857,7 @@ export class WorkflowService {
2405
2857
  return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
2406
2858
  }
2407
2859
  approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
2408
- const { identity } = this.context(repository);
2860
+ const { identity } = this.#mutationContext(repository);
2409
2861
  const current = this.store.readKnowledgeMap(identity.projectId);
2410
2862
  assertExpectedRevision(current, expectedRevision);
2411
2863
  const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
@@ -2422,43 +2874,35 @@ export class WorkflowService {
2422
2874
  return this.store.writeKnowledgeMap(approved, expectedRevision);
2423
2875
  }
2424
2876
  reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
2425
- const { identity } = this.context(repository);
2877
+ const { identity } = this.#mutationContext(repository);
2426
2878
  const current = this.store.readKnowledgeMap(identity.projectId);
2427
2879
  assertExpectedRevision(current, expectedRevision);
2428
2880
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2429
2881
  if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2430
2882
  const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
2431
- const activeTask = this.store
2432
- .listTasks(identity.projectId)
2433
- .find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2434
- if (activeTask?.planHash
2435
- && taskCanRebindKnowledge(activeTask)
2436
- && !this.taskHasTerminalCorrectiveDecision(activeTask)) {
2437
- const inspected = inspectKnowledgeMap(current, scanned);
2438
- const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
2439
- if (assessment.eligible
2440
- && this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
2441
- throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2442
- taskId: activeTask.id,
2443
- requiredAction: 'Run the exact task context-refresh action returned by next.',
2444
- });
2445
- }
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
+ });
2446
2890
  }
2447
2891
  }
2448
2892
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
2449
2893
  }
2450
2894
  graphRefreshRequest(repository, graphKind, mode = 'moderate') {
2451
- const { identity } = this.context(repository);
2895
+ const { identity } = this.#mutationContext(repository);
2452
2896
  return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2453
2897
  }
2454
2898
  graphStatus(repository, graphKind, mode = 'moderate') {
2455
- const { identity } = this.context(repository);
2899
+ const { identity } = this.#mutationContext(repository);
2456
2900
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2457
2901
  const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
2458
2902
  return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
2459
2903
  }
2460
2904
  bindGraph(repository, expectedRevision, request, evidence) {
2461
- const { identity } = this.context(repository);
2905
+ const { identity } = this.#mutationContext(repository);
2462
2906
  const currentRequest = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, request.graphKind, request.mode, this.now());
2463
2907
  validateGraphRequestCurrent(request, currentRequest);
2464
2908
  const existing = this.readGraphBindingOrNull(identity.projectId, request.graphKind);
@@ -2466,13 +2910,78 @@ export class WorkflowService {
2466
2910
  return this.store.writeGraphBinding(binding, expectedRevision);
2467
2911
  }
2468
2912
  recordGraphFallback(repository, graphKind, mode, expectedRevision, reason) {
2469
- const { identity } = this.context(repository);
2913
+ const { identity } = this.#mutationContext(repository);
2470
2914
  const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
2471
2915
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
2472
2916
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
2473
2917
  }
2474
- next(repository) {
2918
+ next(repository, taskId = null) {
2475
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
+ }
2476
2985
  const adoptionInfo = {
2477
2986
  status: status.adoption.status,
2478
2987
  safe: status.adoption.safe,
@@ -2490,19 +2999,117 @@ export class WorkflowService {
2490
2999
  status: status.adoption.status,
2491
3000
  action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
2492
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
+ },
2493
3023
  };
2494
3024
  }
2495
- const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
3025
+ const withAdoption = (value) => ({
3026
+ ...value,
3027
+ adoption: adoptionInfo,
3028
+ observation: status.observation,
3029
+ });
2496
3030
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
3031
+ const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
2497
3032
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
2498
- 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));
2499
3048
  if (activeTask) {
2500
- const activePlanRiskAudit = activeTask.planHash
2501
- ? 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
2502
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
+ }
2503
3109
  const taskNext = activeTask.status === 'planning'
2504
3110
  ? null
2505
- : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
3111
+ : planRiskAuditRecovery
3112
+ ?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2506
3113
  // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
2507
3114
  // strict review are executable while context refresh correctly remains forbidden.
2508
3115
  const hardStop = taskNext?.action === 'task corrective-decision'
@@ -2514,12 +3121,27 @@ export class WorkflowService {
2514
3121
  || hardStop
2515
3122
  || terminalCorrectiveDecision === 'split-required'
2516
3123
  || terminalCorrectiveDecision === 'stop-escalate') {
2517
- return withAdoption(taskNext);
3124
+ return withAdoption(withMilestoneCancellation(taskNext));
2518
3125
  }
2519
3126
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
2520
- 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
+ }));
2521
3144
  }
2522
- const knowledgeMap = this.knowledgeMapStatus(repository);
2523
3145
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
2524
3146
  const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
2525
3147
  const delegatedApprovalOptions = action === 'project-memory approve'
@@ -2535,7 +3157,17 @@ export class WorkflowService {
2535
3157
  const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
2536
3158
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2537
3159
  : [];
2538
- return withAdoption({
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({
2539
3171
  scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2540
3172
  status: knowledgeMap.status,
2541
3173
  action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
@@ -2567,14 +3199,14 @@ export class WorkflowService {
2567
3199
  },
2568
3200
  }
2569
3201
  : {}),
2570
- });
3202
+ }));
2571
3203
  }
2572
3204
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
2573
3205
  if (taskCanRebindKnowledge(activeTask) &&
2574
3206
  activeTask.planHash &&
2575
3207
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
2576
3208
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
2577
- return withAdoption({
3209
+ return withAdoption(withMilestoneCancellation({
2578
3210
  scope: 'task',
2579
3211
  id: activeTask.id,
2580
3212
  status: activeTask.status,
@@ -2582,12 +3214,69 @@ export class WorkflowService {
2582
3214
  revision: activeTask.revision,
2583
3215
  knowledgeMapRevision: knowledgeMap.revision,
2584
3216
  knowledgeMapHash,
2585
- });
3217
+ }));
2586
3218
  }
2587
- 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
+ });
2588
3252
  }
2589
3253
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2590
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
+ }
2591
3280
  if (activeMilestone.status === 'awaiting_final_acceptance') {
2592
3281
  const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
2593
3282
  const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
@@ -2638,6 +3327,19 @@ export class WorkflowService {
2638
3327
  status: activeMilestone.status,
2639
3328
  action: nextForMilestone(activeMilestone),
2640
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
+ : {}),
2641
3343
  ...(activeMilestone.status === 'active'
2642
3344
  ? {
2643
3345
  completionPolicy: {
@@ -2794,7 +3496,20 @@ export class WorkflowService {
2794
3496
  throw error;
2795
3497
  }
2796
3498
  }
2797
- 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);
2798
3513
  const handoffPosture = readTaskC1Posture(this.store, task);
2799
3514
  const strictStepReviewStep = planRiskAudit
2800
3515
  ? task.steps.find((step) => {
@@ -2806,6 +3521,11 @@ export class WorkflowService {
2806
3521
  return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
2807
3522
  }) ?? null
2808
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');
2809
3529
  const handoffOverlay = handoffPosture.state === 'pending'
2810
3530
  ? {
2811
3531
  action: 'task claim',
@@ -2814,6 +3534,15 @@ export class WorkflowService {
2814
3534
  state: 'pending',
2815
3535
  targetActor: handoffPosture.targetActor,
2816
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
+ },
2817
3546
  },
2818
3547
  }
2819
3548
  : handoffPosture.state === 'claimed'
@@ -2836,6 +3565,63 @@ export class WorkflowService {
2836
3565
  },
2837
3566
  }
2838
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
+ };
2839
3625
  let guardedCorrectiveOverlay = {};
2840
3626
  if (remediationModeRecovery) {
2841
3627
  guardedCorrectiveOverlay = {
@@ -2854,7 +3640,12 @@ export class WorkflowService {
2854
3640
  },
2855
3641
  };
2856
3642
  }
2857
- 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')) {
2858
3649
  switch (guardedCorrectivePosture.posture) {
2859
3650
  case 'decision-required':
2860
3651
  if (guardedCorrectivePosture.correctiveDecision
@@ -2929,16 +3720,7 @@ export class WorkflowService {
2929
3720
  };
2930
3721
  break;
2931
3722
  case 'replan-required':
2932
- guardedCorrectiveOverlay = {
2933
- action: 'task plan-set',
2934
- stepId: guardedCorrectivePosture.stepId,
2935
- correctiveDecisionGate: {
2936
- attemptCount: guardedCorrectivePosture.attemptCount,
2937
- decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2938
- decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2939
- requiredAction: 'Record an actual new Plan posture before another remediation run.',
2940
- },
2941
- };
3723
+ guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
2942
3724
  break;
2943
3725
  case 'split-required':
2944
3726
  {
@@ -3022,6 +3804,30 @@ export class WorkflowService {
3022
3804
  strictStepReview: {
3023
3805
  reviewRequired: true,
3024
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
+ },
3025
3831
  },
3026
3832
  }
3027
3833
  : {}),
@@ -3051,13 +3857,37 @@ export class WorkflowService {
3051
3857
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
3052
3858
  };
3053
3859
  if (handoffPosture.state === 'pending') {
3054
- return {
3860
+ return this.withTaskNavigationContracts(task, {
3055
3861
  ...result,
3056
3862
  action: 'task claim',
3057
3863
  blockedAction: next.action,
3058
- };
3864
+ });
3059
3865
  }
3060
- 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
+ };
3061
3891
  }
3062
3892
  assertTaskContextChangeAllowed(task) {
3063
3893
  const posture = this.taskTerminalCorrectivePosture(task);
@@ -3136,6 +3966,22 @@ export class WorkflowService {
3136
3966
  policyHash: grant.policyHash,
3137
3967
  }));
3138
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
+ }
3139
3985
  assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
3140
3986
  const grant = this.store.readDelegation(projectId, grantId);
3141
3987
  if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
@@ -3205,6 +4051,130 @@ export class WorkflowService {
3205
4051
  ? milestone
3206
4052
  : readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
3207
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
+ }
3208
4178
  prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
3209
4179
  validateMilestonePlan(plan);
3210
4180
  return prepareMilestoneScopeChangeCandidate({
@@ -3214,6 +4184,12 @@ export class WorkflowService {
3214
4184
  actor,
3215
4185
  });
3216
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
+ }
3217
4193
  proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
3218
4194
  const currentLease = context.locks.inspect(taskId);
3219
4195
  if (!currentLease) {
@@ -3303,9 +4279,12 @@ export class WorkflowService {
3303
4279
  storedKnowledgeMapHash: storedHash,
3304
4280
  });
3305
4281
  }
3306
- inspectAdoptionPreparation(identity, tasks, milestones) {
3307
- const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
3308
- const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
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;
3309
4288
  if (currentPosture) {
3310
4289
  assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
3311
4290
  }
@@ -3318,7 +4297,7 @@ export class WorkflowService {
3318
4297
  tasks,
3319
4298
  milestones,
3320
4299
  sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
3321
- blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
4300
+ blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, leases),
3322
4301
  now: this.now(),
3323
4302
  }, currentPosture);
3324
4303
  }
@@ -3860,13 +4839,13 @@ function nextForTask(task) {
3860
4839
  awaiting_execution_authorization: 'task authorize',
3861
4840
  ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
3862
4841
  in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
3863
- validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
4842
+ validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
3864
4843
  needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
3865
4844
  awaiting_final_acceptance: 'task accept',
3866
4845
  merging: 'task merge',
3867
4846
  ready_to_merge: 'task merge-confirm',
3868
4847
  merged: 'none',
3869
- blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
4848
+ blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
3870
4849
  cancelled: 'none',
3871
4850
  };
3872
4851
  const selectedStep = activeStep ?? failedStep ?? runnableStep;
@@ -3888,6 +4867,95 @@ function nextForTask(task) {
3888
4867
  : {}),
3889
4868
  };
3890
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
+ }
3891
4959
  function knowledgeSummary(map) {
3892
4960
  return {
3893
4961
  revision: map.revision,