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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +94 -12
  2. package/dist/src/alpha6/adoption.js +13 -3
  3. package/dist/src/alpha6/adoption.js.map +1 -1
  4. package/dist/src/alpha6/handoff.d.ts +26 -2
  5. package/dist/src/alpha6/handoff.js +277 -29
  6. package/dist/src/alpha6/handoff.js.map +1 -1
  7. package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
  8. package/dist/src/alpha6/mechanical-feasibility.js +303 -0
  9. package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
  10. package/dist/src/alpha6/milestone.d.ts +37 -2
  11. package/dist/src/alpha6/milestone.js +472 -33
  12. package/dist/src/alpha6/milestone.js.map +1 -1
  13. package/dist/src/alpha6/plan-integrity.d.ts +14 -0
  14. package/dist/src/alpha6/plan-integrity.js +127 -0
  15. package/dist/src/alpha6/plan-integrity.js.map +1 -0
  16. package/dist/src/alpha6/plan-risk.d.ts +1 -0
  17. package/dist/src/alpha6/plan-risk.js +15 -7
  18. package/dist/src/alpha6/plan-risk.js.map +1 -1
  19. package/dist/src/alpha6/remediation.d.ts +2 -1
  20. package/dist/src/alpha6/remediation.js +186 -9
  21. package/dist/src/alpha6/remediation.js.map +1 -1
  22. package/dist/src/alpha6/review.d.ts +3 -0
  23. package/dist/src/alpha6/review.js +24 -0
  24. package/dist/src/alpha6/review.js.map +1 -1
  25. package/dist/src/cli.js +486 -46
  26. package/dist/src/cli.js.map +1 -1
  27. package/dist/src/contracts.d.ts +238 -6
  28. package/dist/src/contracts.js +1 -1
  29. package/dist/src/credential-output.d.ts +10 -0
  30. package/dist/src/credential-output.js +73 -0
  31. package/dist/src/credential-output.js.map +1 -0
  32. package/dist/src/dependency-provenance.d.ts +19 -0
  33. package/dist/src/dependency-provenance.js +185 -0
  34. package/dist/src/dependency-provenance.js.map +1 -0
  35. package/dist/src/errors.d.ts +1 -1
  36. package/dist/src/errors.js.map +1 -1
  37. package/dist/src/git.d.ts +1 -1
  38. package/dist/src/git.js +13 -5
  39. package/dist/src/git.js.map +1 -1
  40. package/dist/src/index.d.ts +2 -1
  41. package/dist/src/index.js +2 -1
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
  44. package/dist/src/lifecycle/canonical-hash.js +24 -0
  45. package/dist/src/lifecycle/canonical-hash.js.map +1 -0
  46. package/dist/src/lifecycle/catalog.d.ts +26 -0
  47. package/dist/src/lifecycle/catalog.js +208 -0
  48. package/dist/src/lifecycle/catalog.js.map +1 -0
  49. package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
  50. package/dist/src/lifecycle/compiler-inspection.js +29 -0
  51. package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
  52. package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
  53. package/dist/src/lifecycle/core-static-readiness.js +130 -0
  54. package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
  55. package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
  56. package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
  57. package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
  58. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
  59. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
  60. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
  61. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
  62. package/dist/src/lifecycle/corrective-replan-credential-core.js +563 -0
  63. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
  64. package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
  65. package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
  66. package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
  67. package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
  68. package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
  69. package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
  70. package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
  71. package/dist/src/lifecycle/corrective-replan.js +553 -0
  72. package/dist/src/lifecycle/corrective-replan.js.map +1 -0
  73. package/dist/src/lifecycle/evaluator.d.ts +2 -0
  74. package/dist/src/lifecycle/evaluator.js +35 -0
  75. package/dist/src/lifecycle/evaluator.js.map +1 -0
  76. package/dist/src/lifecycle/fingerprint.d.ts +31 -0
  77. package/dist/src/lifecycle/fingerprint.js +388 -0
  78. package/dist/src/lifecycle/fingerprint.js.map +1 -0
  79. package/dist/src/lifecycle/immutable.d.ts +1 -0
  80. package/dist/src/lifecycle/immutable.js +10 -0
  81. package/dist/src/lifecycle/immutable.js.map +1 -0
  82. package/dist/src/lifecycle/implementation-table.d.ts +29 -0
  83. package/dist/src/lifecycle/implementation-table.js +87 -0
  84. package/dist/src/lifecycle/implementation-table.js.map +1 -0
  85. package/dist/src/lifecycle/index.d.ts +4 -0
  86. package/dist/src/lifecycle/index.js +5 -0
  87. package/dist/src/lifecycle/index.js.map +1 -0
  88. package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
  89. package/dist/src/lifecycle/schema-artifact.js +214 -0
  90. package/dist/src/lifecycle/schema-artifact.js.map +1 -0
  91. package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
  92. package/dist/src/lifecycle/semantic-registry.js +489 -0
  93. package/dist/src/lifecycle/semantic-registry.js.map +1 -0
  94. package/dist/src/lifecycle/types.d.ts +718 -0
  95. package/dist/src/lifecycle/types.js +4 -0
  96. package/dist/src/lifecycle/types.js.map +1 -0
  97. package/dist/src/observation.d.ts +20 -0
  98. package/dist/src/observation.js +171 -0
  99. package/dist/src/observation.js.map +1 -0
  100. package/dist/src/reviewer.d.ts +43 -1
  101. package/dist/src/reviewer.js +203 -50
  102. package/dist/src/reviewer.js.map +1 -1
  103. package/dist/src/state/corrective-replan-executor.d.ts +64 -0
  104. package/dist/src/state/corrective-replan-executor.js +608 -0
  105. package/dist/src/state/corrective-replan-executor.js.map +1 -0
  106. package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
  107. package/dist/src/state/corrective-replan-public-schema.js +43 -0
  108. package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
  109. package/dist/src/state/corrective-replan-public.d.ts +95 -0
  110. package/dist/src/state/corrective-replan-public.js +665 -0
  111. package/dist/src/state/corrective-replan-public.js.map +1 -0
  112. package/dist/src/state/corrective-replan-transaction.d.ts +160 -0
  113. package/dist/src/state/corrective-replan-transaction.js +730 -0
  114. package/dist/src/state/corrective-replan-transaction.js.map +1 -0
  115. package/dist/src/state/corrective-yield-executor.d.ts +31 -0
  116. package/dist/src/state/corrective-yield-executor.js +236 -0
  117. package/dist/src/state/corrective-yield-executor.js.map +1 -0
  118. package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
  119. package/dist/src/state/corrective-yield-transaction.js +591 -0
  120. package/dist/src/state/corrective-yield-transaction.js.map +1 -0
  121. package/dist/src/state/lock.d.ts +76 -3
  122. package/dist/src/state/lock.js +611 -30
  123. package/dist/src/state/lock.js.map +1 -1
  124. package/dist/src/state/store.d.ts +3 -0
  125. package/dist/src/state/store.js +65 -12
  126. package/dist/src/state/store.js.map +1 -1
  127. package/dist/src/transition-core.d.ts +67 -0
  128. package/dist/src/transition-core.js +318 -0
  129. package/dist/src/transition-core.js.map +1 -0
  130. package/dist/src/version.d.ts +1 -1
  131. package/dist/src/version.js +1 -1
  132. package/dist/src/version.js.map +1 -1
  133. package/dist/src/workflow.d.ts +88 -24
  134. package/dist/src/workflow.js +1647 -270
  135. package/dist/src/workflow.js.map +1 -1
  136. package/docs/autonomy-guardrails.md +61 -1
  137. package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
  138. package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
  139. package/docs/delegated-approval.md +26 -1
  140. package/docs/development-flow.md +63 -8
  141. package/docs/lifecycle/state-machine-stabilization.md +641 -0
  142. package/docs/pdf/README.md +24 -0
  143. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  144. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  145. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  146. package/docs/pdf/requirements.txt +1 -0
  147. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +234 -0
  148. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
  149. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +413 -0
  150. package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
  151. package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
  152. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
  153. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
  154. package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
  155. package/docs/problem-briefs/README.md +120 -0
  156. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
  157. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
  158. package/docs/release.md +30 -6
  159. package/docs/split-required-recovery.md +19 -25
  160. package/docs/stable-release-defect-register.md +465 -3
  161. package/docs/updating-existing-project.md +59 -4
  162. package/docs/validation-report.md +124 -56
  163. package/package.json +9 -1
  164. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
  165. package/plugins/codex-workflow-gateway/references/protocol.md +92 -4
  166. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
  167. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +263 -4
  168. package/references/state-machine.md +13 -6
  169. package/roles/delivery-coordinator.md +39 -0
  170. package/schemas/adoption-posture-event.schema.json +1 -1
  171. package/schemas/authorization-event.schema.json +64 -1
  172. package/schemas/context-envelope.schema.json +1 -1
  173. package/schemas/corrective-decision-event.schema.json +48 -3
  174. package/schemas/corrective-replan-credentials.private.schema.json +98 -0
  175. package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
  176. package/schemas/corrective-replan-public.schema.json +142 -0
  177. package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
  178. package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
  179. package/schemas/milestone-scope-change-event.schema.json +6 -1
  180. package/schemas/milestone.schema.json +6 -1
  181. package/schemas/remediation-event.schema.json +11 -1
  182. package/schemas/task-handoff-event.schema.json +19 -3
  183. package/schemas/task.schema.json +24 -1
  184. package/schemas/transition-definition.schema.json +2609 -0
  185. package/schemas/transition-payloads.schema.json +140 -0
  186. package/scripts/generate-pdf-docs.py +512 -0
  187. package/scripts/run-pdf-docs.mjs +62 -0
@@ -2,9 +2,11 @@ import { existsSync, lstatSync, readFileSync, realpathSync, readdirSync, rmSync
2
2
  import path from 'node:path';
3
3
  import { WorkflowError } from '../errors.js';
4
4
  import { createEntityId } from '../ulid.js';
5
+ import { readMilestoneAutonomyEvents } from '../alpha7/autonomy.js';
5
6
  import { createJournalSnapshotFromContent, captureJournalTargetSnapshot, listMilestoneTransactionJournals, readJournalSnapshotContent, removeMilestoneTransactionJournal, resolveMilestoneTransactionTargetPath, updateMilestoneTransactionJournalPhase, writeMilestoneTransactionJournal, } from './journal.js';
6
7
  import { assertHash64, assertNoSymlinkPathComponents, canonicalJsonStringify, computeSidecarEventHash, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
7
8
  const SCOPE_CHANGE_SIDECAR = 'scope-change-events.jsonl';
9
+ const INITIAL_PLAN_TARGETS = ['state.json', 'plan.json'];
8
10
  const MILESTONE_TARGETS = ['state.json', 'plan.json', SCOPE_CHANGE_SIDECAR];
9
11
  const SCOPE_CHANGE_ALLOWED_STATUSES = new Set([
10
12
  'planning',
@@ -67,22 +69,172 @@ export function prepareMilestoneScopeChangeBundle(options) {
67
69
  };
68
70
  }
69
71
  export function normalizeMilestonePlan(plan) {
70
- return {
72
+ const normalized = {
71
73
  outcome: plan.outcome.trim(),
72
74
  successSignal: plan.successSignal.trim(),
73
75
  acceptance: [...plan.acceptance],
74
- memberships: plan.memberships.map((membership) => ({
75
- ...membership,
76
- reason: membership.reason.trim(),
77
- })),
76
+ memberships: plan.memberships.map((membership, index) => {
77
+ if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
78
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.', {
79
+ membershipIndex: index,
80
+ });
81
+ }
82
+ if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
83
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires a Task ID.', {
84
+ membershipIndex: index,
85
+ });
86
+ }
87
+ if (typeof membership.reason !== 'string') {
88
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.', {
89
+ membershipIndex: index,
90
+ taskId: membership.taskId,
91
+ });
92
+ }
93
+ if (!Array.isArray(membership.dependsOnTaskIds)) {
94
+ throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.', {
95
+ membershipIndex: index,
96
+ taskId: membership.taskId,
97
+ });
98
+ }
99
+ if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
100
+ throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.', {
101
+ membershipIndex: index,
102
+ taskId: membership.taskId,
103
+ });
104
+ }
105
+ return {
106
+ taskId: membership.taskId.trim(),
107
+ disposition: membership.disposition,
108
+ reason: membership.reason.trim(),
109
+ dependsOnTaskIds: [...membership.dependsOnTaskIds].map((taskId) => taskId.trim()).sort(),
110
+ };
111
+ }),
78
112
  checks: [...plan.checks],
79
- taskIds: plan.memberships.map((membership) => membership.taskId),
113
+ taskIds: plan.memberships.map((membership) => membership.taskId.trim()),
114
+ };
115
+ assertCanonicalMilestoneDependencyGraph(normalized.memberships, 'INVALID_ARGUMENT', 'Milestone Plan');
116
+ return normalized;
117
+ }
118
+ export function assessMilestoneTaskDependencies(milestone, taskId, readTask) {
119
+ const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
120
+ if (!membership) {
121
+ return {
122
+ dependencyState: milestone.memberships.every((candidate) => Array.isArray(candidate.dependsOnTaskIds))
123
+ ? 'canonical'
124
+ : 'legacy-unknown',
125
+ dependsOnTaskIds: [],
126
+ unsatisfiedPredecessors: [],
127
+ runnable: false,
128
+ blockReason: `Task ${taskId} is not a Milestone member.`,
129
+ };
130
+ }
131
+ if (milestone.memberships.some((candidate) => !Array.isArray(candidate.dependsOnTaskIds))) {
132
+ return {
133
+ dependencyState: 'legacy-unknown',
134
+ dependsOnTaskIds: [],
135
+ unsatisfiedPredecessors: [],
136
+ runnable: false,
137
+ blockReason: 'Milestone dependency graph is unknown for this legacy nonterminal Plan.',
138
+ };
139
+ }
140
+ assertCanonicalMilestoneDependencyGraph(milestone.memberships, 'STATE_CORRUPT', `Milestone ${milestone.id}`);
141
+ const dependsOnTaskIds = [...(membership.dependsOnTaskIds ?? [])];
142
+ const unsatisfiedPredecessors = dependsOnTaskIds.flatMap((predecessorTaskId) => {
143
+ const predecessorMembership = milestone.memberships.find((candidate) => candidate.taskId === predecessorTaskId);
144
+ let predecessor = null;
145
+ try {
146
+ predecessor = readTask(predecessorTaskId);
147
+ }
148
+ catch (error) {
149
+ if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
150
+ throw error;
151
+ }
152
+ if (predecessorMembership?.disposition === 'required' && predecessor?.status === 'merged')
153
+ return [];
154
+ return [{
155
+ taskId: predecessorTaskId,
156
+ disposition: predecessorMembership?.disposition ?? 'missing',
157
+ status: predecessor?.status ?? 'missing',
158
+ }];
159
+ });
160
+ const runnable = membership.disposition === 'required' && unsatisfiedPredecessors.length === 0;
161
+ return {
162
+ dependencyState: 'canonical',
163
+ dependsOnTaskIds,
164
+ unsatisfiedPredecessors,
165
+ runnable,
166
+ blockReason: membership.disposition !== 'required'
167
+ ? `Milestone membership is ${membership.disposition}, not required.`
168
+ : unsatisfiedPredecessors.length > 0
169
+ ? 'Required predecessor Tasks have not all merged.'
170
+ : null,
80
171
  };
81
172
  }
173
+ export function assertMilestoneDependenciesDeclared(milestone) {
174
+ if (milestone.memberships.some((membership) => !Array.isArray(membership.dependsOnTaskIds))) {
175
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} has no canonical dependency graph.`, {
176
+ diagnosticCode: 'MILESTONE_DEPENDENCIES_UNKNOWN',
177
+ milestoneId: milestone.id,
178
+ membershipRevision: milestone.membershipRevision,
179
+ requiredAction: 'Apply an explicit Milestone scope change containing dependsOnTaskIds for every membership.',
180
+ });
181
+ }
182
+ assertCanonicalMilestoneDependencyGraph(milestone.memberships, 'STATE_CORRUPT', `Milestone ${milestone.id}`);
183
+ }
184
+ export function assertMilestoneTaskRunnable(milestone, taskId, readTask) {
185
+ const assessment = assessMilestoneTaskDependencies(milestone, taskId, readTask);
186
+ if (milestone.status !== 'active' || !assessment.runnable) {
187
+ const diagnosticCode = assessment.dependencyState === 'legacy-unknown'
188
+ ? 'MILESTONE_DEPENDENCIES_UNKNOWN'
189
+ : milestone.status !== 'active'
190
+ ? 'MILESTONE_NOT_ACTIVE'
191
+ : assessment.unsatisfiedPredecessors.length > 0
192
+ ? 'MILESTONE_PREDECESSORS_UNSATISFIED'
193
+ : 'MILESTONE_MEMBERSHIP_NOT_REQUIRED';
194
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not runnable in Milestone ${milestone.id}.`, {
195
+ diagnosticCode,
196
+ milestoneId: milestone.id,
197
+ milestoneStatus: milestone.status,
198
+ taskId,
199
+ ...assessment,
200
+ requiredAction: assessment.dependencyState === 'legacy-unknown'
201
+ ? 'Apply an explicit Milestone scope change containing dependsOnTaskIds for every membership.'
202
+ : milestone.status !== 'active'
203
+ ? 'Authorize the current Milestone Plan before continuing this Task.'
204
+ : assessment.unsatisfiedPredecessors.length > 0
205
+ ? 'Merge every required predecessor Task before continuing this Task.'
206
+ : 'Restore a required Milestone membership before continuing this Task.',
207
+ });
208
+ }
209
+ return assessment;
210
+ }
82
211
  export function readMilestoneWithIntegrity(store, projectId, milestoneId, readTask, listTasks) {
83
212
  recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask);
84
213
  const milestone = store.readMilestone(projectId, milestoneId);
85
214
  assertMilestonePlanAgreement(store, milestone);
215
+ const initialAssembly = assessMilestoneInitialAssembly({
216
+ store,
217
+ projectId,
218
+ milestone,
219
+ ...(listTasks ? { listTasks } : {}),
220
+ });
221
+ if (initialAssembly.eligible)
222
+ return milestone;
223
+ assertMilestoneMembershipIntegrity(milestone, readTask, {}, listTasks);
224
+ return milestone;
225
+ }
226
+ /** Read-only integrity validation for observation APIs. Pending journals must be assessed first. */
227
+ export function inspectMilestoneWithIntegrity(store, projectId, milestoneId, readTask, listTasks) {
228
+ const milestone = store.readMilestone(projectId, milestoneId);
229
+ assertMilestonePlanAgreement(store, milestone);
230
+ const initialAssembly = assessMilestoneInitialAssembly({
231
+ store,
232
+ projectId,
233
+ milestone,
234
+ ...(listTasks ? { listTasks } : {}),
235
+ });
236
+ if (initialAssembly.eligible)
237
+ return milestone;
86
238
  assertMilestoneMembershipIntegrity(milestone, readTask, {}, listTasks);
87
239
  return milestone;
88
240
  }
@@ -90,6 +242,14 @@ export function readMilestoneForScopeChange(store, projectId, milestoneId, readT
90
242
  recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask);
91
243
  const milestone = store.readMilestone(projectId, milestoneId);
92
244
  assertMilestonePlanAgreement(store, milestone);
245
+ const initialAssembly = assessMilestoneInitialAssembly({
246
+ store,
247
+ projectId,
248
+ milestone,
249
+ ...(listTasks ? { listTasks } : {}),
250
+ });
251
+ if (initialAssembly.eligible)
252
+ return milestone;
93
253
  assertMilestoneMembershipIntegrity(milestone, readTask, { allowRepairableRequiredCancelled: true, allowRepairableReverseOrphans: true }, listTasks);
94
254
  return milestone;
95
255
  }
@@ -102,6 +262,37 @@ export function listMilestonesWithIntegrity(store, projectId, readTask, listTask
102
262
  .map((entry) => readMilestoneWithIntegrity(store, projectId, entry.name, readTask, listTasks))
103
263
  .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
104
264
  }
265
+ export function assessMilestoneInitialAssembly(options) {
266
+ const { milestone, listTasks } = options;
267
+ if (!listTasks)
268
+ return { eligible: false, orphanTaskIds: [] };
269
+ if (milestone.status !== 'planning'
270
+ || milestone.planHash !== null
271
+ || milestone.membershipRevision !== 0
272
+ || milestone.memberships.length !== 0
273
+ || milestone.taskIds.length !== 0
274
+ || milestone.successSignal !== ''
275
+ || milestone.checks.length !== 0
276
+ || milestone.authorizations.length !== 0
277
+ || milestone.resultHash !== null
278
+ || milestone.evidenceHash !== null
279
+ || milestone.acceptedHead !== null
280
+ || milestone.cancellationReason !== null) {
281
+ return { eligible: false, orphanTaskIds: [] };
282
+ }
283
+ const reverseOrphans = listTasks().filter((task) => task.milestoneId === milestone.id);
284
+ if (hasActiveMilestoneScopeChangeTransaction(options.store, options.projectId, milestone.id)) {
285
+ return { eligible: false, orphanTaskIds: reverseOrphans.map((task) => task.id) };
286
+ }
287
+ if (readMilestoneScopeChangeEvents(options.store, options.projectId, milestone.id).length > 0) {
288
+ return { eligible: false, orphanTaskIds: reverseOrphans.map((task) => task.id) };
289
+ }
290
+ if (hasMilestoneAutonomyAuthority(options.store, options.projectId, milestone.id)) {
291
+ return { eligible: false, orphanTaskIds: reverseOrphans.map((task) => task.id) };
292
+ }
293
+ const eligible = reverseOrphans.every((task) => isPristineInitialAssemblyTask(task));
294
+ return { eligible, orphanTaskIds: reverseOrphans.map((task) => task.id) };
295
+ }
105
296
  export function assertMilestoneMembershipIntegrity(milestone, readTask, options = {}, listTasks) {
106
297
  if (!Number.isInteger(milestone.membershipRevision) || milestone.membershipRevision < 0) {
107
298
  throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} has invalid membershipRevision.`, {
@@ -129,6 +320,17 @@ export function assertMilestoneMembershipIntegrity(milestone, readTask, options
129
320
  membershipTaskIds,
130
321
  });
131
322
  }
323
+ const dependencyArraysPresent = milestone.memberships.filter((membership) => Array.isArray(membership.dependsOnTaskIds)).length;
324
+ if (dependencyArraysPresent > 0 && dependencyArraysPresent !== milestone.memberships.length) {
325
+ throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} mixes canonical and legacy dependency memberships.`, {
326
+ diagnosticCode: 'MILESTONE_DEPENDENCY_GRAPH_PARTIAL',
327
+ canonicalMemberships: dependencyArraysPresent,
328
+ totalMemberships: milestone.memberships.length,
329
+ });
330
+ }
331
+ if (dependencyArraysPresent === milestone.memberships.length && milestone.memberships.length > 0) {
332
+ assertCanonicalMilestoneDependencyGraph(milestone.memberships, 'STATE_CORRUPT', `Milestone ${milestone.id}`);
333
+ }
132
334
  for (const membership of milestone.memberships) {
133
335
  const task = readTask(membership.taskId);
134
336
  if (task.milestoneId !== milestone.id) {
@@ -169,6 +371,113 @@ export function assertMilestoneMembershipIntegrity(milestone, readTask, options
169
371
  }
170
372
  }
171
373
  }
374
+ function assertCanonicalMilestoneDependencyGraph(memberships, code, label) {
375
+ const byTaskId = new Map(memberships.map((membership) => [membership.taskId, membership]));
376
+ if (byTaskId.size !== memberships.length) {
377
+ throw new WorkflowError(code, `${label} memberships contain duplicate Task IDs.`);
378
+ }
379
+ for (const membership of memberships) {
380
+ if (!Array.isArray(membership.dependsOnTaskIds)) {
381
+ throw new WorkflowError(code, `${label} membership ${membership.taskId} is missing dependsOnTaskIds.`);
382
+ }
383
+ if (new Set(membership.dependsOnTaskIds).size !== membership.dependsOnTaskIds.length) {
384
+ throw new WorkflowError(code, `${label} membership ${membership.taskId} contains duplicate dependencies.`, {
385
+ taskId: membership.taskId,
386
+ dependsOnTaskIds: membership.dependsOnTaskIds,
387
+ });
388
+ }
389
+ if (membership.dependsOnTaskIds.some((taskId) => !taskId)) {
390
+ throw new WorkflowError(code, `${label} membership ${membership.taskId} contains an empty dependency.`);
391
+ }
392
+ const sorted = [...membership.dependsOnTaskIds].sort();
393
+ if (canonicalJsonStringify(sorted) !== canonicalJsonStringify(membership.dependsOnTaskIds)) {
394
+ throw new WorkflowError(code, `${label} membership ${membership.taskId} dependencies are not canonical.`, {
395
+ taskId: membership.taskId,
396
+ expected: sorted,
397
+ actual: membership.dependsOnTaskIds,
398
+ });
399
+ }
400
+ if (membership.dependsOnTaskIds.includes(membership.taskId)) {
401
+ throw new WorkflowError(code, `${label} membership ${membership.taskId} cannot depend on itself.`);
402
+ }
403
+ for (const predecessorTaskId of membership.dependsOnTaskIds) {
404
+ const predecessor = byTaskId.get(predecessorTaskId);
405
+ if (!predecessor) {
406
+ throw new WorkflowError(code, `${label} membership ${membership.taskId} has a dangling dependency.`, {
407
+ taskId: membership.taskId,
408
+ predecessorTaskId,
409
+ });
410
+ }
411
+ if (membership.disposition === 'required' && predecessor.disposition !== 'required') {
412
+ throw new WorkflowError(code, `${label} required membership ${membership.taskId} depends on a non-required provider.`, {
413
+ taskId: membership.taskId,
414
+ predecessorTaskId,
415
+ predecessorDisposition: predecessor.disposition,
416
+ });
417
+ }
418
+ }
419
+ }
420
+ const visiting = new Set();
421
+ const visited = new Set();
422
+ const visit = (taskId, path) => {
423
+ if (visiting.has(taskId)) {
424
+ const cycleStart = path.indexOf(taskId);
425
+ throw new WorkflowError(code, `${label} dependency graph contains a cycle.`, {
426
+ cycle: [...path.slice(Math.max(cycleStart, 0)), taskId],
427
+ });
428
+ }
429
+ if (visited.has(taskId))
430
+ return;
431
+ visiting.add(taskId);
432
+ const membership = byTaskId.get(taskId);
433
+ for (const predecessorTaskId of membership.dependsOnTaskIds) {
434
+ visit(predecessorTaskId, [...path, taskId]);
435
+ }
436
+ visiting.delete(taskId);
437
+ visited.add(taskId);
438
+ };
439
+ for (const membership of memberships)
440
+ visit(membership.taskId, []);
441
+ }
442
+ function hasActiveMilestoneScopeChangeTransaction(store, projectId, milestoneId) {
443
+ const milestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
444
+ return listMilestoneTransactionJournals(milestoneRoot, { expectedMilestoneId: milestoneId })
445
+ .some((journal) => journal.phase === 'prepared' || journal.phase === 'applying');
446
+ }
447
+ function hasMilestoneAutonomyAuthority(store, projectId, milestoneId) {
448
+ return readMilestoneAutonomyEvents(store, projectId, milestoneId).length > 0;
449
+ }
450
+ function isPristineInitialAssemblyTask(task) {
451
+ return task.status === 'planning'
452
+ && task.workspaceOwner === null
453
+ && task.taskBranch === null
454
+ && task.baseCommit === null
455
+ && task.planHash === null
456
+ && task.planObjective === null
457
+ && task.resultHash === null
458
+ && task.evidenceHash === null
459
+ && task.knowledgeImpact === null
460
+ && task.knowledgeImpactReason === null
461
+ && task.knowledgeTargets.length === 0
462
+ && task.knowledgeMapRevision === null
463
+ && task.knowledgeMapHash === null
464
+ && task.graphUse === null
465
+ && task.steps.length === 0
466
+ && task.authorizations.length === 0
467
+ && task.knowledgeRebinds.length === 0
468
+ && task.review.status === 'pending'
469
+ && task.review.reviewer === ''
470
+ && task.review.recordedAt === null
471
+ && task.review.headCommit === null
472
+ && task.review.briefHash === null
473
+ && task.review.planHash === null
474
+ && task.review.evidenceHash === null
475
+ && task.review.summary === ''
476
+ && task.review.findings.length === 0
477
+ && task.blockReason === null
478
+ && task.mergedCommit === null
479
+ && task.systemCommits.length === 0;
480
+ }
172
481
  export function assertMilestoneScopeChangeStatusAllowed(milestone) {
173
482
  if (!SCOPE_CHANGE_ALLOWED_STATUSES.has(milestone.status)) {
174
483
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} cannot change scope while ${milestone.status}.`, {
@@ -381,6 +690,90 @@ export function applyMilestoneScopeChangeTransaction(options) {
381
690
  expectedRevisionAfterApply: expectedRevision + 1,
382
691
  });
383
692
  }
693
+ export function applyInitialMilestonePlanTransaction(options) {
694
+ const { store, milestone, expectedRevision, normalizedPlan, now, readTask, listTasks } = options;
695
+ if (milestone.status !== 'planning'
696
+ || milestone.planHash !== null
697
+ || milestone.membershipRevision !== 0
698
+ || milestone.taskIds.length > 0
699
+ || milestone.memberships.length > 0) {
700
+ throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} is not in the initial Plan posture.`);
701
+ }
702
+ const milestoneRoot = store.milestoneRoot(milestone.projectId, milestone.id);
703
+ const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, milestone.projectId, milestone.id);
704
+ const planText = renderMilestonePlanArtifact(normalizedPlan);
705
+ const stagedPlan = store.stageArtifact(milestoneRoot, 'plan.json', planText);
706
+ const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
707
+ ? { ...authorization, decision: 'superseded' }
708
+ : authorization);
709
+ const candidateState = {
710
+ ...milestone,
711
+ revision: milestone.revision + 1,
712
+ updatedAt: now.toISOString(),
713
+ status: 'awaiting_execution_authorization',
714
+ outcome: normalizedPlan.outcome,
715
+ successSignal: normalizedPlan.successSignal,
716
+ acceptance: normalizedPlan.acceptance,
717
+ taskIds: normalizedPlan.taskIds,
718
+ memberships: normalizedPlan.memberships,
719
+ checks: normalizedPlan.checks,
720
+ membershipRevision: 1,
721
+ authorizations,
722
+ planHash: stagedPlan.hash,
723
+ resultHash: null,
724
+ evidenceHash: null,
725
+ acceptedHead: null,
726
+ cancellationReason: null,
727
+ };
728
+ assertMilestoneMembershipIntegrity(candidateState, readTask, {}, listTasks);
729
+ const stagedState = store.stageArtifact(milestoneRoot, 'state.json', `${JSON.stringify(candidateState, null, 2)}\n`);
730
+ const transactionId = `tx-${createEntityId('MS', now.getTime())}`;
731
+ const actor = 'workflow:milestone-plan-set';
732
+ const confirmationCodeBindingHash = sha256Hex(canonicalJsonStringify({
733
+ kind: 'milestone-initial-plan-v1',
734
+ milestoneId: milestone.id,
735
+ expectedRevision,
736
+ planHash: stagedPlan.hash,
737
+ membershipRevision: candidateState.membershipRevision,
738
+ }));
739
+ const journal = {
740
+ transactionId,
741
+ milestoneId: milestone.id,
742
+ expectedMilestoneRevision: milestone.revision,
743
+ actor,
744
+ confirmationCodeBindingHash,
745
+ phase: 'prepared',
746
+ targetFiles: [
747
+ {
748
+ path: 'state.json',
749
+ preimage: captureJournalTargetSnapshot(alpha6MilestoneRoot, 'state.json'),
750
+ staged: createJournalSnapshotFromContent(alpha6MilestoneRoot, readFileSync(stagedState.stagedFile), { mode: 'inline' }),
751
+ },
752
+ {
753
+ path: 'plan.json',
754
+ preimage: captureJournalTargetSnapshot(alpha6MilestoneRoot, 'plan.json'),
755
+ staged: createJournalSnapshotFromContent(alpha6MilestoneRoot, readFileSync(stagedPlan.stagedFile), { mode: 'inline' }),
756
+ },
757
+ ],
758
+ preparedAt: now.toISOString(),
759
+ updatedAt: now.toISOString(),
760
+ };
761
+ writeMilestoneTransactionJournal(alpha6MilestoneRoot, journal);
762
+ try {
763
+ updateMilestoneTransactionJournalPhase(alpha6MilestoneRoot, transactionId, 'applying', now.toISOString(), {
764
+ expectedMilestoneId: milestone.id,
765
+ });
766
+ store.publishArtifact(stagedPlan);
767
+ store.publishArtifact(stagedState);
768
+ }
769
+ catch (error) {
770
+ rollbackMilestoneTransaction(store, milestone.projectId, milestone.id, journal, readTask);
771
+ throw error;
772
+ }
773
+ return finalizeCompletedMilestoneTransaction(store, milestone.projectId, milestone.id, journal, readTask, {
774
+ expectedRevisionAfterApply: expectedRevision + 1,
775
+ });
776
+ }
384
777
  function recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask) {
385
778
  const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
386
779
  const transactionDir = path.join(alpha6MilestoneRoot, '.transactions');
@@ -406,7 +799,17 @@ function assertMilestonePlanAgreement(store, milestone) {
406
799
  if (milestone.planHash === null)
407
800
  return;
408
801
  const milestoneRoot = store.milestoneRoot(milestone.projectId, milestone.id);
409
- const currentPlanHash = store.hashArtifact(milestoneRoot, 'plan.json');
802
+ let currentPlanHash;
803
+ try {
804
+ currentPlanHash = store.hashArtifact(milestoneRoot, 'plan.json');
805
+ }
806
+ catch (error) {
807
+ throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} declares a planHash but plan.json is unavailable.`, {
808
+ milestoneId: milestone.id,
809
+ expectedPlanHash: milestone.planHash,
810
+ cause: error instanceof Error ? error.message : String(error),
811
+ });
812
+ }
410
813
  if (currentPlanHash !== milestone.planHash) {
411
814
  throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} planHash does not match plan.json.`, {
412
815
  expectedPlanHash: milestone.planHash,
@@ -531,12 +934,13 @@ function validateMilestoneScopeChangeEvent(event, expectedMilestoneId) {
531
934
  function validateMilestoneTransactionTargets(journal) {
532
935
  const paths = journal.targetFiles.map((target) => target.path);
533
936
  const unique = new Set(paths);
534
- if (unique.size !== MILESTONE_TARGETS.length || paths.length !== MILESTONE_TARGETS.length) {
937
+ const expectedTargets = unique.has(SCOPE_CHANGE_SIDECAR) ? MILESTONE_TARGETS : INITIAL_PLAN_TARGETS;
938
+ if (unique.size !== expectedTargets.length || paths.length !== expectedTargets.length) {
535
939
  throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} target set is invalid.`, {
536
940
  paths,
537
941
  });
538
942
  }
539
- for (const target of MILESTONE_TARGETS) {
943
+ for (const target of expectedTargets) {
540
944
  if (!unique.has(target)) {
541
945
  throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} is missing ${target}.`);
542
946
  }
@@ -647,7 +1051,7 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
647
1051
  const stateSnapshot = journal.targetFiles.find((target) => target.path === 'state.json')?.staged;
648
1052
  const planSnapshot = journal.targetFiles.find((target) => target.path === 'plan.json')?.staged;
649
1053
  const sidecarSnapshot = journal.targetFiles.find((target) => target.path === SCOPE_CHANGE_SIDECAR)?.staged;
650
- if (!stateSnapshot || !planSnapshot || !sidecarSnapshot) {
1054
+ if (!stateSnapshot || !planSnapshot) {
651
1055
  throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} is missing a staged bundle.`);
652
1056
  }
653
1057
  const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
@@ -659,26 +1063,27 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
659
1063
  expectedMilestoneId: milestoneId,
660
1064
  transactionId: journal.transactionId,
661
1065
  });
662
- const sidecarBuffer = readJournalSnapshotContent(alpha6MilestoneRoot, sidecarSnapshot, {
663
- expectedMilestoneId: milestoneId,
664
- transactionId: journal.transactionId,
665
- });
666
- if (!stateBuffer || !planBuffer || !sidecarBuffer) {
1066
+ const sidecarBuffer = sidecarSnapshot
1067
+ ? readJournalSnapshotContent(alpha6MilestoneRoot, sidecarSnapshot, {
1068
+ expectedMilestoneId: milestoneId,
1069
+ transactionId: journal.transactionId,
1070
+ })
1071
+ : null;
1072
+ if (!stateBuffer || !planBuffer || (sidecarSnapshot && !sidecarBuffer)) {
667
1073
  throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} has an incomplete staged bundle.`);
668
1074
  }
669
1075
  const state = parseStagedMilestoneStateArtifact(stateBuffer.toString('utf8'), projectId, journal.milestoneId, journal.transactionId);
670
1076
  const plan = parseMilestonePlanArtifact(planBuffer.toString('utf8'), journal.milestoneId);
671
- const events = parseMilestoneScopeChangeEvents(sidecarBuffer.toString('utf8'), journal.milestoneId);
672
- const latestEvent = events.at(-1);
673
- if (!latestEvent) {
674
- throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} has no staged scope-change event.`);
675
- }
676
- if (latestEvent.candidatePlanHash !== state.planHash
677
- || latestEvent.candidateMembershipRevision !== state.membershipRevision
678
- || canonicalJsonStringify(latestEvent.after) !== canonicalJsonStringify(milestoneScopeSnapshotFromState(state))
679
- || canonicalJsonStringify(milestoneScopeSnapshotFromPlan(plan)) !== canonicalJsonStringify(milestoneScopeSnapshotFromState(state))
680
- || latestEvent.actor !== journal.actor
681
- || latestEvent.confirmationCodeBindingHash !== journal.confirmationCodeBindingHash) {
1077
+ const events = sidecarBuffer
1078
+ ? parseMilestoneScopeChangeEvents(sidecarBuffer.toString('utf8'), journal.milestoneId)
1079
+ : [];
1080
+ const latestEvent = events.at(-1) ?? null;
1081
+ if (canonicalJsonStringify(milestoneScopeSnapshotFromPlan(plan)) !== canonicalJsonStringify(milestoneScopeSnapshotFromState(state))
1082
+ || (latestEvent !== null && (latestEvent.candidatePlanHash !== state.planHash
1083
+ || latestEvent.candidateMembershipRevision !== state.membershipRevision
1084
+ || canonicalJsonStringify(latestEvent.after) !== canonicalJsonStringify(milestoneScopeSnapshotFromState(state))
1085
+ || latestEvent.actor !== journal.actor
1086
+ || latestEvent.confirmationCodeBindingHash !== journal.confirmationCodeBindingHash))) {
682
1087
  throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} staged bundle is inconsistent.`);
683
1088
  }
684
1089
  if (state.revision !== journal.expectedMilestoneRevision + 1) {
@@ -687,7 +1092,7 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
687
1092
  actualRevision: state.revision,
688
1093
  });
689
1094
  }
690
- if (latestEvent.previousMilestoneRevision !== journal.expectedMilestoneRevision) {
1095
+ if (latestEvent && latestEvent.previousMilestoneRevision !== journal.expectedMilestoneRevision) {
691
1096
  throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} latest event revision does not match journal precondition.`, {
692
1097
  expectedPreviousMilestoneRevision: journal.expectedMilestoneRevision,
693
1098
  actualPreviousMilestoneRevision: latestEvent.previousMilestoneRevision,
@@ -725,13 +1130,21 @@ function parseMilestonePlanArtifact(content, milestoneId) {
725
1130
  const acceptance = parseStringArray(record.acceptance, `Milestone ${milestoneId} plan.json acceptance`);
726
1131
  const checks = parseStringArray(record.checks, `Milestone ${milestoneId} plan.json checks`);
727
1132
  const memberships = parseMilestoneMembershipArray(record.memberships, milestoneId);
728
- return normalizeMilestonePlan({
729
- outcome: record.outcome,
730
- successSignal: record.successSignal,
1133
+ const canonicalMembershipCount = memberships.filter((membership) => Array.isArray(membership.dependsOnTaskIds)).length;
1134
+ if (canonicalMembershipCount > 0 && canonicalMembershipCount !== memberships.length) {
1135
+ throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} plan.json mixes canonical and legacy dependency memberships.`);
1136
+ }
1137
+ if (canonicalMembershipCount === memberships.length) {
1138
+ assertCanonicalMilestoneDependencyGraph(memberships, 'STATE_CORRUPT', `Milestone ${milestoneId} plan.json`);
1139
+ }
1140
+ return {
1141
+ outcome: record.outcome.trim(),
1142
+ successSignal: record.successSignal.trim(),
731
1143
  acceptance,
732
1144
  memberships,
733
1145
  checks,
734
- });
1146
+ taskIds: memberships.map((membership) => membership.taskId),
1147
+ };
735
1148
  }
736
1149
  function parseMilestoneScopeChangeEvents(content, milestoneId) {
737
1150
  if (content.length === 0) {
@@ -949,7 +1362,7 @@ function parseMilestoneMembership(value, milestoneId, index, line) {
949
1362
  throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership at index ${index} must be an object.`, line !== undefined ? { line, index } : { index });
950
1363
  }
951
1364
  const record = value;
952
- const allowedKeys = new Set(['taskId', 'disposition', 'reason']);
1365
+ const allowedKeys = new Set(['taskId', 'disposition', 'reason', 'dependsOnTaskIds']);
953
1366
  const extraKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
954
1367
  if (extraKeys.length > 0) {
955
1368
  throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership contains unexpected fields.`, line !== undefined ? { line, index, extraKeys } : { index, extraKeys });
@@ -964,7 +1377,33 @@ function parseMilestoneMembership(value, milestoneId, index, line) {
964
1377
  if (disposition !== 'required' && reason.length === 0) {
965
1378
  throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership ${disposition} requires a reason.`, line !== undefined ? { line, index } : { index });
966
1379
  }
967
- return { taskId, disposition, reason };
1380
+ const dependsOnTaskIds = record.dependsOnTaskIds === undefined
1381
+ ? undefined
1382
+ : parseTaskDependencyArray(record.dependsOnTaskIds, `Milestone ${milestoneId} membership.dependsOnTaskIds`, line !== undefined ? { line, index } : { index });
1383
+ return {
1384
+ taskId,
1385
+ disposition,
1386
+ reason,
1387
+ ...(dependsOnTaskIds === undefined ? {} : { dependsOnTaskIds }),
1388
+ };
1389
+ }
1390
+ function parseTaskDependencyArray(value, label, details = {}) {
1391
+ if (!Array.isArray(value)) {
1392
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be an array.`, details);
1393
+ }
1394
+ const taskIds = value.map((entry, index) => {
1395
+ if (typeof entry !== 'string' || !entry.trim()) {
1396
+ throw new WorkflowError('STATE_CORRUPT', `${label}[${index}] must be a non-empty Task ID.`, {
1397
+ ...details,
1398
+ index,
1399
+ });
1400
+ }
1401
+ return entry.trim();
1402
+ });
1403
+ if (new Set(taskIds).size !== taskIds.length) {
1404
+ throw new WorkflowError('STATE_CORRUPT', `${label} contains duplicate Task IDs.`, { ...details, taskIds });
1405
+ }
1406
+ return taskIds;
968
1407
  }
969
1408
  function parseStringArray(value, label, details = {}) {
970
1409
  if (!Array.isArray(value) || value.length === 0) {