codex-workflow-v2 2.0.0-beta.10 → 2.0.0-beta.12
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.
- package/README.md +47 -13
- package/dist/src/alpha6/handoff.d.ts +4 -3
- package/dist/src/alpha6/handoff.js +58 -3
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
- package/dist/src/alpha6/mechanical-feasibility.js +303 -0
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +25 -2
- package/dist/src/alpha6/milestone.js +359 -32
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-integrity.d.ts +14 -0
- package/dist/src/alpha6/plan-integrity.js +127 -0
- package/dist/src/alpha6/plan-integrity.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +3 -2
- package/dist/src/alpha6/remediation.js +168 -55
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +5 -0
- package/dist/src/alpha6/review.js +110 -2
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/cli.js +14 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +82 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +8 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +8 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/reviewer.d.ts +26 -0
- package/dist/src/reviewer.js +54 -1
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +5 -0
- package/dist/src/workflow.js +449 -148
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +42 -33
- package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
- package/docs/decisions.md +5 -4
- package/docs/delegated-approval.md +5 -4
- package/docs/development-flow.md +38 -12
- package/docs/lifecycle/state-machine-stabilization.md +2 -2
- package/docs/pdf/README.md +24 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +1 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +235 -0
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +414 -0
- package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
- package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
- package/docs/problem-briefs/README.md +120 -0
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
- package/docs/release.md +17 -6
- package/docs/split-required-recovery.md +19 -26
- package/docs/validation-report.md +83 -70
- package/package.json +5 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +31 -9
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +23 -6
- package/references/state-machine.md +18 -13
- package/references/validation-and-review.md +11 -4
- package/schemas/authorization-event.schema.json +64 -1
- package/schemas/corrective-decision-event.schema.json +48 -3
- package/schemas/milestone-scope-change-event.schema.json +6 -1
- package/schemas/milestone.schema.json +6 -1
- package/schemas/review-result.schema.json +17 -1
- package/schemas/step-review-event.schema.json +15 -0
- package/schemas/task-handoff-event.schema.json +18 -2
- package/scripts/generate-pdf-docs.py +513 -0
- package/scripts/run-pdf-docs.mjs +62 -0
|
@@ -6,6 +6,7 @@ import { readMilestoneAutonomyEvents } from '../alpha7/autonomy.js';
|
|
|
6
6
|
import { createJournalSnapshotFromContent, captureJournalTargetSnapshot, listMilestoneTransactionJournals, readJournalSnapshotContent, removeMilestoneTransactionJournal, resolveMilestoneTransactionTargetPath, updateMilestoneTransactionJournalPhase, writeMilestoneTransactionJournal, } from './journal.js';
|
|
7
7
|
import { assertHash64, assertNoSymlinkPathComponents, canonicalJsonStringify, computeSidecarEventHash, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
|
|
8
8
|
const SCOPE_CHANGE_SIDECAR = 'scope-change-events.jsonl';
|
|
9
|
+
const INITIAL_PLAN_TARGETS = ['state.json', 'plan.json'];
|
|
9
10
|
const MILESTONE_TARGETS = ['state.json', 'plan.json', SCOPE_CHANGE_SIDECAR];
|
|
10
11
|
const SCOPE_CHANGE_ALLOWED_STATUSES = new Set([
|
|
11
12
|
'planning',
|
|
@@ -68,17 +69,144 @@ export function prepareMilestoneScopeChangeBundle(options) {
|
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
export function normalizeMilestonePlan(plan) {
|
|
71
|
-
|
|
72
|
+
const normalized = {
|
|
72
73
|
outcome: plan.outcome.trim(),
|
|
73
74
|
successSignal: plan.successSignal.trim(),
|
|
74
75
|
acceptance: [...plan.acceptance],
|
|
75
|
-
memberships: plan.memberships.map((membership) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}),
|
|
79
112
|
checks: [...plan.checks],
|
|
80
|
-
taskIds: plan.memberships.map((membership) => membership.taskId),
|
|
113
|
+
taskIds: plan.memberships.map((membership) => membership.taskId.trim()),
|
|
81
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,
|
|
171
|
+
};
|
|
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;
|
|
82
210
|
}
|
|
83
211
|
export function readMilestoneWithIntegrity(store, projectId, milestoneId, readTask, listTasks) {
|
|
84
212
|
recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask);
|
|
@@ -192,6 +320,17 @@ export function assertMilestoneMembershipIntegrity(milestone, readTask, options
|
|
|
192
320
|
membershipTaskIds,
|
|
193
321
|
});
|
|
194
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
|
+
}
|
|
195
334
|
for (const membership of milestone.memberships) {
|
|
196
335
|
const task = readTask(membership.taskId);
|
|
197
336
|
if (task.milestoneId !== milestone.id) {
|
|
@@ -232,6 +371,74 @@ export function assertMilestoneMembershipIntegrity(milestone, readTask, options
|
|
|
232
371
|
}
|
|
233
372
|
}
|
|
234
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
|
+
}
|
|
235
442
|
function hasActiveMilestoneScopeChangeTransaction(store, projectId, milestoneId) {
|
|
236
443
|
const milestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
|
|
237
444
|
return listMilestoneTransactionJournals(milestoneRoot, { expectedMilestoneId: milestoneId })
|
|
@@ -483,6 +690,90 @@ export function applyMilestoneScopeChangeTransaction(options) {
|
|
|
483
690
|
expectedRevisionAfterApply: expectedRevision + 1,
|
|
484
691
|
});
|
|
485
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
|
+
}
|
|
486
777
|
function recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask) {
|
|
487
778
|
const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
|
|
488
779
|
const transactionDir = path.join(alpha6MilestoneRoot, '.transactions');
|
|
@@ -643,12 +934,13 @@ function validateMilestoneScopeChangeEvent(event, expectedMilestoneId) {
|
|
|
643
934
|
function validateMilestoneTransactionTargets(journal) {
|
|
644
935
|
const paths = journal.targetFiles.map((target) => target.path);
|
|
645
936
|
const unique = new Set(paths);
|
|
646
|
-
|
|
937
|
+
const expectedTargets = unique.has(SCOPE_CHANGE_SIDECAR) ? MILESTONE_TARGETS : INITIAL_PLAN_TARGETS;
|
|
938
|
+
if (unique.size !== expectedTargets.length || paths.length !== expectedTargets.length) {
|
|
647
939
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} target set is invalid.`, {
|
|
648
940
|
paths,
|
|
649
941
|
});
|
|
650
942
|
}
|
|
651
|
-
for (const target of
|
|
943
|
+
for (const target of expectedTargets) {
|
|
652
944
|
if (!unique.has(target)) {
|
|
653
945
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} is missing ${target}.`);
|
|
654
946
|
}
|
|
@@ -759,7 +1051,7 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
|
|
|
759
1051
|
const stateSnapshot = journal.targetFiles.find((target) => target.path === 'state.json')?.staged;
|
|
760
1052
|
const planSnapshot = journal.targetFiles.find((target) => target.path === 'plan.json')?.staged;
|
|
761
1053
|
const sidecarSnapshot = journal.targetFiles.find((target) => target.path === SCOPE_CHANGE_SIDECAR)?.staged;
|
|
762
|
-
if (!stateSnapshot || !planSnapshot
|
|
1054
|
+
if (!stateSnapshot || !planSnapshot) {
|
|
763
1055
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} is missing a staged bundle.`);
|
|
764
1056
|
}
|
|
765
1057
|
const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
|
|
@@ -771,26 +1063,27 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
|
|
|
771
1063
|
expectedMilestoneId: milestoneId,
|
|
772
1064
|
transactionId: journal.transactionId,
|
|
773
1065
|
});
|
|
774
|
-
const sidecarBuffer =
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1066
|
+
const sidecarBuffer = sidecarSnapshot
|
|
1067
|
+
? readJournalSnapshotContent(alpha6MilestoneRoot, sidecarSnapshot, {
|
|
1068
|
+
expectedMilestoneId: milestoneId,
|
|
1069
|
+
transactionId: journal.transactionId,
|
|
1070
|
+
})
|
|
1071
|
+
: null;
|
|
1072
|
+
if (!stateBuffer || !planBuffer || (sidecarSnapshot && !sidecarBuffer)) {
|
|
779
1073
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} has an incomplete staged bundle.`);
|
|
780
1074
|
}
|
|
781
1075
|
const state = parseStagedMilestoneStateArtifact(stateBuffer.toString('utf8'), projectId, journal.milestoneId, journal.transactionId);
|
|
782
1076
|
const plan = parseMilestonePlanArtifact(planBuffer.toString('utf8'), journal.milestoneId);
|
|
783
|
-
const events =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|| 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))) {
|
|
794
1087
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} staged bundle is inconsistent.`);
|
|
795
1088
|
}
|
|
796
1089
|
if (state.revision !== journal.expectedMilestoneRevision + 1) {
|
|
@@ -799,7 +1092,7 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
|
|
|
799
1092
|
actualRevision: state.revision,
|
|
800
1093
|
});
|
|
801
1094
|
}
|
|
802
|
-
if (latestEvent.previousMilestoneRevision !== journal.expectedMilestoneRevision) {
|
|
1095
|
+
if (latestEvent && latestEvent.previousMilestoneRevision !== journal.expectedMilestoneRevision) {
|
|
803
1096
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} latest event revision does not match journal precondition.`, {
|
|
804
1097
|
expectedPreviousMilestoneRevision: journal.expectedMilestoneRevision,
|
|
805
1098
|
actualPreviousMilestoneRevision: latestEvent.previousMilestoneRevision,
|
|
@@ -837,13 +1130,21 @@ function parseMilestonePlanArtifact(content, milestoneId) {
|
|
|
837
1130
|
const acceptance = parseStringArray(record.acceptance, `Milestone ${milestoneId} plan.json acceptance`);
|
|
838
1131
|
const checks = parseStringArray(record.checks, `Milestone ${milestoneId} plan.json checks`);
|
|
839
1132
|
const memberships = parseMilestoneMembershipArray(record.memberships, milestoneId);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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(),
|
|
843
1143
|
acceptance,
|
|
844
1144
|
memberships,
|
|
845
1145
|
checks,
|
|
846
|
-
|
|
1146
|
+
taskIds: memberships.map((membership) => membership.taskId),
|
|
1147
|
+
};
|
|
847
1148
|
}
|
|
848
1149
|
function parseMilestoneScopeChangeEvents(content, milestoneId) {
|
|
849
1150
|
if (content.length === 0) {
|
|
@@ -1061,7 +1362,7 @@ function parseMilestoneMembership(value, milestoneId, index, line) {
|
|
|
1061
1362
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership at index ${index} must be an object.`, line !== undefined ? { line, index } : { index });
|
|
1062
1363
|
}
|
|
1063
1364
|
const record = value;
|
|
1064
|
-
const allowedKeys = new Set(['taskId', 'disposition', 'reason']);
|
|
1365
|
+
const allowedKeys = new Set(['taskId', 'disposition', 'reason', 'dependsOnTaskIds']);
|
|
1065
1366
|
const extraKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
|
|
1066
1367
|
if (extraKeys.length > 0) {
|
|
1067
1368
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership contains unexpected fields.`, line !== undefined ? { line, index, extraKeys } : { index, extraKeys });
|
|
@@ -1076,7 +1377,33 @@ function parseMilestoneMembership(value, milestoneId, index, line) {
|
|
|
1076
1377
|
if (disposition !== 'required' && reason.length === 0) {
|
|
1077
1378
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership ${disposition} requires a reason.`, line !== undefined ? { line, index } : { index });
|
|
1078
1379
|
}
|
|
1079
|
-
|
|
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;
|
|
1080
1407
|
}
|
|
1081
1408
|
function parseStringArray(value, label, details = {}) {
|
|
1082
1409
|
if (!Array.isArray(value) || value.length === 0) {
|