codex-workflow-v2 2.0.0-alpha.7.2 → 2.0.0-beta.1

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 (38) hide show
  1. package/README.md +23 -5
  2. package/dist/src/alpha6/milestone.d.ts +17 -0
  3. package/dist/src/alpha6/milestone.js +54 -0
  4. package/dist/src/alpha6/milestone.js.map +1 -1
  5. package/dist/src/alpha6/remediation.d.ts +18 -1
  6. package/dist/src/alpha6/remediation.js +458 -9
  7. package/dist/src/alpha6/remediation.js.map +1 -1
  8. package/dist/src/beta1/project-transaction.d.ts +52 -0
  9. package/dist/src/beta1/project-transaction.js +297 -0
  10. package/dist/src/beta1/project-transaction.js.map +1 -0
  11. package/dist/src/cli.js +23 -1
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/contracts.d.ts +39 -0
  14. package/dist/src/diagnostics.d.ts +11 -0
  15. package/dist/src/diagnostics.js +54 -0
  16. package/dist/src/diagnostics.js.map +1 -1
  17. package/dist/src/git.js +2 -5
  18. package/dist/src/git.js.map +1 -1
  19. package/dist/src/version.d.ts +1 -1
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/version.js.map +1 -1
  22. package/dist/src/workflow.d.ts +11 -1
  23. package/dist/src/workflow.js +519 -56
  24. package/dist/src/workflow.js.map +1 -1
  25. package/docs/alpha7.2.1-remediation-recovery-brief.md +86 -0
  26. package/docs/autonomy-guardrails.md +3 -2
  27. package/docs/beta1-stabilization-brief.md +165 -0
  28. package/docs/delegated-approval.md +4 -3
  29. package/docs/development-flow.md +6 -4
  30. package/docs/project-memory.md +7 -5
  31. package/docs/release.md +9 -2
  32. package/docs/split-required-recovery.md +53 -0
  33. package/docs/stable-release-defect-register.md +268 -0
  34. package/package.json +2 -2
  35. package/plugins/codex-workflow-gateway/references/protocol.md +6 -2
  36. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +9 -2
  37. package/schemas/remediation-mode-recovery-event.schema.json +42 -0
  38. package/schemas/task.schema.json +3 -1
@@ -1,11 +1,15 @@
1
- import { existsSync } from 'node:fs';
1
+ import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { WorkflowError } from '../errors.js';
4
4
  import { createUlid } from '../ulid.js';
5
- import { assertHash64 } from './store-sidecars.js';
5
+ import { assertHash64, canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
6
+ import { readTaskC1Posture } from './handoff.js';
6
7
  import { buildCurrentStrictStepReviewCycle, readReviewerAttestations, readStepReviewEvents, } from './review.js';
7
8
  export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
8
9
  export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
10
+ export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
11
+ export const REMEDIATION_MODE_RECOVERIES_SIDECAR = 'remediation-mode-recoveries.jsonl';
12
+ const REMEDIATION_MODE_RECOVERY_LOCK = '.remediation-mode-recovery.lock';
9
13
  const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
10
14
  const REMEDIATION_FAILURE_KINDS = new Set([
11
15
  'checks-failed',
@@ -65,15 +69,35 @@ export function assertCorrectiveAuditorIndependence(store, task, stepId, auditor
65
69
  function validateRemediationHistory(store, task) {
66
70
  const remediationEvents = readRawRemediationEvents(store, task);
67
71
  const correctiveDecisionEvents = readRawCorrectiveDecisionEvents(store, task);
72
+ const modeRecoveries = readRemediationModeRecoveryEvents(store, task);
68
73
  validateRemediationAttemptOrdinals(remediationEvents, task);
69
74
  validateRemediationReviewReferences(store, task, remediationEvents);
70
75
  validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
71
- validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisionEvents);
76
+ const recoveredEventIds = validateRemediationModeRecoveries(store, task, remediationEvents, correctiveDecisionEvents, modeRecoveries);
77
+ validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisionEvents, recoveredEventIds);
72
78
  return {
73
79
  remediationEvents,
74
80
  correctiveDecisionEvents,
75
81
  };
76
82
  }
83
+ export function readRemediationModeRecoveryEvents(store, task) {
84
+ const root = store.taskRoot(task.projectId, task.id);
85
+ const file = path.join(root, REMEDIATION_MODE_RECOVERIES_SIDECAR);
86
+ if (!existsSync(file))
87
+ return [];
88
+ return store.readSidecarEvents(root, REMEDIATION_MODE_RECOVERIES_SIDECAR, {
89
+ validateEvent: (event, context) => validateStoredRemediationModeRecovery(event, task, context.file, context.line),
90
+ });
91
+ }
92
+ function readRawCorrectiveDecisionRecoveries(store, task) {
93
+ const root = store.taskRoot(task.projectId, task.id);
94
+ const file = path.join(root, CORRECTIVE_DECISION_RECOVERIES_SIDECAR);
95
+ if (!existsSync(file))
96
+ return [];
97
+ return store.readSidecarEvents(root, CORRECTIVE_DECISION_RECOVERIES_SIDECAR, {
98
+ validateEvent: (event, context) => validateStoredCorrectiveDecisionRecoveryReference(event, task, context.file, context.line),
99
+ });
100
+ }
77
101
  function readRawRemediationEvents(store, task) {
78
102
  const sidecarFile = path.join(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR);
79
103
  if (!existsSync(sidecarFile))
@@ -91,6 +115,7 @@ function readRawCorrectiveDecisionEvents(store, task) {
91
115
  });
92
116
  }
93
117
  export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now) {
118
+ const recoveredDecisionEventId = findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId);
94
119
  const existing = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
95
120
  const matching = existing.filter((event) => remediationEventMatchesFailure(event, failureKind, failureEvidence));
96
121
  if (matching.length > 1) {
@@ -104,7 +129,9 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
104
129
  if (matching.length === 1) {
105
130
  const reused = matching[0];
106
131
  const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
107
- const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task));
132
+ const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task), {
133
+ recoveredDecisionEventId,
134
+ });
108
135
  if (reused.attemptOrdinal !== gate.nextAttemptOrdinal || reused.mode !== gate.mode || reused.guardedCategory !== gate.guardedCategory) {
109
136
  throw new WorkflowError('STATE_CORRUPT', 'Existing remediation event conflicts with the current guarded retry binding.', {
110
137
  taskId: task.id,
@@ -118,7 +145,9 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
118
145
  }
119
146
  return reused;
120
147
  }
121
- const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task));
148
+ const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task), {
149
+ recoveredDecisionEventId,
150
+ });
122
151
  return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, {
123
152
  eventId: `RME-${createUlid(now.getTime())}`,
124
153
  recordedAt: now.toISOString(),
@@ -133,6 +162,290 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
133
162
  validateEvent: (event, context) => validateStoredRemediationEvent(event, task, context.file, context.line),
134
163
  });
135
164
  }
165
+ function findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId) {
166
+ const remediations = readRawRemediationEvents(store, task).filter((event) => event.stepId === stepId);
167
+ if (remediations.length !== 2)
168
+ return null;
169
+ const decisions = readRawCorrectiveDecisionEvents(store, task).filter((event) => event.stepId === stepId
170
+ && event.triggeringAttemptCount === 3
171
+ && event.decision === 'continue-fix'
172
+ && event.planHash !== task.planHash);
173
+ if (decisions.length !== 1)
174
+ return null;
175
+ const recoveries = readRawCorrectiveDecisionRecoveries(store, task).filter((event) => event.stepId === stepId
176
+ && event.triggeringAttemptCount === 3
177
+ && event.correctiveDecisionEventId === decisions[0].eventId
178
+ && event.correctiveDecisionEventHash === decisions[0].eventHash
179
+ && event.reboundPlanHash === task.planHash);
180
+ if (recoveries.length > 1) {
181
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple corrective recoveries match one remediation attempt.', {
182
+ taskId: task.id,
183
+ stepId,
184
+ recoveryEventIds: recoveries.map((event) => event.eventId),
185
+ });
186
+ }
187
+ return recoveries[0]?.correctiveDecisionEventId ?? null;
188
+ }
189
+ export function findRemediationModeRecoveryCandidate(store, task) {
190
+ const raw = readRawRemediationEvents(store, task);
191
+ const candidates = raw.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
192
+ if (candidates.length === 0)
193
+ return null;
194
+ if (candidates.length !== 1) {
195
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple malformed remediation modes are not recoverable.', {
196
+ taskId: task.id,
197
+ eventIds: candidates.map((event) => event.eventId),
198
+ });
199
+ }
200
+ const posture = readTaskC1Posture(store, task);
201
+ const lease = readWriterLeaseForModeRecovery(store, task);
202
+ const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
203
+ return assessRemediationModeRecovery(store, task, candidates[0].stepId, actor, lease?.token ?? null);
204
+ }
205
+ export function assessRemediationModeRecovery(store, task, stepId, actor, writerToken) {
206
+ const normalizedActor = actor.trim();
207
+ if (!normalizedActor)
208
+ throw new WorkflowError('INVALID_ARGUMENT', 'Remediation mode recovery actor is required.');
209
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
210
+ if (!step)
211
+ throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
212
+ if (task.status !== 'needs_fix' || step.status !== 'failed') {
213
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the exact failed Task/Step posture.', {
214
+ taskId: task.id,
215
+ taskStatus: task.status,
216
+ stepId,
217
+ stepStatus: step.status,
218
+ });
219
+ }
220
+ if (!task.planHash)
221
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
222
+ const remediations = readRawRemediationEvents(store, task);
223
+ const decisions = readRawCorrectiveDecisionEvents(store, task);
224
+ validateRemediationAttemptOrdinals(remediations, task);
225
+ validateRemediationReviewReferences(store, task, remediations);
226
+ validateCorrectiveDecisionCoverage(task, decisions, remediations);
227
+ const malformed = remediations.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
228
+ if (malformed.length !== 1 || malformed[0].stepId !== stepId || malformed[0].attemptOrdinal !== 3) {
229
+ throw new WorkflowError('STATE_CORRUPT', 'State does not contain the one exact alpha.7.2 remediation mode omission.', {
230
+ taskId: task.id,
231
+ stepId,
232
+ malformedEventIds: malformed.map((event) => event.eventId),
233
+ });
234
+ }
235
+ const stepRemediations = remediations.filter((event) => event.stepId === stepId);
236
+ if (stepRemediations.length !== 3
237
+ || stepRemediations[0].mode !== 'ordinary'
238
+ || stepRemediations[1].mode !== 'ordinary'
239
+ || stepRemediations[2].eventId !== malformed[0].eventId) {
240
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires exactly two ordinary attempts and one malformed third attempt.', {
241
+ taskId: task.id,
242
+ stepId,
243
+ });
244
+ }
245
+ const decisionMatches = decisions.filter((event) => event.stepId === stepId
246
+ && event.triggeringAttemptCount === 3
247
+ && event.decision === 'continue-fix'
248
+ && sameStringArray(event.coveredEventIds, stepRemediations.slice(0, 2).map((event) => event.eventId)));
249
+ if (decisionMatches.length !== 1) {
250
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact continue-fix decision.', {
251
+ taskId: task.id,
252
+ stepId,
253
+ decisionEventIds: decisionMatches.map((event) => event.eventId),
254
+ });
255
+ }
256
+ const decision = decisionMatches[0];
257
+ const correctiveRecoveries = readRawCorrectiveDecisionRecoveries(store, task).filter((event) => event.stepId === stepId
258
+ && event.triggeringAttemptCount === 3
259
+ && event.correctiveDecisionEventId === decision.eventId
260
+ && event.correctiveDecisionEventHash === decision.eventHash
261
+ && event.reboundPlanHash === task.planHash);
262
+ if (correctiveRecoveries.length !== 1) {
263
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact corrective-decision recovery.', {
264
+ taskId: task.id,
265
+ stepId,
266
+ recoveryEventIds: correctiveRecoveries.map((event) => event.eventId),
267
+ });
268
+ }
269
+ const correctiveRecovery = correctiveRecoveries[0];
270
+ const authority = requireRemediationModeRecoveryAuthority(store, task, normalizedActor, writerToken);
271
+ const existing = readRemediationModeRecoveryEvents(store, task).filter((event) => event.remediationEventId === malformed[0].eventId);
272
+ if (existing.length > 1) {
273
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation mode recoveries target one event.', {
274
+ taskId: task.id,
275
+ remediationEventId: malformed[0].eventId,
276
+ });
277
+ }
278
+ if (existing[0]) {
279
+ validateRemediationModeRecoveryBinding(existing[0], malformed[0], decision, correctiveRecovery);
280
+ const retryMismatches = [
281
+ existing[0].taskRevision !== task.revision ? 'taskRevision' : null,
282
+ existing[0].planHash !== task.planHash ? 'planHash' : null,
283
+ existing[0].actor !== normalizedActor ? 'actor' : null,
284
+ existing[0].c1ClaimEventHash !== authority.c1ClaimEventHash ? 'c1ClaimEventHash' : null,
285
+ existing[0].writerLeaseTokenHash !== authority.writerLeaseTokenHash ? 'writerLeaseTokenHash' : null,
286
+ ].filter((value) => value !== null);
287
+ if (retryMismatches.length > 0) {
288
+ throw new WorkflowError('STATE_CONFLICT', 'Existing remediation mode recovery conflicts with the requested retry binding.', {
289
+ taskId: task.id,
290
+ stepId,
291
+ recoveryEventId: existing[0].eventId,
292
+ mismatches: retryMismatches,
293
+ });
294
+ }
295
+ }
296
+ const evidenceFingerprint = sha256Hex(canonicalJsonStringify({
297
+ taskId: task.id,
298
+ taskRevision: task.revision,
299
+ stepId,
300
+ planHash: task.planHash,
301
+ remediationEventHash: malformed[0].eventHash,
302
+ correctiveDecisionEventHash: decision.eventHash,
303
+ correctiveDecisionRecoveryEventHash: correctiveRecovery.eventHash,
304
+ actor: normalizedActor,
305
+ c1ClaimEventHash: authority.c1ClaimEventHash,
306
+ writerLeaseTokenHash: authority.writerLeaseTokenHash,
307
+ }));
308
+ return {
309
+ task,
310
+ remediation: malformed[0],
311
+ decision,
312
+ correctiveRecovery,
313
+ existingRecovery: existing[0] ?? null,
314
+ actor: normalizedActor,
315
+ c1ClaimEventHash: authority.c1ClaimEventHash,
316
+ writerLeaseTokenHash: authority.writerLeaseTokenHash,
317
+ evidenceFingerprint,
318
+ };
319
+ }
320
+ export function appendRemediationModeRecovery(store, task, stepId, expectedRevision, actor, writerToken, now) {
321
+ if (task.revision !== expectedRevision) {
322
+ throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
323
+ expectedRevision,
324
+ actualRevision: task.revision,
325
+ });
326
+ }
327
+ const preflight = assessRemediationModeRecovery(store, task, stepId, actor, writerToken);
328
+ const root = store.taskRoot(task.projectId, task.id);
329
+ const lockFile = path.join(root, REMEDIATION_MODE_RECOVERY_LOCK);
330
+ let descriptor;
331
+ try {
332
+ descriptor = openSync(lockFile, 'wx', 0o600);
333
+ closeSync(descriptor);
334
+ }
335
+ catch (error) {
336
+ if (existsSync(lockFile)) {
337
+ throw new WorkflowError('LOCKED', `Remediation mode recovery for ${task.id} is already being serialized.`, {
338
+ taskId: task.id,
339
+ stepId,
340
+ });
341
+ }
342
+ throw error;
343
+ }
344
+ try {
345
+ const currentTask = store.readTask(task.projectId, task.id);
346
+ if (currentTask.revision !== expectedRevision) {
347
+ throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
348
+ expectedRevision,
349
+ actualRevision: currentTask.revision,
350
+ });
351
+ }
352
+ let underLock;
353
+ try {
354
+ underLock = assessRemediationModeRecovery(store, currentTask, stepId, actor, writerToken);
355
+ }
356
+ catch (error) {
357
+ throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
358
+ taskId: task.id,
359
+ stepId,
360
+ cause: error instanceof Error ? error.message : String(error),
361
+ });
362
+ }
363
+ if (underLock.existingRecovery)
364
+ return underLock.existingRecovery;
365
+ if (underLock.evidenceFingerprint !== preflight.evidenceFingerprint) {
366
+ throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
367
+ taskId: task.id,
368
+ stepId,
369
+ });
370
+ }
371
+ return store.appendSidecarEvent(root, REMEDIATION_MODE_RECOVERIES_SIDECAR, {
372
+ eventId: `RMR-${createUlid(now.getTime())}`,
373
+ recordedAt: now.toISOString(),
374
+ taskId: task.id,
375
+ stepId,
376
+ taskRevision: expectedRevision,
377
+ remediationEventId: underLock.remediation.eventId,
378
+ remediationEventHash: underLock.remediation.eventHash,
379
+ correctiveDecisionEventId: underLock.decision.eventId,
380
+ correctiveDecisionEventHash: underLock.decision.eventHash,
381
+ correctiveDecisionRecoveryEventId: underLock.correctiveRecovery.eventId,
382
+ correctiveDecisionRecoveryEventHash: underLock.correctiveRecovery.eventHash,
383
+ attemptOrdinal: 3,
384
+ planHash: currentTask.planHash,
385
+ actor: underLock.actor,
386
+ c1ClaimEventHash: underLock.c1ClaimEventHash,
387
+ writerLeaseTokenHash: underLock.writerLeaseTokenHash,
388
+ fromMode: 'ordinary',
389
+ toMode: 'corrective',
390
+ reason: 'alpha7.2-recovered-continue-fix-mode-omission',
391
+ }, {
392
+ validateEvent: (event, context) => validateStoredRemediationModeRecovery(event, currentTask, context.file, context.line),
393
+ });
394
+ }
395
+ finally {
396
+ if (existsSync(lockFile))
397
+ unlinkSync(lockFile);
398
+ }
399
+ }
400
+ function requireRemediationModeRecoveryAuthority(store, task, actor, writerToken) {
401
+ const posture = readTaskC1Posture(store, task);
402
+ const lease = readWriterLeaseForModeRecovery(store, task);
403
+ if (posture.state === 'pending') {
404
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Pending C1 handoff must be claimed before remediation mode recovery.');
405
+ }
406
+ if (posture.state === 'claimed') {
407
+ const tokenHash = writerToken ? sha256Hex(writerToken) : null;
408
+ if (!lease
409
+ || actor !== posture.claimant
410
+ || lease.owner !== posture.claimant
411
+ || !writerToken
412
+ || lease.token !== writerToken
413
+ || tokenHash !== posture.writerLeaseTokenHash) {
414
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the claimed C1 actor and exact writer token.', {
415
+ taskId: task.id,
416
+ claimant: posture.claimant,
417
+ leaseOwner: lease?.owner ?? null,
418
+ });
419
+ }
420
+ return { c1ClaimEventHash: posture.latestEvent.eventHash, writerLeaseTokenHash: tokenHash };
421
+ }
422
+ if (lease && (lease.owner !== actor || !writerToken || lease.token !== writerToken)) {
423
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Existing writer lease is not owned by the remediation recovery actor/token.');
424
+ }
425
+ if (!lease && writerToken) {
426
+ throw new WorkflowError('TRANSITION_BLOCKED', 'A writer token was provided without an existing writer lease.');
427
+ }
428
+ return {
429
+ c1ClaimEventHash: posture.latestEvent?.eventHash ?? null,
430
+ writerLeaseTokenHash: lease ? sha256Hex(lease.token) : null,
431
+ };
432
+ }
433
+ function readWriterLeaseForModeRecovery(store, task) {
434
+ const file = path.join(store.lockRoot(task.projectId), `${task.id}.json`);
435
+ if (!existsSync(file))
436
+ return null;
437
+ let lease;
438
+ try {
439
+ lease = JSON.parse(readFileSync(file, 'utf8'));
440
+ }
441
+ catch {
442
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease is unreadable: ${file}`);
443
+ }
444
+ if (lease.entityId !== task.id || typeof lease.owner !== 'string' || typeof lease.token !== 'string') {
445
+ throw new WorkflowError('STATE_CORRUPT', `Writer lease does not match Task ${task.id}.`);
446
+ }
447
+ return lease;
448
+ }
136
449
  export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAudit, planRiskAudit, planHash, now) {
137
450
  validateCorrectiveDecisionAudit(correctiveAudit);
138
451
  const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
@@ -553,6 +866,142 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
553
866
  normalizeStringList(event.coveredEventIds, 'Corrective decision coveredEventIds');
554
867
  assertHash64(event.planHash, `Corrective decision planHash at ${file}:${line}`);
555
868
  }
869
+ function validateStoredCorrectiveDecisionRecoveryReference(event, task, file, line) {
870
+ if (!/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)
871
+ || event.taskId !== task.id
872
+ || event.triggeringAttemptCount !== 3) {
873
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery reference does not match the Task/ordinal.', {
874
+ file,
875
+ line,
876
+ taskId: task.id,
877
+ eventTaskId: event.taskId,
878
+ triggeringAttemptCount: event.triggeringAttemptCount,
879
+ });
880
+ }
881
+ if (typeof event.stepId !== 'string' || !event.stepId.trim() || typeof event.actor !== 'string' || !event.actor.trim()) {
882
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery reference is missing Step or actor.', { file, line });
883
+ }
884
+ for (const hash of [
885
+ event.correctiveDecisionEventHash,
886
+ event.decisionPlanHash,
887
+ event.reboundPlanHash,
888
+ event.semanticPlanHash,
889
+ event.fromKnowledgeMapHash,
890
+ event.toKnowledgeMapHash,
891
+ event.sourcePlanRiskAuditEventHash,
892
+ event.reboundPlanRiskAuditEventHash,
893
+ event.knowledgeMapApprovalFingerprint,
894
+ event.taskAuthorizationFingerprint,
895
+ event.delegationGrantFingerprint,
896
+ event.c1EvidenceFingerprint,
897
+ event.evidenceFingerprint,
898
+ ]) {
899
+ assertHash64(hash, `Corrective recovery hash at ${file}:${line}`);
900
+ }
901
+ if (!Number.isInteger(event.fromKnowledgeMapRevision)
902
+ || event.fromKnowledgeMapRevision < 1
903
+ || !Number.isInteger(event.toKnowledgeMapRevision)
904
+ || event.toKnowledgeMapRevision < 1) {
905
+ throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery Knowledge Map revisions are malformed.', { file, line });
906
+ }
907
+ }
908
+ function validateStoredRemediationModeRecovery(event, task, file, line) {
909
+ if (!/^RMR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)) {
910
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery eventId is malformed.', { file, line });
911
+ }
912
+ if (event.taskId !== task.id || typeof event.stepId !== 'string' || !event.stepId.trim()) {
913
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery does not match the current Task.', {
914
+ file,
915
+ line,
916
+ taskId: task.id,
917
+ eventTaskId: event.taskId,
918
+ });
919
+ }
920
+ if (!Number.isInteger(event.taskRevision) || event.taskRevision < 1 || event.attemptOrdinal !== 3) {
921
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery has an invalid revision or ordinal.', { file, line });
922
+ }
923
+ if (event.fromMode !== 'ordinary'
924
+ || event.toMode !== 'corrective'
925
+ || event.reason !== 'alpha7.2-recovered-continue-fix-mode-omission') {
926
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery has an unsupported correction.', { file, line });
927
+ }
928
+ if (!/^RME-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.remediationEventId)
929
+ || !/^CRD-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.correctiveDecisionEventId)
930
+ || !/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.correctiveDecisionRecoveryEventId)
931
+ || typeof event.actor !== 'string'
932
+ || !event.actor.trim()) {
933
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery identifiers are malformed.', { file, line });
934
+ }
935
+ for (const [label, value] of [
936
+ ['remediationEventHash', event.remediationEventHash],
937
+ ['correctiveDecisionEventHash', event.correctiveDecisionEventHash],
938
+ ['correctiveDecisionRecoveryEventHash', event.correctiveDecisionRecoveryEventHash],
939
+ ['planHash', event.planHash],
940
+ ]) {
941
+ assertHash64(value, `Remediation mode recovery ${label} at ${file}:${line}`);
942
+ }
943
+ if (event.c1ClaimEventHash !== null)
944
+ assertHash64(event.c1ClaimEventHash, `Remediation mode recovery C1 hash at ${file}:${line}`);
945
+ if (event.writerLeaseTokenHash !== null)
946
+ assertHash64(event.writerLeaseTokenHash, `Remediation mode recovery token hash at ${file}:${line}`);
947
+ }
948
+ function validateRemediationModeRecoveryBinding(recovery, remediation, decision, correctiveRecovery) {
949
+ const mismatches = [
950
+ recovery.taskId !== remediation.taskId ? 'taskId' : null,
951
+ recovery.stepId !== remediation.stepId ? 'stepId' : null,
952
+ recovery.remediationEventId !== remediation.eventId ? 'remediationEventId' : null,
953
+ recovery.remediationEventHash !== remediation.eventHash ? 'remediationEventHash' : null,
954
+ recovery.correctiveDecisionEventId !== decision.eventId ? 'correctiveDecisionEventId' : null,
955
+ recovery.correctiveDecisionEventHash !== decision.eventHash ? 'correctiveDecisionEventHash' : null,
956
+ recovery.correctiveDecisionRecoveryEventId !== correctiveRecovery.eventId ? 'correctiveDecisionRecoveryEventId' : null,
957
+ recovery.correctiveDecisionRecoveryEventHash !== correctiveRecovery.eventHash ? 'correctiveDecisionRecoveryEventHash' : null,
958
+ recovery.attemptOrdinal !== remediation.attemptOrdinal ? 'attemptOrdinal' : null,
959
+ recovery.planHash !== correctiveRecovery.reboundPlanHash ? 'planHash' : null,
960
+ recovery.actor !== correctiveRecovery.actor ? 'actor' : null,
961
+ ].filter((value) => value !== null);
962
+ if (mismatches.length > 0) {
963
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery does not match its append-only evidence.', {
964
+ recoveryEventId: recovery.eventId,
965
+ mismatches,
966
+ });
967
+ }
968
+ }
969
+ function validateRemediationModeRecoveries(store, task, remediations, decisions, recoveries) {
970
+ const correctiveRecoveries = readRawCorrectiveDecisionRecoveries(store, task);
971
+ const recoveredEventIds = new Set();
972
+ for (const recovery of recoveries) {
973
+ if (recoveredEventIds.has(recovery.remediationEventId)) {
974
+ throw new WorkflowError('STATE_CORRUPT', 'Duplicate remediation mode recoveries target one event.', {
975
+ taskId: task.id,
976
+ remediationEventId: recovery.remediationEventId,
977
+ });
978
+ }
979
+ const remediation = remediations.find((event) => event.eventId === recovery.remediationEventId) ?? null;
980
+ const decision = decisions.find((event) => event.eventId === recovery.correctiveDecisionEventId) ?? null;
981
+ const correctiveRecovery = correctiveRecoveries.find((event) => event.eventId === recovery.correctiveDecisionRecoveryEventId) ?? null;
982
+ if (!remediation || !decision || !correctiveRecovery) {
983
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery references missing evidence.', {
984
+ taskId: task.id,
985
+ recoveryEventId: recovery.eventId,
986
+ });
987
+ }
988
+ if (recovery.taskRevision !== task.revision
989
+ || recovery.planHash !== task.planHash
990
+ || remediation.mode !== 'ordinary'
991
+ || remediation.attemptOrdinal !== 3
992
+ || decision.decision !== 'continue-fix'
993
+ || decision.triggeringAttemptCount !== 3
994
+ || correctiveRecovery.correctiveDecisionEventId !== decision.eventId) {
995
+ throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery references an ineligible lifecycle shape.', {
996
+ taskId: task.id,
997
+ recoveryEventId: recovery.eventId,
998
+ });
999
+ }
1000
+ validateRemediationModeRecoveryBinding(recovery, remediation, decision, correctiveRecovery);
1001
+ recoveredEventIds.add(remediation.eventId);
1002
+ }
1003
+ return recoveredEventIds;
1004
+ }
556
1005
  function validateRemediationAttemptOrdinals(events, task) {
557
1006
  const attemptsByStep = new Map();
558
1007
  for (const event of events) {
@@ -712,7 +1161,7 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
712
1161
  }
713
1162
  }
714
1163
  }
715
- function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions) {
1164
+ function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds) {
716
1165
  for (const event of remediationEvents) {
717
1166
  if (event.attemptOrdinal <= 2) {
718
1167
  if (event.mode !== 'ordinary') {
@@ -726,7 +1175,7 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
726
1175
  }
727
1176
  continue;
728
1177
  }
729
- if (event.mode === 'ordinary') {
1178
+ if (event.mode === 'ordinary' && !recoveredEventIds.has(event.eventId)) {
730
1179
  throw new WorkflowError('STATE_CORRUPT', 'Guarded remediation attempt ordinals above 2 must not remain ordinary.', {
731
1180
  taskId: task.id,
732
1181
  stepId,
@@ -753,13 +1202,13 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
753
1202
  }
754
1203
  }
755
1204
  }
756
- function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions) {
1205
+ function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds) {
757
1206
  const stepIds = new Set(remediationEvents.map((event) => event.stepId));
758
1207
  for (const stepId of stepIds) {
759
1208
  const stepEvents = remediationEvents
760
1209
  .filter((event) => event.stepId === stepId)
761
1210
  .sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
762
- validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions);
1211
+ validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds);
763
1212
  }
764
1213
  }
765
1214
  function validateCorrectiveDecisionAudit(audit) {