codex-workflow-v2 2.0.0-alpha.3 → 2.0.0-alpha.6

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 (85) hide show
  1. package/README.md +104 -7
  2. package/dist/src/alpha6/adoption.d.ts +55 -0
  3. package/dist/src/alpha6/adoption.js +920 -0
  4. package/dist/src/alpha6/adoption.js.map +1 -0
  5. package/dist/src/alpha6/handoff.d.ts +39 -0
  6. package/dist/src/alpha6/handoff.js +975 -0
  7. package/dist/src/alpha6/handoff.js.map +1 -0
  8. package/dist/src/alpha6/journal.d.ts +30 -0
  9. package/dist/src/alpha6/journal.js +369 -0
  10. package/dist/src/alpha6/journal.js.map +1 -0
  11. package/dist/src/alpha6/milestone.d.ts +49 -0
  12. package/dist/src/alpha6/milestone.js +1049 -0
  13. package/dist/src/alpha6/milestone.js.map +1 -0
  14. package/dist/src/alpha6/plan-risk.d.ts +32 -0
  15. package/dist/src/alpha6/plan-risk.js +847 -0
  16. package/dist/src/alpha6/plan-risk.js.map +1 -0
  17. package/dist/src/alpha6/remediation.d.ts +20 -0
  18. package/dist/src/alpha6/remediation.js +748 -0
  19. package/dist/src/alpha6/remediation.js.map +1 -0
  20. package/dist/src/alpha6/review.d.ts +46 -0
  21. package/dist/src/alpha6/review.js +785 -0
  22. package/dist/src/alpha6/review.js.map +1 -0
  23. package/dist/src/alpha6/store-sidecars.d.ts +35 -0
  24. package/dist/src/alpha6/store-sidecars.js +281 -0
  25. package/dist/src/alpha6/store-sidecars.js.map +1 -0
  26. package/dist/src/cli.js +110 -23
  27. package/dist/src/cli.js.map +1 -1
  28. package/dist/src/contracts.d.ts +319 -1
  29. package/dist/src/delegation.d.ts +8 -0
  30. package/dist/src/delegation.js +155 -0
  31. package/dist/src/delegation.js.map +1 -0
  32. package/dist/src/git.js +2 -1
  33. package/dist/src/git.js.map +1 -1
  34. package/dist/src/index.d.ts +3 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/reviewer.d.ts +6 -1
  38. package/dist/src/reviewer.js +145 -32
  39. package/dist/src/reviewer.js.map +1 -1
  40. package/dist/src/state/lock.d.ts +1 -0
  41. package/dist/src/state/lock.js +7 -1
  42. package/dist/src/state/lock.js.map +1 -1
  43. package/dist/src/state/store.d.ts +43 -1
  44. package/dist/src/state/store.js +140 -1
  45. package/dist/src/state/store.js.map +1 -1
  46. package/dist/src/ulid.d.ts +1 -1
  47. package/dist/src/ulid.js.map +1 -1
  48. package/dist/src/version.d.ts +1 -1
  49. package/dist/src/version.js +1 -1
  50. package/dist/src/workflow.d.ts +91 -13
  51. package/dist/src/workflow.js +1459 -76
  52. package/dist/src/workflow.js.map +1 -1
  53. package/docs/autonomy-guardrails.md +143 -0
  54. package/docs/decisions.md +30 -0
  55. package/docs/delegated-approval.md +189 -0
  56. package/docs/development-flow.md +53 -2
  57. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +426 -410
  58. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +778 -612
  59. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +526 -452
  60. package/docs/project-memory.md +7 -0
  61. package/docs/release.md +7 -0
  62. package/docs/updating-existing-project.md +141 -4
  63. package/docs/validation-report.md +47 -24
  64. package/package.json +1 -1
  65. package/plugins/codex-workflow-gateway/references/protocol.md +162 -5
  66. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +38 -3
  67. package/references/git-policy.md +5 -2
  68. package/references/state-machine.md +48 -0
  69. package/references/validation-and-review.md +28 -1
  70. package/roles/delivery-coordinator.md +11 -0
  71. package/roles/independent-reviewer.md +3 -0
  72. package/roles/technical-planner.md +7 -0
  73. package/roles/worker.md +4 -0
  74. package/schemas/adoption-posture-event.schema.json +129 -0
  75. package/schemas/authorization-event.schema.json +55 -0
  76. package/schemas/corrective-decision-event.schema.json +55 -0
  77. package/schemas/corrective-plan-audit.schema.json +20 -0
  78. package/schemas/delegation-grant.schema.json +69 -0
  79. package/schemas/milestone-scope-change-event.schema.json +68 -0
  80. package/schemas/milestone-transaction-journal.schema.json +95 -0
  81. package/schemas/plan-risk-audit-event.schema.json +90 -0
  82. package/schemas/remediation-event.schema.json +53 -0
  83. package/schemas/reviewer-attestation-event.schema.json +49 -0
  84. package/schemas/step-review-event.schema.json +74 -0
  85. package/schemas/task-handoff-event.schema.json +116 -0
@@ -0,0 +1,785 @@
1
+ import { WorkflowError } from '../errors.js';
2
+ import { canonicalJsonStringify, assertHash64, sha256Hex } from './store-sidecars.js';
3
+ import { createUlid } from '../ulid.js';
4
+ const STEP_REVIEW_EVENTS_SIDECAR = 'step-review-events.jsonl';
5
+ const REVIEWER_ATTESTATIONS_SIDECAR = 'reviewer-attestations.jsonl';
6
+ const GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
7
+ const STEP_REVIEW_EVENT_TYPES = new Set([
8
+ 'pending',
9
+ 'review-recorded',
10
+ 'attestation-verified',
11
+ ]);
12
+ const REVIEW_DECISIONS = new Set(['pending', 'passed', 'failed', 'unverified']);
13
+ const REVIEW_FINDING_SEVERITIES = new Set(['critical', 'major', 'minor']);
14
+ export function isStepReviewRequired(task, stepId, reviewRequiredStepIds) {
15
+ return reviewRequiredStepIds.includes(stepId);
16
+ }
17
+ export function ensurePendingStrictStepReview(store, task, stepId, now) {
18
+ const current = buildCurrentStrictStepReviewCycle(store, task, stepId);
19
+ if (current.pendingEvent && current.latestMatchingEvent?.eventType === 'pending')
20
+ return current.pendingEvent;
21
+ return appendStepReviewEvent(store, task, {
22
+ eventId: `SRE-${createUlid(now.getTime())}`,
23
+ recordedAt: now.toISOString(),
24
+ taskId: task.id,
25
+ taskRevision: task.revision,
26
+ stepId: current.binding.stepId,
27
+ stepDefinitionHash: current.binding.stepDefinitionHash,
28
+ completionCommit: current.binding.completionCommit,
29
+ checksHash: current.binding.checksHash,
30
+ planHash: current.binding.planHash,
31
+ evidenceHash: current.binding.evidenceHash,
32
+ eventType: 'pending',
33
+ decision: 'pending',
34
+ reviewerAttestationHash: null,
35
+ });
36
+ }
37
+ export function requireCurrentStrictStepReview(store, task, stepId) {
38
+ const current = buildCurrentStrictStepReviewCycle(store, task, stepId);
39
+ if (!current.pendingEvent) {
40
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review pending event is missing for ${stepId}.`, {
41
+ taskId: task.id,
42
+ stepId,
43
+ completionCommit: current.binding.completionCommit,
44
+ });
45
+ }
46
+ if (current.latestMatchingEvent?.eventType !== 'pending') {
47
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review for ${stepId} is no longer pending.`, {
48
+ taskId: task.id,
49
+ stepId,
50
+ eventType: current.latestMatchingEvent?.eventType ?? null,
51
+ completionCommit: current.binding.completionCommit,
52
+ });
53
+ }
54
+ return current;
55
+ }
56
+ export function buildCurrentStrictStepReviewCycle(store, task, stepId) {
57
+ const current = buildCurrentStrictStepBinding(task, stepId);
58
+ const stepReviewEvents = readStepReviewEvents(store, task);
59
+ const reviewerAttestations = readReviewerAttestations(store, task);
60
+ validateGlobalStrictStepReviewHistory(stepReviewEvents, reviewerAttestations);
61
+ return resolveCurrentStrictStepReviewCycle(current, stepReviewEvents, reviewerAttestations);
62
+ }
63
+ export function recordStrictStepReview(store, task, stepId, input, now) {
64
+ let current = buildCurrentStrictStepReviewCycle(store, task, stepId);
65
+ if (!current.pendingEvent) {
66
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review pending event is missing for ${stepId}.`, {
67
+ taskId: task.id,
68
+ stepId,
69
+ completionCommit: current.binding.completionCommit,
70
+ });
71
+ }
72
+ validateStepReviewRecordInput(current, input);
73
+ const reviewRecordedEvent = current.reviewRecordedEvent
74
+ ? reuseReviewRecordedEvent(current, input)
75
+ : appendStepReviewEvent(store, task, {
76
+ eventId: `SRE-${createUlid(now.getTime())}`,
77
+ recordedAt: now.toISOString(),
78
+ taskId: task.id,
79
+ taskRevision: task.revision,
80
+ stepId: current.binding.stepId,
81
+ stepDefinitionHash: current.binding.stepDefinitionHash,
82
+ completionCommit: current.binding.completionCommit,
83
+ checksHash: current.binding.checksHash,
84
+ planHash: current.binding.planHash,
85
+ evidenceHash: current.binding.evidenceHash,
86
+ eventType: 'review-recorded',
87
+ decision: input.review.status,
88
+ reviewerAttestationHash: null,
89
+ });
90
+ current = buildCurrentStrictStepReviewCycle(store, task, stepId);
91
+ const reviewerAttestationEvent = current.reviewerAttestationEvent
92
+ ? reuseReviewerAttestationEvent(current, reviewRecordedEvent, input)
93
+ : appendReviewerAttestationEvent(store, task, {
94
+ eventId: `RAT-${createUlid(now.getTime())}`,
95
+ recordedAt: now.toISOString(),
96
+ taskId: task.id,
97
+ stepId: current.binding.stepId,
98
+ reviewer: input.review.reviewer.trim(),
99
+ isolationResult: input.isolationResult,
100
+ outputHash: input.reviewerOutputHash,
101
+ reviewedCommit: input.reviewedCommit,
102
+ reviewEventHash: reviewRecordedEvent.eventHash,
103
+ planHash: current.binding.planHash,
104
+ evidenceHash: current.binding.evidenceHash,
105
+ });
106
+ current = buildCurrentStrictStepReviewCycle(store, task, stepId);
107
+ const attestationVerifiedEvent = input.isolationResult === 'verified'
108
+ ? current.attestationVerifiedEvent
109
+ ? reuseAttestationVerifiedEvent(current, reviewRecordedEvent, reviewerAttestationEvent)
110
+ : appendStepReviewEvent(store, task, {
111
+ eventId: `SRE-${createUlid(now.getTime())}`,
112
+ recordedAt: now.toISOString(),
113
+ taskId: task.id,
114
+ taskRevision: task.revision,
115
+ stepId: current.binding.stepId,
116
+ stepDefinitionHash: current.binding.stepDefinitionHash,
117
+ completionCommit: current.binding.completionCommit,
118
+ checksHash: current.binding.checksHash,
119
+ planHash: current.binding.planHash,
120
+ evidenceHash: current.binding.evidenceHash,
121
+ eventType: 'attestation-verified',
122
+ decision: reviewRecordedEvent.decision,
123
+ reviewerAttestationHash: reviewerAttestationEvent.eventHash,
124
+ })
125
+ : null;
126
+ const resolution = deriveStrictStepReviewResolution(reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent);
127
+ return {
128
+ reviewRecordedEvent,
129
+ reviewerAttestationEvent,
130
+ attestationVerifiedEvent,
131
+ resolution,
132
+ canFinalizeStep: resolution === 'passed_verified',
133
+ };
134
+ }
135
+ export function readStepReviewEvents(store, task) {
136
+ return store.readSidecarEvents(store.taskRoot(task.projectId, task.id), STEP_REVIEW_EVENTS_SIDECAR, {
137
+ validateEvent: (event, context) => validateStoredStepReviewEvent(event, task.id, context.file, context.line),
138
+ });
139
+ }
140
+ export function readReviewerAttestations(store, task) {
141
+ return store.readSidecarEvents(store.taskRoot(task.projectId, task.id), REVIEWER_ATTESTATIONS_SIDECAR, {
142
+ validateEvent: (event, context) => validateStoredReviewerAttestationEvent(event, task.id, context.file, context.line),
143
+ });
144
+ }
145
+ export function deriveStrictStepReviewResolution(reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent) {
146
+ if (!reviewRecordedEvent || !reviewerAttestationEvent)
147
+ return 'pending';
148
+ if (reviewRecordedEvent.decision === 'failed')
149
+ return 'failed';
150
+ if (reviewRecordedEvent.decision === 'unverified')
151
+ return 'unverified';
152
+ if (reviewRecordedEvent.decision !== 'passed')
153
+ return 'pending';
154
+ if (reviewerAttestationEvent.isolationResult !== 'verified')
155
+ return 'unverified';
156
+ return attestationVerifiedEvent ? 'passed_verified' : 'pending';
157
+ }
158
+ function buildCurrentStrictStepBinding(task, stepId) {
159
+ const step = requireTaskStep(task, stepId);
160
+ if (!step.evidence) {
161
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review requires canonical evidence for ${stepId}.`, {
162
+ taskId: task.id,
163
+ stepId,
164
+ });
165
+ }
166
+ if (!task.planHash) {
167
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no current plan hash for strict Step Review.`, {
168
+ taskId: task.id,
169
+ stepId,
170
+ });
171
+ }
172
+ return {
173
+ step,
174
+ evidence: step.evidence,
175
+ binding: {
176
+ stepId: step.id,
177
+ taskRevision: task.revision,
178
+ stepDefinitionHash: sha256Hex(canonicalJsonStringify(stripStepRuntime(step))),
179
+ completionCommit: step.evidence.commitSha,
180
+ checksHash: sha256Hex(canonicalJsonStringify(step.evidence.checks)),
181
+ planHash: task.planHash,
182
+ evidenceHash: sha256Hex(canonicalJsonStringify(step.evidence)),
183
+ },
184
+ };
185
+ }
186
+ function resolveCurrentStrictStepReviewCycle(current, stepReviewEvents, reviewerAttestations) {
187
+ const matchingEvents = stepReviewEvents.filter((event) => eventMatchesBinding(event, current.binding));
188
+ const latestMatchingEvent = matchingEvents.at(-1) ?? null;
189
+ const latestPendingIndex = findLastIndex(matchingEvents, (event) => event.eventType === 'pending');
190
+ if (latestPendingIndex < 0) {
191
+ if (matchingEvents.length > 0) {
192
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review history is missing the pending event that should anchor the current cycle.', {
193
+ stepId: current.binding.stepId,
194
+ completionCommit: current.binding.completionCommit,
195
+ latestEventType: latestMatchingEvent?.eventType ?? null,
196
+ });
197
+ }
198
+ return {
199
+ ...current,
200
+ latestMatchingEvent: null,
201
+ pendingEvent: null,
202
+ reviewRecordedEvent: null,
203
+ reviewerAttestationEvent: null,
204
+ attestationVerifiedEvent: null,
205
+ resolution: 'pending',
206
+ };
207
+ }
208
+ const pendingEvent = matchingEvents[latestPendingIndex];
209
+ const cycleEvents = matchingEvents.slice(latestPendingIndex + 1);
210
+ const reviewRecordedEvents = cycleEvents.filter((event) => event.eventType === 'review-recorded');
211
+ const attestationVerifiedEvents = cycleEvents.filter((event) => event.eventType === 'attestation-verified');
212
+ if (reviewRecordedEvents.length > 1) {
213
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review cycle has multiple review-recorded events for one pending event.', {
214
+ stepId: current.binding.stepId,
215
+ completionCommit: current.binding.completionCommit,
216
+ eventIds: reviewRecordedEvents.map((event) => event.eventId),
217
+ });
218
+ }
219
+ if (attestationVerifiedEvents.length > 1) {
220
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review cycle has multiple attestation-verified events for one pending event.', {
221
+ stepId: current.binding.stepId,
222
+ completionCommit: current.binding.completionCommit,
223
+ eventIds: attestationVerifiedEvents.map((event) => event.eventId),
224
+ });
225
+ }
226
+ const reviewRecordedEvent = reviewRecordedEvents[0] ?? null;
227
+ const attestationVerifiedEvent = attestationVerifiedEvents[0] ?? null;
228
+ let reviewerAttestationEvent = null;
229
+ if (reviewRecordedEvent) {
230
+ const linkedAttestations = reviewerAttestations.filter((event) => event.reviewEventHash === reviewRecordedEvent.eventHash);
231
+ if (linkedAttestations.length > 1) {
232
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review cycle has multiple reviewer attestations for one review event.', {
233
+ stepId: current.binding.stepId,
234
+ reviewEventId: reviewRecordedEvent.eventId,
235
+ eventIds: linkedAttestations.map((event) => event.eventId),
236
+ });
237
+ }
238
+ reviewerAttestationEvent = linkedAttestations[0] ?? null;
239
+ if (reviewerAttestationEvent)
240
+ validateAttestationBinding(current.binding, reviewRecordedEvent, reviewerAttestationEvent);
241
+ }
242
+ else if (attestationVerifiedEvent) {
243
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification cannot exist without a review-recorded event.', {
244
+ stepId: current.binding.stepId,
245
+ eventId: attestationVerifiedEvent.eventId,
246
+ });
247
+ }
248
+ if (attestationVerifiedEvent) {
249
+ if (!reviewerAttestationEvent || !reviewRecordedEvent) {
250
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification is missing its prerequisite chain.', {
251
+ stepId: current.binding.stepId,
252
+ eventId: attestationVerifiedEvent.eventId,
253
+ });
254
+ }
255
+ validateAttestationVerifiedChain(current.binding, reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent);
256
+ }
257
+ return {
258
+ ...current,
259
+ latestMatchingEvent,
260
+ pendingEvent,
261
+ reviewRecordedEvent,
262
+ reviewerAttestationEvent,
263
+ attestationVerifiedEvent,
264
+ resolution: deriveStrictStepReviewResolution(reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent),
265
+ };
266
+ }
267
+ function validateGlobalStrictStepReviewHistory(stepReviewEvents, reviewerAttestations) {
268
+ const reviewRecordedEventsByHash = indexStepReviewEventsByHash(stepReviewEvents, 'review-recorded');
269
+ const reviewerAttestationsByReviewHash = new Map();
270
+ for (const attestation of reviewerAttestations) {
271
+ const linkedReviewEvents = reviewRecordedEventsByHash.get(attestation.reviewEventHash) ?? [];
272
+ if (linkedReviewEvents.length !== 1) {
273
+ throw new WorkflowError('STATE_CORRUPT', linkedReviewEvents.length === 0
274
+ ? 'Reviewer attestation references a missing review-recorded Step Review event.'
275
+ : 'Reviewer attestation references multiple review-recorded Step Review events.', {
276
+ eventId: attestation.eventId,
277
+ reviewEventHash: attestation.reviewEventHash,
278
+ matchCount: linkedReviewEvents.length,
279
+ });
280
+ }
281
+ const linkedReviewEvent = linkedReviewEvents[0];
282
+ validateHistoricalAttestationBinding(linkedReviewEvent, attestation);
283
+ const linkedAttestations = reviewerAttestationsByReviewHash.get(attestation.reviewEventHash) ?? [];
284
+ linkedAttestations.push(attestation);
285
+ reviewerAttestationsByReviewHash.set(attestation.reviewEventHash, linkedAttestations);
286
+ }
287
+ for (const [reviewEventHash, linkedAttestations] of reviewerAttestationsByReviewHash) {
288
+ if (linkedAttestations.length > 1) {
289
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple reviewer attestations target the same review-recorded Step Review event.', {
290
+ reviewEventHash,
291
+ eventIds: linkedAttestations.map((event) => event.eventId),
292
+ });
293
+ }
294
+ }
295
+ const reviewerAttestationsByHash = indexReviewerAttestationsByHash(reviewerAttestations);
296
+ const attestationVerifiedEventsByAttestationHash = new Map();
297
+ for (const verifiedEvent of stepReviewEvents) {
298
+ if (verifiedEvent.eventType !== 'attestation-verified')
299
+ continue;
300
+ const attestationHash = verifiedEvent.reviewerAttestationHash;
301
+ if (typeof attestationHash !== 'string') {
302
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event is missing its attestation hash.', {
303
+ eventId: verifiedEvent.eventId,
304
+ });
305
+ }
306
+ const linkedAttestations = reviewerAttestationsByHash.get(attestationHash) ?? [];
307
+ if (linkedAttestations.length !== 1) {
308
+ throw new WorkflowError('STATE_CORRUPT', linkedAttestations.length === 0
309
+ ? 'Attestation-verified Step Review event references a missing reviewer attestation.'
310
+ : 'Attestation-verified Step Review event references multiple reviewer attestations.', {
311
+ eventId: verifiedEvent.eventId,
312
+ reviewerAttestationHash: attestationHash,
313
+ matchCount: linkedAttestations.length,
314
+ });
315
+ }
316
+ const reviewerAttestationEvent = linkedAttestations[0];
317
+ const linkedReviewEvents = reviewRecordedEventsByHash.get(reviewerAttestationEvent.reviewEventHash) ?? [];
318
+ if (linkedReviewEvents.length !== 1) {
319
+ throw new WorkflowError('STATE_CORRUPT', linkedReviewEvents.length === 0
320
+ ? 'Attestation-verified Step Review event references an attestation whose review-recorded event is missing.'
321
+ : 'Attestation-verified Step Review event references an attestation with multiple review-recorded targets.', {
322
+ eventId: verifiedEvent.eventId,
323
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
324
+ reviewEventHash: reviewerAttestationEvent.reviewEventHash,
325
+ matchCount: linkedReviewEvents.length,
326
+ });
327
+ }
328
+ const linkedReviewEvent = linkedReviewEvents[0];
329
+ validateHistoricalAttestationBinding(linkedReviewEvent, reviewerAttestationEvent);
330
+ validateHistoricalAttestationVerifiedChain(linkedReviewEvent, reviewerAttestationEvent, verifiedEvent);
331
+ const linkedVerifiedEvents = attestationVerifiedEventsByAttestationHash.get(attestationHash) ?? [];
332
+ linkedVerifiedEvents.push(verifiedEvent);
333
+ attestationVerifiedEventsByAttestationHash.set(attestationHash, linkedVerifiedEvents);
334
+ }
335
+ for (const [reviewerAttestationHash, linkedVerifiedEvents] of attestationVerifiedEventsByAttestationHash) {
336
+ if (linkedVerifiedEvents.length > 1) {
337
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple attestation-verified Step Review events target the same reviewer attestation.', {
338
+ reviewerAttestationHash,
339
+ eventIds: linkedVerifiedEvents.map((event) => event.eventId),
340
+ });
341
+ }
342
+ }
343
+ }
344
+ function stripStepRuntime(step) {
345
+ const { status: _status, evidence: _evidence, ...definition } = step;
346
+ return definition;
347
+ }
348
+ function requireTaskStep(task, stepId) {
349
+ const step = task.steps.find((candidate) => candidate.id === stepId);
350
+ if (!step)
351
+ throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
352
+ return step;
353
+ }
354
+ function eventMatchesBinding(event, binding) {
355
+ return event.stepId === binding.stepId
356
+ && event.stepDefinitionHash === binding.stepDefinitionHash
357
+ && event.completionCommit === binding.completionCommit
358
+ && event.checksHash === binding.checksHash
359
+ && event.planHash === binding.planHash
360
+ && event.evidenceHash === binding.evidenceHash;
361
+ }
362
+ function stepReviewEventsMatchStructuralBinding(left, right) {
363
+ return left.taskId === right.taskId
364
+ && left.stepId === right.stepId
365
+ && left.stepDefinitionHash === right.stepDefinitionHash
366
+ && left.completionCommit === right.completionCommit
367
+ && left.checksHash === right.checksHash
368
+ && left.planHash === right.planHash
369
+ && left.evidenceHash === right.evidenceHash;
370
+ }
371
+ function validateStepReviewRecordInput(current, input) {
372
+ const reviewer = typeof input.review.reviewer === 'string' ? input.review.reviewer.trim() : '';
373
+ const summary = typeof input.review.summary === 'string' ? input.review.summary.trim() : '';
374
+ if (!reviewer || !summary) {
375
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review requires reviewer identity and summary.', {
376
+ stepId: current.binding.stepId,
377
+ });
378
+ }
379
+ if (!Array.isArray(input.review.findings)) {
380
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings must be an array.', {
381
+ stepId: current.binding.stepId,
382
+ });
383
+ }
384
+ if (input.review.status === 'pending' || !REVIEW_DECISIONS.has(input.review.status)) {
385
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review status is invalid.', {
386
+ stepId: current.binding.stepId,
387
+ status: input.review.status,
388
+ });
389
+ }
390
+ for (const finding of input.review.findings) {
391
+ if (!finding || typeof finding !== 'object') {
392
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings must be objects.', {
393
+ stepId: current.binding.stepId,
394
+ });
395
+ }
396
+ if (typeof finding.id !== 'string' || !finding.id.trim()) {
397
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings require an id.', {
398
+ stepId: current.binding.stepId,
399
+ });
400
+ }
401
+ if (!REVIEW_FINDING_SEVERITIES.has(finding.severity)) {
402
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings severity is invalid.', {
403
+ stepId: current.binding.stepId,
404
+ findingId: finding.id,
405
+ severity: finding.severity,
406
+ });
407
+ }
408
+ if (finding.requirement !== null && (typeof finding.requirement !== 'string' || !finding.requirement.trim())) {
409
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review finding requirement must be null or a non-empty string.', {
410
+ stepId: current.binding.stepId,
411
+ findingId: finding.id,
412
+ });
413
+ }
414
+ for (const [label, value] of [
415
+ ['summary', finding.summary],
416
+ ['evidence', finding.evidence],
417
+ ['requiredAction', finding.requiredAction],
418
+ ]) {
419
+ if (typeof value !== 'string' || !value.trim()) {
420
+ throw new WorkflowError('INVALID_ARGUMENT', `Strict Step Review finding ${label} is required.`, {
421
+ stepId: current.binding.stepId,
422
+ findingId: finding.id,
423
+ });
424
+ }
425
+ }
426
+ }
427
+ if (input.review.status === 'passed' && input.review.findings.length > 0) {
428
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review cannot mark passed while findings remain.', {
429
+ stepId: current.binding.stepId,
430
+ });
431
+ }
432
+ if (input.review.status === 'failed' && input.review.findings.length === 0) {
433
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review failure must include at least one finding.', {
434
+ stepId: current.binding.stepId,
435
+ });
436
+ }
437
+ assertHash64(input.reviewerOutputHash, 'Strict Step Review reviewer output hash');
438
+ if (!GIT_SHA_PATTERN.test(input.reviewedCommit)) {
439
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review reviewed commit is not a full Git SHA.', {
440
+ stepId: current.binding.stepId,
441
+ reviewedCommit: input.reviewedCommit,
442
+ });
443
+ }
444
+ if (input.reviewedCommit !== current.binding.completionCommit) {
445
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review must bind the exact completion commit.', {
446
+ stepId: current.binding.stepId,
447
+ reviewedCommit: input.reviewedCommit,
448
+ completionCommit: current.binding.completionCommit,
449
+ });
450
+ }
451
+ if (input.isolationResult !== 'verified' && input.isolationResult !== 'unverified') {
452
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review isolation result is invalid.', {
453
+ stepId: current.binding.stepId,
454
+ isolationResult: input.isolationResult,
455
+ });
456
+ }
457
+ }
458
+ function reuseReviewRecordedEvent(current, input) {
459
+ const existing = current.reviewRecordedEvent;
460
+ if (!existing)
461
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review review-recorded event is unexpectedly missing.', {
462
+ stepId: current.binding.stepId,
463
+ });
464
+ if (existing.decision !== input.review.status) {
465
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review already recorded a conflicting decision for this completion commit.', {
466
+ stepId: current.binding.stepId,
467
+ existingDecision: existing.decision,
468
+ requestedDecision: input.review.status,
469
+ completionCommit: current.binding.completionCommit,
470
+ });
471
+ }
472
+ return existing;
473
+ }
474
+ function reuseReviewerAttestationEvent(current, reviewRecordedEvent, input) {
475
+ const existing = current.reviewerAttestationEvent;
476
+ if (!existing)
477
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation is unexpectedly missing.', {
478
+ stepId: current.binding.stepId,
479
+ });
480
+ validateAttestationBinding(current.binding, reviewRecordedEvent, existing);
481
+ const reviewer = input.review.reviewer.trim();
482
+ if (existing.reviewer !== reviewer
483
+ || existing.isolationResult !== input.isolationResult
484
+ || existing.outputHash !== input.reviewerOutputHash
485
+ || existing.reviewedCommit !== input.reviewedCommit) {
486
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review already recorded a conflicting reviewer attestation for this completion commit.', {
487
+ stepId: current.binding.stepId,
488
+ completionCommit: current.binding.completionCommit,
489
+ existingReviewer: existing.reviewer,
490
+ requestedReviewer: reviewer,
491
+ existingIsolationResult: existing.isolationResult,
492
+ requestedIsolationResult: input.isolationResult,
493
+ existingOutputHash: existing.outputHash,
494
+ requestedOutputHash: input.reviewerOutputHash,
495
+ existingReviewedCommit: existing.reviewedCommit,
496
+ requestedReviewedCommit: input.reviewedCommit,
497
+ });
498
+ }
499
+ return existing;
500
+ }
501
+ function reuseAttestationVerifiedEvent(current, reviewRecordedEvent, reviewerAttestationEvent) {
502
+ const existing = current.attestationVerifiedEvent;
503
+ if (!existing)
504
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification is unexpectedly missing.', {
505
+ stepId: current.binding.stepId,
506
+ });
507
+ validateAttestationVerifiedChain(current.binding, reviewRecordedEvent, reviewerAttestationEvent, existing);
508
+ return existing;
509
+ }
510
+ function validateAttestationBinding(binding, reviewRecordedEvent, attestation) {
511
+ if (attestation.stepId !== binding.stepId
512
+ || attestation.taskId !== reviewRecordedEvent.taskId
513
+ || attestation.reviewedCommit !== binding.completionCommit
514
+ || attestation.reviewEventHash !== reviewRecordedEvent.eventHash
515
+ || attestation.planHash !== binding.planHash
516
+ || attestation.evidenceHash !== binding.evidenceHash) {
517
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review reviewer attestation does not match the current completion binding.', {
518
+ stepId: binding.stepId,
519
+ completionCommit: binding.completionCommit,
520
+ reviewEventId: reviewRecordedEvent.eventId,
521
+ attestationEventId: attestation.eventId,
522
+ });
523
+ }
524
+ }
525
+ function validateHistoricalAttestationBinding(reviewRecordedEvent, attestation) {
526
+ if (reviewRecordedEvent.eventType !== 'review-recorded') {
527
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation must target a review-recorded Step Review event.', {
528
+ reviewEventId: reviewRecordedEvent.eventId,
529
+ reviewEventType: reviewRecordedEvent.eventType,
530
+ attestationEventId: attestation.eventId,
531
+ });
532
+ }
533
+ if (attestation.taskId !== reviewRecordedEvent.taskId
534
+ || attestation.stepId !== reviewRecordedEvent.stepId
535
+ || attestation.reviewedCommit !== reviewRecordedEvent.completionCommit
536
+ || attestation.reviewEventHash !== reviewRecordedEvent.eventHash
537
+ || attestation.planHash !== reviewRecordedEvent.planHash
538
+ || attestation.evidenceHash !== reviewRecordedEvent.evidenceHash) {
539
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation does not match the referenced review-recorded Step Review event binding.', {
540
+ reviewEventId: reviewRecordedEvent.eventId,
541
+ attestationEventId: attestation.eventId,
542
+ reviewEventHash: reviewRecordedEvent.eventHash,
543
+ });
544
+ }
545
+ }
546
+ function validateAttestationVerifiedChain(binding, reviewRecordedEvent, reviewerAttestationEvent, verifiedEvent) {
547
+ if (!eventMatchesBinding(verifiedEvent, binding)) {
548
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification does not match the current completion binding.', {
549
+ stepId: binding.stepId,
550
+ eventId: verifiedEvent.eventId,
551
+ });
552
+ }
553
+ if (reviewerAttestationEvent.isolationResult !== 'verified') {
554
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification cannot exist for an unverified isolation result.', {
555
+ stepId: binding.stepId,
556
+ attestationEventId: reviewerAttestationEvent.eventId,
557
+ verifiedEventId: verifiedEvent.eventId,
558
+ });
559
+ }
560
+ if (verifiedEvent.reviewerAttestationHash !== reviewerAttestationEvent.eventHash) {
561
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification does not reference the expected attestation event.', {
562
+ stepId: binding.stepId,
563
+ attestationEventId: reviewerAttestationEvent.eventId,
564
+ verifiedEventId: verifiedEvent.eventId,
565
+ });
566
+ }
567
+ if (verifiedEvent.decision !== reviewRecordedEvent.decision) {
568
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification decision does not match the review event.', {
569
+ stepId: binding.stepId,
570
+ reviewEventId: reviewRecordedEvent.eventId,
571
+ verifiedEventId: verifiedEvent.eventId,
572
+ reviewDecision: reviewRecordedEvent.decision,
573
+ verifiedDecision: verifiedEvent.decision,
574
+ });
575
+ }
576
+ validateAttestationBinding(binding, reviewRecordedEvent, reviewerAttestationEvent);
577
+ }
578
+ function validateHistoricalAttestationVerifiedChain(reviewRecordedEvent, reviewerAttestationEvent, verifiedEvent) {
579
+ if (reviewRecordedEvent.eventType !== 'review-recorded') {
580
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event must link to a review-recorded Step Review event.', {
581
+ reviewEventId: reviewRecordedEvent.eventId,
582
+ reviewEventType: reviewRecordedEvent.eventType,
583
+ verifiedEventId: verifiedEvent.eventId,
584
+ });
585
+ }
586
+ if (!stepReviewEventsMatchStructuralBinding(verifiedEvent, reviewRecordedEvent)) {
587
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event does not match the referenced review-recorded binding.', {
588
+ reviewEventId: reviewRecordedEvent.eventId,
589
+ verifiedEventId: verifiedEvent.eventId,
590
+ });
591
+ }
592
+ if (reviewerAttestationEvent.isolationResult !== 'verified') {
593
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event cannot link to an unverified reviewer attestation.', {
594
+ reviewEventId: reviewRecordedEvent.eventId,
595
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
596
+ verifiedEventId: verifiedEvent.eventId,
597
+ });
598
+ }
599
+ if (verifiedEvent.reviewerAttestationHash !== reviewerAttestationEvent.eventHash) {
600
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event does not reference the expected reviewer attestation.', {
601
+ reviewEventId: reviewRecordedEvent.eventId,
602
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
603
+ verifiedEventId: verifiedEvent.eventId,
604
+ });
605
+ }
606
+ if (verifiedEvent.decision !== reviewRecordedEvent.decision) {
607
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review decision does not match the referenced review-recorded event.', {
608
+ reviewEventId: reviewRecordedEvent.eventId,
609
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
610
+ verifiedEventId: verifiedEvent.eventId,
611
+ reviewDecision: reviewRecordedEvent.decision,
612
+ verifiedDecision: verifiedEvent.decision,
613
+ });
614
+ }
615
+ validateHistoricalAttestationBinding(reviewRecordedEvent, reviewerAttestationEvent);
616
+ }
617
+ function appendStepReviewEvent(store, task, event) {
618
+ return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), STEP_REVIEW_EVENTS_SIDECAR, event, {
619
+ validateEvent: (candidate, context) => validateStoredStepReviewEvent(candidate, task.id, context.file, context.line),
620
+ });
621
+ }
622
+ function appendReviewerAttestationEvent(store, task, event) {
623
+ return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REVIEWER_ATTESTATIONS_SIDECAR, event, {
624
+ validateEvent: (candidate, context) => validateStoredReviewerAttestationEvent(candidate, task.id, context.file, context.line),
625
+ });
626
+ }
627
+ function validateStoredStepReviewEvent(event, expectedTaskId, file, line) {
628
+ if (event.taskId !== expectedTaskId) {
629
+ throw new WorkflowError('STATE_CORRUPT', 'Step Review event task binding does not match its task root.', {
630
+ file,
631
+ line,
632
+ eventId: event.eventId,
633
+ expectedTaskId,
634
+ actualTaskId: event.taskId,
635
+ });
636
+ }
637
+ assertPositiveInteger(event.taskRevision, 'Step Review taskRevision', file, line, event.eventId);
638
+ assertNonEmptyString(event.stepId, 'Step Review stepId', file, line, event.eventId);
639
+ assertHash64(event.stepDefinitionHash, 'Step Review stepDefinitionHash');
640
+ assertGitSha(event.completionCommit, 'Step Review completionCommit', file, line, event.eventId);
641
+ assertHash64(event.checksHash, 'Step Review checksHash');
642
+ assertHash64(event.planHash, 'Step Review planHash');
643
+ assertOptionalHash64(event.evidenceHash, 'Step Review evidenceHash', file, line, event.eventId);
644
+ if (!STEP_REVIEW_EVENT_TYPES.has(event.eventType)) {
645
+ throw new WorkflowError('STATE_CORRUPT', 'Step Review eventType is invalid.', {
646
+ file,
647
+ line,
648
+ eventId: event.eventId,
649
+ eventType: event.eventType,
650
+ });
651
+ }
652
+ if (!REVIEW_DECISIONS.has(event.decision)) {
653
+ throw new WorkflowError('STATE_CORRUPT', 'Step Review decision is invalid.', {
654
+ file,
655
+ line,
656
+ eventId: event.eventId,
657
+ decision: event.decision,
658
+ });
659
+ }
660
+ if (event.eventType === 'pending') {
661
+ if (event.decision !== 'pending' || event.reviewerAttestationHash !== null) {
662
+ throw new WorkflowError('STATE_CORRUPT', 'Pending Step Review event must remain unattested with decision pending.', {
663
+ file,
664
+ line,
665
+ eventId: event.eventId,
666
+ });
667
+ }
668
+ }
669
+ if (event.eventType === 'review-recorded') {
670
+ if (event.decision === 'pending' || event.reviewerAttestationHash !== null) {
671
+ throw new WorkflowError('STATE_CORRUPT', 'Review-recorded Step Review event must carry a terminal review decision and no attestation hash.', {
672
+ file,
673
+ line,
674
+ eventId: event.eventId,
675
+ });
676
+ }
677
+ }
678
+ if (event.eventType === 'attestation-verified') {
679
+ if (event.decision === 'pending' || typeof event.reviewerAttestationHash !== 'string') {
680
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event must carry a terminal decision and attestation hash.', {
681
+ file,
682
+ line,
683
+ eventId: event.eventId,
684
+ });
685
+ }
686
+ assertHash64(event.reviewerAttestationHash, 'Step Review reviewerAttestationHash');
687
+ }
688
+ }
689
+ function validateStoredReviewerAttestationEvent(event, expectedTaskId, file, line) {
690
+ if (event.taskId !== expectedTaskId) {
691
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation task binding does not match its task root.', {
692
+ file,
693
+ line,
694
+ eventId: event.eventId,
695
+ expectedTaskId,
696
+ actualTaskId: event.taskId,
697
+ });
698
+ }
699
+ assertNonEmptyString(event.stepId, 'Reviewer attestation stepId', file, line, event.eventId);
700
+ assertNonEmptyString(event.reviewer, 'Reviewer attestation reviewer', file, line, event.eventId);
701
+ if (event.isolationResult !== 'verified' && event.isolationResult !== 'unverified') {
702
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation isolation result is invalid.', {
703
+ file,
704
+ line,
705
+ eventId: event.eventId,
706
+ isolationResult: event.isolationResult,
707
+ });
708
+ }
709
+ assertHash64(event.outputHash, 'Reviewer attestation outputHash');
710
+ assertGitSha(event.reviewedCommit, 'Reviewer attestation reviewedCommit', file, line, event.eventId);
711
+ assertHash64(event.reviewEventHash, 'Reviewer attestation reviewEventHash');
712
+ assertHash64(event.planHash, 'Reviewer attestation planHash');
713
+ assertOptionalHash64(event.evidenceHash, 'Reviewer attestation evidenceHash', file, line, event.eventId);
714
+ }
715
+ function findLastIndex(items, predicate) {
716
+ for (let index = items.length - 1; index >= 0; index -= 1) {
717
+ if (predicate(items[index]))
718
+ return index;
719
+ }
720
+ return -1;
721
+ }
722
+ function indexStepReviewEventsByHash(stepReviewEvents, eventType) {
723
+ const indexed = new Map();
724
+ for (const event of stepReviewEvents) {
725
+ if (event.eventType !== eventType)
726
+ continue;
727
+ const matches = indexed.get(event.eventHash) ?? [];
728
+ matches.push(event);
729
+ indexed.set(event.eventHash, matches);
730
+ }
731
+ return indexed;
732
+ }
733
+ function indexReviewerAttestationsByHash(reviewerAttestations) {
734
+ const indexed = new Map();
735
+ for (const event of reviewerAttestations) {
736
+ const matches = indexed.get(event.eventHash) ?? [];
737
+ matches.push(event);
738
+ indexed.set(event.eventHash, matches);
739
+ }
740
+ return indexed;
741
+ }
742
+ function assertPositiveInteger(value, label, file, line, eventId) {
743
+ if (!Number.isInteger(value) || Number(value) < 1) {
744
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a positive integer.`, {
745
+ file,
746
+ line,
747
+ eventId,
748
+ value,
749
+ });
750
+ }
751
+ }
752
+ function assertNonEmptyString(value, label, file, line, eventId) {
753
+ if (typeof value !== 'string' || !value.trim()) {
754
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a non-empty string.`, {
755
+ file,
756
+ line,
757
+ eventId,
758
+ value,
759
+ });
760
+ }
761
+ }
762
+ function assertGitSha(value, label, file, line, eventId) {
763
+ if (typeof value !== 'string' || !GIT_SHA_PATTERN.test(value)) {
764
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a full Git SHA.`, {
765
+ file,
766
+ line,
767
+ eventId,
768
+ value,
769
+ });
770
+ }
771
+ }
772
+ function assertOptionalHash64(value, label, file, line, eventId) {
773
+ if (value === null)
774
+ return;
775
+ if (typeof value !== 'string') {
776
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be null or a sha256 hex digest.`, {
777
+ file,
778
+ line,
779
+ eventId,
780
+ value,
781
+ });
782
+ }
783
+ assertHash64(value, label);
784
+ }
785
+ //# sourceMappingURL=review.js.map