codex-workflow-v2 2.0.0-alpha.4 → 2.0.0-alpha.7

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 (82) hide show
  1. package/README.md +114 -10
  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 +40 -0
  6. package/dist/src/alpha6/handoff.js +981 -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 +52 -0
  12. package/dist/src/alpha6/milestone.js +1103 -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 +908 -0
  16. package/dist/src/alpha6/plan-risk.js.map +1 -0
  17. package/dist/src/alpha6/remediation.d.ts +21 -0
  18. package/dist/src/alpha6/remediation.js +754 -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/alpha7/autonomy.d.ts +61 -0
  27. package/dist/src/alpha7/autonomy.js +256 -0
  28. package/dist/src/alpha7/autonomy.js.map +1 -0
  29. package/dist/src/cli.js +109 -19
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/contracts.d.ts +268 -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 +5 -0
  35. package/dist/src/index.js +3 -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 +39 -1
  44. package/dist/src/state/store.js +127 -1
  45. package/dist/src/state/store.js.map +1 -1
  46. package/dist/src/version.d.ts +1 -1
  47. package/dist/src/version.js +1 -1
  48. package/dist/src/workflow.d.ts +113 -8
  49. package/dist/src/workflow.js +1635 -78
  50. package/dist/src/workflow.js.map +1 -1
  51. package/docs/autonomy-guardrails.md +164 -0
  52. package/docs/decisions.md +39 -0
  53. package/docs/delegated-approval.md +37 -5
  54. package/docs/development-flow.md +45 -1
  55. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +155 -136
  56. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +236 -223
  57. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +225 -206
  58. package/docs/project-memory.md +7 -0
  59. package/docs/release.md +9 -0
  60. package/docs/updating-existing-project.md +63 -5
  61. package/docs/validation-report.md +52 -34
  62. package/package.json +1 -1
  63. package/plugins/codex-workflow-gateway/references/protocol.md +182 -6
  64. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +35 -1
  65. package/references/git-policy.md +5 -2
  66. package/references/state-machine.md +43 -0
  67. package/references/validation-and-review.md +28 -1
  68. package/roles/delivery-coordinator.md +21 -1
  69. package/roles/independent-reviewer.md +3 -0
  70. package/roles/technical-planner.md +10 -0
  71. package/roles/worker.md +4 -0
  72. package/schemas/adoption-posture-event.schema.json +129 -0
  73. package/schemas/corrective-decision-event.schema.json +55 -0
  74. package/schemas/corrective-plan-audit.schema.json +20 -0
  75. package/schemas/milestone-autonomy-event.schema.json +45 -0
  76. package/schemas/milestone-scope-change-event.schema.json +70 -0
  77. package/schemas/milestone-transaction-journal.schema.json +95 -0
  78. package/schemas/plan-risk-audit-event.schema.json +107 -0
  79. package/schemas/remediation-event.schema.json +53 -0
  80. package/schemas/reviewer-attestation-event.schema.json +49 -0
  81. package/schemas/step-review-event.schema.json +74 -0
  82. package/schemas/task-handoff-event.schema.json +116 -0
@@ -0,0 +1,754 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { WorkflowError } from '../errors.js';
4
+ import { createUlid } from '../ulid.js';
5
+ import { assertHash64 } from './store-sidecars.js';
6
+ import { readReviewerAttestations, readStepReviewEvents } from './review.js';
7
+ export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
8
+ export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
9
+ const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
10
+ const REMEDIATION_FAILURE_KINDS = new Set([
11
+ 'checks-failed',
12
+ 'review-failed',
13
+ 'review-unverified',
14
+ ]);
15
+ const REMEDIATION_MODES = new Set(['ordinary', 'corrective']);
16
+ const CORRECTIVE_DECISION_VALUES = new Set([
17
+ 'continue-fix',
18
+ 'replan-required',
19
+ 'split-required',
20
+ 'stop-escalate',
21
+ ]);
22
+ export function readRemediationEvents(store, task) {
23
+ return validateRemediationHistory(store, task).remediationEvents;
24
+ }
25
+ export function readCorrectiveDecisionEvents(store, task) {
26
+ return validateRemediationHistory(store, task).correctiveDecisionEvents;
27
+ }
28
+ export function defaultCorrectiveAuditorActor(taskId) {
29
+ if (!/^TASK-[0-9A-HJKMNP-TV-Z]{26}$/.test(taskId)) {
30
+ throw new WorkflowError('INVALID_ARGUMENT', `Cannot derive a corrective Auditor actor from invalid Task ID ${taskId}.`);
31
+ }
32
+ return `agent:corrective-auditor:${taskId.toLowerCase()}`;
33
+ }
34
+ export function assertCorrectiveAuditorIndependence(store, task, stepId, auditor) {
35
+ const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
36
+ const latestRemediationEvent = remediationEvents.at(-1) ?? null;
37
+ if (!latestRemediationEvent || latestRemediationEvent.failureKind === 'checks-failed')
38
+ return;
39
+ const reviewerAttestationHash = latestRemediationEvent.failureEvidence.reviewerAttestationHash;
40
+ if (!reviewerAttestationHash) {
41
+ throw new WorkflowError('STATE_CORRUPT', 'Latest guarded remediation event is missing its strict-review attestation binding.', {
42
+ taskId: task.id,
43
+ stepId,
44
+ eventId: latestRemediationEvent.eventId,
45
+ });
46
+ }
47
+ const attestation = readReviewerAttestations(store, task)
48
+ .find((event) => event.eventHash === reviewerAttestationHash) ?? null;
49
+ if (!attestation) {
50
+ throw new WorkflowError('STATE_CORRUPT', 'Latest guarded remediation event references a missing reviewer attestation.', {
51
+ taskId: task.id,
52
+ stepId,
53
+ reviewerAttestationHash,
54
+ });
55
+ }
56
+ if (attestation.reviewer.trim() === auditor.trim()) {
57
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} must be recorded by an auditor distinct from the latest strict reviewer.`, {
58
+ taskId: task.id,
59
+ stepId,
60
+ reviewer: attestation.reviewer,
61
+ auditor: auditor.trim(),
62
+ });
63
+ }
64
+ }
65
+ function validateRemediationHistory(store, task) {
66
+ const remediationEvents = readRawRemediationEvents(store, task);
67
+ const correctiveDecisionEvents = readRawCorrectiveDecisionEvents(store, task);
68
+ validateRemediationAttemptOrdinals(remediationEvents, task);
69
+ validateRemediationReviewReferences(store, task, remediationEvents);
70
+ validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
71
+ validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisionEvents);
72
+ return {
73
+ remediationEvents,
74
+ correctiveDecisionEvents,
75
+ };
76
+ }
77
+ function readRawRemediationEvents(store, task) {
78
+ const sidecarFile = path.join(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR);
79
+ if (!existsSync(sidecarFile))
80
+ return [];
81
+ return store.readSidecarEvents(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, {
82
+ validateEvent: (event, context) => validateStoredRemediationEvent(event, task, context.file, context.line),
83
+ });
84
+ }
85
+ function readRawCorrectiveDecisionEvents(store, task) {
86
+ const sidecarFile = path.join(store.taskRoot(task.projectId, task.id), CORRECTIVE_DECISIONS_SIDECAR);
87
+ if (!existsSync(sidecarFile))
88
+ return [];
89
+ return store.readSidecarEvents(store.taskRoot(task.projectId, task.id), CORRECTIVE_DECISIONS_SIDECAR, {
90
+ validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
91
+ });
92
+ }
93
+ export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now) {
94
+ const existing = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
95
+ const matching = existing.filter((event) => remediationEventMatchesFailure(event, failureKind, failureEvidence));
96
+ if (matching.length > 1) {
97
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation events match the same failure identity.', {
98
+ taskId: task.id,
99
+ stepId,
100
+ failureKind,
101
+ matchingEventIds: matching.map((event) => event.eventId),
102
+ });
103
+ }
104
+ if (matching.length === 1) {
105
+ const reused = matching[0];
106
+ const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
107
+ const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task));
108
+ if (reused.attemptOrdinal !== gate.nextAttemptOrdinal || reused.mode !== gate.mode || reused.guardedCategory !== gate.guardedCategory) {
109
+ throw new WorkflowError('STATE_CORRUPT', 'Existing remediation event conflicts with the current guarded retry binding.', {
110
+ taskId: task.id,
111
+ stepId,
112
+ eventId: reused.eventId,
113
+ expectedAttemptOrdinal: gate.nextAttemptOrdinal,
114
+ actualAttemptOrdinal: reused.attemptOrdinal,
115
+ expectedMode: gate.mode,
116
+ actualMode: reused.mode,
117
+ });
118
+ }
119
+ return reused;
120
+ }
121
+ const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task));
122
+ return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, {
123
+ eventId: `RME-${createUlid(now.getTime())}`,
124
+ recordedAt: now.toISOString(),
125
+ taskId: task.id,
126
+ stepId,
127
+ guardedCategory: gate.guardedCategory,
128
+ failureKind,
129
+ failureEvidence,
130
+ attemptOrdinal: gate.nextAttemptOrdinal,
131
+ mode: gate.mode,
132
+ }, {
133
+ validateEvent: (event, context) => validateStoredRemediationEvent(event, task, context.file, context.line),
134
+ });
135
+ }
136
+ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAudit, planRiskAudit, planHash, now) {
137
+ validateCorrectiveDecisionAudit(correctiveAudit);
138
+ const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
139
+ const triggeringAttemptCount = remediationEvents.length + 1;
140
+ if (triggeringAttemptCount < 3) {
141
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision is not available for ${stepId} before two guarded remediation failures exist.`, {
142
+ taskId: task.id,
143
+ stepId,
144
+ triggeringAttemptCount,
145
+ });
146
+ }
147
+ if (triggeringAttemptCount > 3
148
+ && correctiveAudit.decision !== 'split-required'
149
+ && correctiveAudit.decision !== 'stop-escalate') {
150
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} reached the hard stop after its third failed remediation review.`, {
151
+ taskId: task.id,
152
+ stepId,
153
+ failedAttemptCount: remediationEvents.length,
154
+ requestedDecision: correctiveAudit.decision,
155
+ allowedDecisions: ['split-required', 'stop-escalate'],
156
+ });
157
+ }
158
+ const decisions = readCorrectiveDecisionEvents(store, task);
159
+ const matching = decisions.filter((event) => event.taskId === task.id
160
+ && event.stepId === stepId
161
+ && event.triggeringAttemptCount === triggeringAttemptCount);
162
+ if (matching.length > 1) {
163
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple corrective decisions exist for the current guarded remediation binding.', {
164
+ taskId: task.id,
165
+ stepId,
166
+ triggeringAttemptCount,
167
+ eventIds: matching.map((event) => event.eventId),
168
+ });
169
+ }
170
+ const coveredFindingIds = normalizeStringList(correctiveAudit.reviewedFindingIds, 'Corrective decision reviewedFindingIds');
171
+ const coveredEventIds = remediationEvents.map((event) => event.eventId);
172
+ if (matching.length === 1) {
173
+ const reused = matching[0];
174
+ if (reused.planHash !== planHash) {
175
+ throw new WorkflowError('STATE_CONFLICT', 'A corrective decision already exists for this Step and triggering ordinal under a different Plan hash.', {
176
+ taskId: task.id,
177
+ stepId,
178
+ triggeringAttemptCount,
179
+ existingPlanHash: reused.planHash,
180
+ requestedPlanHash: planHash,
181
+ eventId: reused.eventId,
182
+ });
183
+ }
184
+ if (reused.decision !== correctiveAudit.decision
185
+ || reused.auditor !== correctiveAudit.auditor.trim()
186
+ || !sameStringArray(reused.coveredFindingIds, coveredFindingIds)
187
+ || !sameStringArray(reused.coveredEventIds, coveredEventIds)) {
188
+ throw new WorkflowError('STATE_CONFLICT', 'Existing corrective decision payload does not match the requested guarded remediation binding.', {
189
+ taskId: task.id,
190
+ stepId,
191
+ triggeringAttemptCount,
192
+ eventId: reused.eventId,
193
+ });
194
+ }
195
+ return reused;
196
+ }
197
+ requireGuardedCategory(planRiskAudit, stepId);
198
+ return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), CORRECTIVE_DECISIONS_SIDECAR, {
199
+ eventId: `CRD-${createUlid(now.getTime())}`,
200
+ recordedAt: now.toISOString(),
201
+ taskId: task.id,
202
+ stepId,
203
+ triggeringAttemptCount,
204
+ decision: correctiveAudit.decision,
205
+ auditor: correctiveAudit.auditor.trim(),
206
+ coveredFindingIds,
207
+ coveredEventIds,
208
+ planHash,
209
+ }, {
210
+ validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
211
+ });
212
+ }
213
+ export function assertGuardedRemediationAttemptAllowed(store, task, stepId, planRiskAudit) {
214
+ const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
215
+ return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, readCorrectiveDecisionEvents(store, task), {
216
+ enforceRuntimeGate: true,
217
+ });
218
+ }
219
+ export function findFailedGuardedStepRequiringCorrectiveDecision(store, task, planRiskAudit) {
220
+ const failedGuardedSteps = task.steps.filter((step) => step.status === 'failed' && planRiskAudit.reviewRequiredStepIds.includes(step.id));
221
+ const matches = failedGuardedSteps
222
+ .map((step) => {
223
+ const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === step.id);
224
+ if (remediationEvents.length < 2)
225
+ return null;
226
+ return deriveAttemptGateFromHistory(task, step.id, planRiskAudit, remediationEvents, readCorrectiveDecisionEvents(store, task));
227
+ })
228
+ .filter((value) => value !== null);
229
+ if (matches.length === 0)
230
+ return null;
231
+ if (matches.length > 1) {
232
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple guarded failed Steps simultaneously require corrective remediation decisions.', {
233
+ taskId: task.id,
234
+ stepIds: matches.map((match) => match.stepId),
235
+ });
236
+ }
237
+ return matches[0];
238
+ }
239
+ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
240
+ const attemptCount = remediationEvents.length;
241
+ const nextAttemptOrdinal = attemptCount + 1;
242
+ const guardedCategory = requireGuardedCategory(planRiskAudit, stepId);
243
+ if (attemptCount >= 3) {
244
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} reached the hard stop after its third failed remediation review.`, {
245
+ taskId: task.id,
246
+ stepId,
247
+ failedAttemptCount: attemptCount,
248
+ requiredAction: 'Record split-required or stop-escalate; another remediation run is prohibited.',
249
+ });
250
+ }
251
+ if (attemptCount < 2) {
252
+ return {
253
+ stepId,
254
+ guardedCategory,
255
+ attemptCount,
256
+ nextAttemptOrdinal,
257
+ mode: 'ordinary',
258
+ correctiveDecision: null,
259
+ };
260
+ }
261
+ const correctiveDecision = findCurrentCorrectiveDecision(task, stepId, nextAttemptOrdinal, correctiveDecisions);
262
+ if (!correctiveDecision) {
263
+ if (options.enforceRuntimeGate) {
264
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} exhausted two ordinary remediation attempts and requires a corrective decision before another run.`, {
265
+ taskId: task.id,
266
+ stepId,
267
+ attemptCount,
268
+ requiredSidecar: CORRECTIVE_DECISIONS_SIDECAR,
269
+ nextAttemptOrdinal,
270
+ allowedDecision: 'continue-fix',
271
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
272
+ });
273
+ }
274
+ return {
275
+ stepId,
276
+ guardedCategory,
277
+ attemptCount,
278
+ nextAttemptOrdinal,
279
+ mode: 'ordinary',
280
+ correctiveDecision: null,
281
+ };
282
+ }
283
+ if (correctiveDecision.planHash !== task.planHash) {
284
+ if (correctiveDecision.decision === 'replan-required') {
285
+ return {
286
+ stepId,
287
+ guardedCategory,
288
+ attemptCount,
289
+ nextAttemptOrdinal,
290
+ mode: 'corrective',
291
+ correctiveDecision,
292
+ };
293
+ }
294
+ if (options.enforceRuntimeGate) {
295
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} has a corrective decision for a different Plan hash and cannot continue under the current Plan.`, {
296
+ taskId: task.id,
297
+ stepId,
298
+ triggeringAttemptCount: nextAttemptOrdinal,
299
+ currentPlanHash: task.planHash,
300
+ decisionPlanHash: correctiveDecision.planHash,
301
+ decisionEventId: correctiveDecision.eventId,
302
+ });
303
+ }
304
+ return {
305
+ stepId,
306
+ guardedCategory,
307
+ attemptCount,
308
+ nextAttemptOrdinal,
309
+ mode: 'ordinary',
310
+ correctiveDecision,
311
+ };
312
+ }
313
+ if (correctiveDecision.decision !== 'continue-fix' && options.enforceRuntimeGate) {
314
+ throw correctiveDecisionBlockedError(task, stepId, attemptCount, correctiveDecision);
315
+ }
316
+ return {
317
+ stepId,
318
+ guardedCategory,
319
+ attemptCount,
320
+ nextAttemptOrdinal,
321
+ mode: correctiveDecision.decision === 'continue-fix' ? 'corrective' : 'ordinary',
322
+ correctiveDecision,
323
+ };
324
+ }
325
+ function findCurrentCorrectiveDecision(task, stepId, triggeringAttemptOrdinal, events) {
326
+ const matches = events.filter((event) => event.taskId === task.id
327
+ && event.stepId === stepId
328
+ && event.triggeringAttemptCount === triggeringAttemptOrdinal);
329
+ if (matches.length === 0)
330
+ return null;
331
+ if (matches.length > 1) {
332
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple corrective decisions exist for the same guarded remediation binding.', {
333
+ taskId: task.id,
334
+ stepId,
335
+ triggeringAttemptCount: triggeringAttemptOrdinal,
336
+ eventIds: matches.map((event) => event.eventId),
337
+ });
338
+ }
339
+ return matches[0];
340
+ }
341
+ function correctiveDecisionBlockedError(task, stepId, attemptCount, event) {
342
+ if (event.decision === 'replan-required') {
343
+ return new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} cannot continue because the corrective decision requires a replan.`, {
344
+ taskId: task.id,
345
+ stepId,
346
+ attemptCount,
347
+ decision: event.decision,
348
+ decisionEventId: event.eventId,
349
+ auditor: event.auditor,
350
+ });
351
+ }
352
+ if (event.decision === 'split-required') {
353
+ return new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} cannot continue because the corrective decision requires splitting work into a separate Task.`, {
354
+ taskId: task.id,
355
+ stepId,
356
+ attemptCount,
357
+ decision: event.decision,
358
+ decisionEventId: event.eventId,
359
+ auditor: event.auditor,
360
+ });
361
+ }
362
+ return new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} cannot continue because the corrective decision requires coordinator escalation.`, {
363
+ taskId: task.id,
364
+ stepId,
365
+ attemptCount,
366
+ decision: event.decision,
367
+ decisionEventId: event.eventId,
368
+ auditor: event.auditor,
369
+ });
370
+ }
371
+ function requireGuardedCategory(planRiskAudit, stepId) {
372
+ const classification = planRiskAudit.audit.stepClassifications.find((candidate) => candidate.stepId === stepId) ?? null;
373
+ if (!classification || classification.categories.length === 0 || !classification.reviewRequired) {
374
+ throw new WorkflowError('STATE_CORRUPT', `Current Plan Risk Audit does not define a guarded category for ${stepId}.`, {
375
+ taskId: planRiskAudit.audit.taskId,
376
+ stepId,
377
+ });
378
+ }
379
+ return classification.categories[0];
380
+ }
381
+ function validateStoredRemediationEvent(event, task, file, line) {
382
+ if (event.taskId !== task.id) {
383
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event taskId does not match the current Task.', {
384
+ file,
385
+ line,
386
+ taskId: task.id,
387
+ eventTaskId: event.taskId,
388
+ });
389
+ }
390
+ if (typeof event.stepId !== 'string' || !event.stepId.trim()) {
391
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event is missing stepId.', { file, line, taskId: task.id });
392
+ }
393
+ if (!REMEDIATION_FAILURE_KINDS.has(event.failureKind)) {
394
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event has an unknown failureKind.', {
395
+ file,
396
+ line,
397
+ taskId: task.id,
398
+ stepId: event.stepId,
399
+ failureKind: event.failureKind,
400
+ });
401
+ }
402
+ if (!REMEDIATION_MODES.has(event.mode)) {
403
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event has an unknown mode.', {
404
+ file,
405
+ line,
406
+ taskId: task.id,
407
+ stepId: event.stepId,
408
+ mode: event.mode,
409
+ });
410
+ }
411
+ assertCommitSha(event.failureEvidence.completionCommit, 'Remediation failure evidence completionCommit', file, line);
412
+ assertHash64(event.failureEvidence.checksHash, `Remediation failure evidence checksHash at ${file}:${line}`);
413
+ if (event.failureEvidence.reviewEventHash !== null) {
414
+ assertHash64(event.failureEvidence.reviewEventHash, `Remediation failure evidence reviewEventHash at ${file}:${line}`);
415
+ }
416
+ if (event.failureEvidence.reviewerAttestationHash !== null) {
417
+ assertHash64(event.failureEvidence.reviewerAttestationHash, `Remediation failure evidence reviewerAttestationHash at ${file}:${line}`);
418
+ }
419
+ if (!Number.isInteger(event.attemptOrdinal) || event.attemptOrdinal <= 0) {
420
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event attemptOrdinal must be a positive integer.', {
421
+ file,
422
+ line,
423
+ taskId: task.id,
424
+ stepId: event.stepId,
425
+ attemptOrdinal: event.attemptOrdinal,
426
+ });
427
+ }
428
+ if (event.failureKind === 'checks-failed') {
429
+ if (event.failureEvidence.reviewEventHash !== null || event.failureEvidence.reviewerAttestationHash !== null) {
430
+ throw new WorkflowError('STATE_CORRUPT', 'checks-failed remediation events must not reference strict review sidecars.', {
431
+ file,
432
+ line,
433
+ taskId: task.id,
434
+ stepId: event.stepId,
435
+ eventId: event.eventId,
436
+ });
437
+ }
438
+ }
439
+ else if (event.failureEvidence.reviewEventHash === null || event.failureEvidence.reviewerAttestationHash === null) {
440
+ throw new WorkflowError('STATE_CORRUPT', 'review-failed and review-unverified remediation events must reference both strict review sidecars.', {
441
+ file,
442
+ line,
443
+ taskId: task.id,
444
+ stepId: event.stepId,
445
+ eventId: event.eventId,
446
+ });
447
+ }
448
+ }
449
+ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
450
+ if (event.taskId !== task.id) {
451
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision taskId does not match the current Task.', {
452
+ file,
453
+ line,
454
+ taskId: task.id,
455
+ eventTaskId: event.taskId,
456
+ });
457
+ }
458
+ if (typeof event.stepId !== 'string' || !event.stepId.trim()) {
459
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision is missing stepId.', { file, line, taskId: task.id });
460
+ }
461
+ if (!Number.isInteger(event.triggeringAttemptCount) || event.triggeringAttemptCount < 3) {
462
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision triggeringAttemptCount must be an integer >= 3.', {
463
+ file,
464
+ line,
465
+ taskId: task.id,
466
+ stepId: event.stepId,
467
+ triggeringAttemptCount: event.triggeringAttemptCount,
468
+ });
469
+ }
470
+ if (!CORRECTIVE_DECISION_VALUES.has(event.decision)) {
471
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision has an unknown decision value.', {
472
+ file,
473
+ line,
474
+ taskId: task.id,
475
+ stepId: event.stepId,
476
+ decision: event.decision,
477
+ });
478
+ }
479
+ if (typeof event.auditor !== 'string' || !event.auditor.trim()) {
480
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision is missing auditor.', {
481
+ file,
482
+ line,
483
+ taskId: task.id,
484
+ stepId: event.stepId,
485
+ });
486
+ }
487
+ normalizeStringList(event.coveredFindingIds, 'Corrective decision coveredFindingIds');
488
+ normalizeStringList(event.coveredEventIds, 'Corrective decision coveredEventIds');
489
+ assertHash64(event.planHash, `Corrective decision planHash at ${file}:${line}`);
490
+ }
491
+ function validateRemediationAttemptOrdinals(events, task) {
492
+ const attemptsByStep = new Map();
493
+ for (const event of events) {
494
+ const expectedAttemptOrdinal = (attemptsByStep.get(event.stepId) ?? 0) + 1;
495
+ if (event.attemptOrdinal !== expectedAttemptOrdinal) {
496
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event attemptOrdinal does not match the sidecar history for this Step.', {
497
+ taskId: task.id,
498
+ stepId: event.stepId,
499
+ eventId: event.eventId,
500
+ expectedAttemptOrdinal,
501
+ actualAttemptOrdinal: event.attemptOrdinal,
502
+ });
503
+ }
504
+ attemptsByStep.set(event.stepId, event.attemptOrdinal);
505
+ }
506
+ }
507
+ function validateRemediationReviewReferences(store, task, events) {
508
+ if (events.length === 0)
509
+ return;
510
+ const requiresStrictReviewLookups = events.some((event) => event.failureEvidence.reviewEventHash !== null || event.failureEvidence.reviewerAttestationHash !== null);
511
+ if (!requiresStrictReviewLookups) {
512
+ for (const event of events) {
513
+ if (event.failureKind !== 'checks-failed') {
514
+ throw new WorkflowError('STATE_CORRUPT', 'Only checks-failed remediation events may omit strict review references.', {
515
+ taskId: task.id,
516
+ stepId: event.stepId,
517
+ eventId: event.eventId,
518
+ failureKind: event.failureKind,
519
+ });
520
+ }
521
+ }
522
+ return;
523
+ }
524
+ const reviewEvents = readStepReviewEvents(store, task);
525
+ const reviewEventsByHash = new Map(reviewEvents.map((event) => [event.eventHash, event]));
526
+ const reviewerAttestations = readReviewerAttestations(store, task);
527
+ const reviewerAttestationsByHash = new Map(reviewerAttestations.map((event) => [event.eventHash, event]));
528
+ const eventKeys = new Set();
529
+ for (const event of events) {
530
+ const key = remediationFailureIdentityKey(event.failureKind, event.failureEvidence);
531
+ if (eventKeys.has(`${event.stepId}\0${key}`)) {
532
+ throw new WorkflowError('STATE_CORRUPT', 'Duplicate remediation events reference the same failure identity.', {
533
+ taskId: task.id,
534
+ stepId: event.stepId,
535
+ eventId: event.eventId,
536
+ });
537
+ }
538
+ eventKeys.add(`${event.stepId}\0${key}`);
539
+ if (event.failureEvidence.reviewEventHash === null && event.failureEvidence.reviewerAttestationHash === null) {
540
+ if (event.failureKind !== 'checks-failed') {
541
+ throw new WorkflowError('STATE_CORRUPT', 'Only checks-failed remediation events may omit strict review references.', {
542
+ taskId: task.id,
543
+ stepId: event.stepId,
544
+ eventId: event.eventId,
545
+ failureKind: event.failureKind,
546
+ });
547
+ }
548
+ continue;
549
+ }
550
+ if (event.failureEvidence.reviewEventHash === null || event.failureEvidence.reviewerAttestationHash === null) {
551
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event must provide both reviewEventHash and reviewerAttestationHash together.', {
552
+ taskId: task.id,
553
+ stepId: event.stepId,
554
+ eventId: event.eventId,
555
+ });
556
+ }
557
+ const reviewEvent = reviewEventsByHash.get(event.failureEvidence.reviewEventHash) ?? null;
558
+ const attestation = reviewerAttestationsByHash.get(event.failureEvidence.reviewerAttestationHash) ?? null;
559
+ if (!reviewEvent || !attestation) {
560
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event references missing strict review sidecar history.', {
561
+ taskId: task.id,
562
+ stepId: event.stepId,
563
+ eventId: event.eventId,
564
+ reviewEventHash: event.failureEvidence.reviewEventHash,
565
+ reviewerAttestationHash: event.failureEvidence.reviewerAttestationHash,
566
+ });
567
+ }
568
+ if (reviewEvent.taskId !== task.id
569
+ || reviewEvent.stepId !== event.stepId
570
+ || reviewEvent.completionCommit !== event.failureEvidence.completionCommit
571
+ || reviewEvent.checksHash !== event.failureEvidence.checksHash) {
572
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event reviewEventHash does not match the exact strict review binding.', {
573
+ taskId: task.id,
574
+ stepId: event.stepId,
575
+ eventId: event.eventId,
576
+ reviewEventId: reviewEvent.eventId,
577
+ });
578
+ }
579
+ if (attestation.taskId !== task.id
580
+ || attestation.stepId !== event.stepId
581
+ || attestation.reviewedCommit !== event.failureEvidence.completionCommit
582
+ || attestation.reviewEventHash !== reviewEvent.eventHash) {
583
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event reviewerAttestationHash does not match the exact strict review binding.', {
584
+ taskId: task.id,
585
+ stepId: event.stepId,
586
+ eventId: event.eventId,
587
+ reviewerAttestationEventId: attestation.eventId,
588
+ });
589
+ }
590
+ const expectedDecision = event.failureKind === 'review-failed' ? 'failed' : 'unverified';
591
+ if (reviewEvent.decision !== expectedDecision) {
592
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation event failureKind does not match the referenced strict review decision.', {
593
+ taskId: task.id,
594
+ stepId: event.stepId,
595
+ eventId: event.eventId,
596
+ failureKind: event.failureKind,
597
+ reviewDecision: reviewEvent.decision,
598
+ });
599
+ }
600
+ }
601
+ }
602
+ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents) {
603
+ const remediationEventsById = new Map(remediationEvents.map((event) => [event.eventId, event]));
604
+ const decisionBindings = new Set();
605
+ for (const decision of decisions) {
606
+ const binding = `${decision.stepId}\0${decision.triggeringAttemptCount}`;
607
+ if (decisionBindings.has(binding)) {
608
+ throw new WorkflowError('STATE_CORRUPT', 'Duplicate corrective decisions exist for the same Step and triggering ordinal.', {
609
+ taskId: task.id,
610
+ stepId: decision.stepId,
611
+ triggeringAttemptCount: decision.triggeringAttemptCount,
612
+ });
613
+ }
614
+ decisionBindings.add(binding);
615
+ const priorEvents = remediationEvents
616
+ .filter((event) => event.stepId === decision.stepId && event.attemptOrdinal < decision.triggeringAttemptCount)
617
+ .sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
618
+ const coveredEventIds = normalizeStringList(decision.coveredEventIds, 'Corrective decision coveredEventIds');
619
+ if (decision.triggeringAttemptCount !== priorEvents.length + 1) {
620
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision triggeringAttemptCount must equal the blocked next attempt ordinal.', {
621
+ taskId: task.id,
622
+ stepId: decision.stepId,
623
+ decisionEventId: decision.eventId,
624
+ triggeringAttemptCount: decision.triggeringAttemptCount,
625
+ priorAttemptCount: priorEvents.length,
626
+ });
627
+ }
628
+ if (!sameStringArray(coveredEventIds, priorEvents.map((event) => event.eventId))) {
629
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision coveredEventIds must exactly cover prior remediation events for that Step binding.', {
630
+ taskId: task.id,
631
+ stepId: decision.stepId,
632
+ decisionEventId: decision.eventId,
633
+ coveredEventIds,
634
+ expectedCoveredEventIds: priorEvents.map((event) => event.eventId),
635
+ });
636
+ }
637
+ for (const eventId of coveredEventIds) {
638
+ const linked = remediationEventsById.get(eventId);
639
+ if (!linked || linked.stepId !== decision.stepId) {
640
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective decision coveredEventIds references an invalid remediation event.', {
641
+ taskId: task.id,
642
+ stepId: decision.stepId,
643
+ decisionEventId: decision.eventId,
644
+ eventId,
645
+ });
646
+ }
647
+ }
648
+ }
649
+ }
650
+ function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions) {
651
+ for (const event of remediationEvents) {
652
+ if (event.attemptOrdinal <= 2) {
653
+ if (event.mode !== 'ordinary') {
654
+ throw new WorkflowError('STATE_CORRUPT', 'The first two remediation attempts must remain ordinary.', {
655
+ taskId: task.id,
656
+ stepId,
657
+ eventId: event.eventId,
658
+ attemptOrdinal: event.attemptOrdinal,
659
+ mode: event.mode,
660
+ });
661
+ }
662
+ continue;
663
+ }
664
+ if (event.mode === 'ordinary') {
665
+ throw new WorkflowError('STATE_CORRUPT', 'Guarded remediation attempt ordinals above 2 must not remain ordinary.', {
666
+ taskId: task.id,
667
+ stepId,
668
+ eventId: event.eventId,
669
+ attemptOrdinal: event.attemptOrdinal,
670
+ });
671
+ }
672
+ const priorEvents = remediationEvents
673
+ .filter((candidate) => candidate.stepId === stepId && candidate.attemptOrdinal < event.attemptOrdinal)
674
+ .sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
675
+ const matching = correctiveDecisions.filter((decision) => decision.taskId === task.id
676
+ && decision.stepId === stepId
677
+ && decision.triggeringAttemptCount === event.attemptOrdinal
678
+ && (decision.decision === 'continue-fix' || decision.decision === 'replan-required')
679
+ && sameStringArray(decision.coveredEventIds, priorEvents.map((candidate) => candidate.eventId)));
680
+ if (matching.length !== 1) {
681
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective remediation mode requires one unique continue-fix or replan-required decision for the exact blocked ordinal.', {
682
+ taskId: task.id,
683
+ stepId,
684
+ eventId: event.eventId,
685
+ attemptOrdinal: event.attemptOrdinal,
686
+ matchingDecisionIds: matching.map((decision) => decision.eventId),
687
+ });
688
+ }
689
+ }
690
+ }
691
+ function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions) {
692
+ const stepIds = new Set(remediationEvents.map((event) => event.stepId));
693
+ for (const stepId of stepIds) {
694
+ const stepEvents = remediationEvents
695
+ .filter((event) => event.stepId === stepId)
696
+ .sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
697
+ validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions);
698
+ }
699
+ }
700
+ function validateCorrectiveDecisionAudit(audit) {
701
+ if (!audit.auditor.trim() || !audit.summary.trim()) {
702
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective decision requires auditor and summary.');
703
+ }
704
+ if (!CORRECTIVE_DECISION_VALUES.has(audit.decision)) {
705
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective decision value is not supported.', {
706
+ decision: audit.decision,
707
+ });
708
+ }
709
+ normalizeStringList(audit.reviewedFindingIds, 'Corrective decision reviewedFindingIds');
710
+ }
711
+ function normalizeStringList(values, label) {
712
+ if (!Array.isArray(values)) {
713
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} must be an array.`);
714
+ }
715
+ const normalized = values.map((value) => {
716
+ if (typeof value !== 'string' || !value.trim()) {
717
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} must contain non-empty strings.`);
718
+ }
719
+ return value.trim();
720
+ });
721
+ if (new Set(normalized).size !== normalized.length) {
722
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} must not contain duplicates.`);
723
+ }
724
+ return normalized;
725
+ }
726
+ function remediationEventMatchesFailure(event, failureKind, failureEvidence) {
727
+ return event.failureKind === failureKind
728
+ && event.failureEvidence.completionCommit === failureEvidence.completionCommit
729
+ && event.failureEvidence.checksHash === failureEvidence.checksHash
730
+ && event.failureEvidence.reviewEventHash === failureEvidence.reviewEventHash
731
+ && event.failureEvidence.reviewerAttestationHash === failureEvidence.reviewerAttestationHash;
732
+ }
733
+ function remediationFailureIdentityKey(failureKind, failureEvidence) {
734
+ return [
735
+ failureKind,
736
+ failureEvidence.completionCommit,
737
+ failureEvidence.checksHash,
738
+ failureEvidence.reviewEventHash ?? '',
739
+ failureEvidence.reviewerAttestationHash ?? '',
740
+ ].join('\0');
741
+ }
742
+ function sameStringArray(left, right) {
743
+ return left.length === right.length && left.every((value, index) => value === right[index]);
744
+ }
745
+ function assertCommitSha(value, label, file, line) {
746
+ if (!COMMIT_SHA_PATTERN.test(value)) {
747
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a full lowercase Git commit SHA.`, {
748
+ file,
749
+ line,
750
+ value,
751
+ });
752
+ }
753
+ }
754
+ //# sourceMappingURL=remediation.js.map