codex-workflow-v2 2.0.0-beta.12 → 2.0.0-beta.12.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 (30) hide show
  1. package/README.md +4 -3
  2. package/dist/src/alpha6/remediation.d.ts +6 -1
  3. package/dist/src/alpha6/remediation.js +356 -7
  4. package/dist/src/alpha6/remediation.js.map +1 -1
  5. package/dist/src/cli.js +18 -0
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/contracts.d.ts +42 -0
  8. package/dist/src/dependency-provenance.js +5 -3
  9. package/dist/src/dependency-provenance.js.map +1 -1
  10. package/dist/src/version.d.ts +1 -1
  11. package/dist/src/version.js +1 -1
  12. package/dist/src/version.js.map +1 -1
  13. package/dist/src/workflow.d.ts +4 -1
  14. package/dist/src/workflow.js +113 -13
  15. package/dist/src/workflow.js.map +1 -1
  16. package/docs/autonomy-guardrails.md +22 -1
  17. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  18. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  19. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  20. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +16 -5
  21. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +10 -8
  22. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +25 -11
  23. package/docs/release.md +6 -1
  24. package/docs/updating-existing-project.md +25 -0
  25. package/docs/validation-report.md +82 -81
  26. package/package.json +1 -1
  27. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
  28. package/plugins/codex-workflow-gateway/references/protocol.md +15 -1
  29. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +17 -0
  30. package/schemas/stop-escalate-override-event.schema.json +45 -0
package/README.md CHANGED
@@ -43,7 +43,7 @@ cross-machine synchronization are not part of its contract.
43
43
  Node.js 22 or newer is required. Consumers pin the exact package version:
44
44
 
45
45
  ```bash
46
- npm install --save-dev --save-exact codex-workflow-v2@2.0.0-beta.12
46
+ npm install --save-dev --save-exact codex-workflow-v2@2.0.0-beta.12.1
47
47
  npx codex-workflow gateway handshake --repo .
48
48
  npx codex-workflow doctor --repo .
49
49
  ```
@@ -302,9 +302,10 @@ alpha regressions that must not recur are tracked in the
302
302
  - [Chat-only development guide](docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf)
303
303
  - [Deep technical reference](docs/pdf/codex-workflow-v2-technical-reference-ru.pdf)
304
304
 
305
- These beta.12 documents cover delegated Discovery, Milestone Autonomy Contract issuance,
305
+ These beta.12.1 documents cover delegated Discovery, Milestone Autonomy Contract issuance,
306
306
  dependency-aware Task routing, C1 credential handling, external-sealed reviews, bounded
307
- Plan-integrity recovery, and the P04-A structural-replacement stop boundary. Their reviewable
307
+ Plan-integrity recovery, Task-local dependency provenance recovery, the human-confirmed legacy
308
+ attempt-four stop override, and the P04-A structural-replacement stop boundary. Their reviewable
308
309
  sources live in [`docs/pdf/sources`](docs/pdf/sources), and `npm run docs:pdf:check` proves the
309
310
  tracked binaries were generated from the current source and package version. The PDFs remain
310
311
  human-facing artifacts; the Markdown documents above, bundled gateway, schemas, and runtime code
@@ -1,10 +1,11 @@
1
- import type { CorrectiveDecisionEvent, CorrectiveDecisionRecoveryEvent, CorrectivePlanAuditInput, GuardedRiskCategory, PlanIntegrityConflictEvidence, RemediationEvent, RemediationFailureEvidence, RemediationFailureKind, RemediationMode, RemediationModeRecoveryEvent, TaskState } from '../contracts.js';
1
+ import type { CorrectiveDecisionEvent, CorrectiveDecisionRecoveryEvent, CorrectivePlanAuditInput, GuardedRiskCategory, PlanIntegrityConflictEvidence, RemediationEvent, RemediationFailureEvidence, RemediationFailureKind, RemediationMode, RemediationModeRecoveryEvent, StopEscalateOverrideEvent, StopEscalateOverridePreparation, TaskState } from '../contracts.js';
2
2
  import type { FileStateStore } from '../state/store.js';
3
3
  import type { CurrentPlanRiskAudit } from './plan-risk.js';
4
4
  export declare const REMEDIATION_EVENTS_SIDECAR = "remediation-events.jsonl";
5
5
  export declare const CORRECTIVE_DECISIONS_SIDECAR = "corrective-decisions.jsonl";
6
6
  export declare const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = "corrective-decision-recoveries.jsonl";
7
7
  export declare const REMEDIATION_MODE_RECOVERIES_SIDECAR = "remediation-mode-recoveries.jsonl";
8
+ export declare const STOP_ESCALATE_OVERRIDES_SIDECAR = "stop-escalate-overrides.jsonl";
8
9
  export interface GuardedRemediationAttemptGate {
9
10
  stepId: string;
10
11
  guardedCategory: GuardedRiskCategory;
@@ -30,11 +31,15 @@ export declare function readCorrectiveDecisionEvents(store: FileStateStore, task
30
31
  export declare function defaultCorrectiveAuditorActor(taskId: string): string;
31
32
  export declare function assertCorrectiveAuditorIndependence(store: FileStateStore, task: TaskState, stepId: string, auditor: string): void;
32
33
  export declare function readRemediationModeRecoveryEvents(store: FileStateStore, task: TaskState): RemediationModeRecoveryEvent[];
34
+ export declare function readStopEscalateOverrideEvents(store: FileStateStore, task: TaskState): StopEscalateOverrideEvent[];
35
+ export declare function stopEscalateOverrideDecisionEventIds(store: FileStateStore, task: TaskState): ReadonlySet<string>;
33
36
  export declare function appendRemediationEvent(store: FileStateStore, task: TaskState, stepId: string, failureKind: RemediationFailureKind, failureEvidence: RemediationFailureEvidence, planRiskAudit: CurrentPlanRiskAudit, now: Date): RemediationEvent;
34
37
  export declare function findRemediationModeRecoveryCandidate(store: FileStateStore, task: TaskState): RemediationModeRecoveryAssessment | null;
35
38
  export declare function assessRemediationModeRecovery(store: FileStateStore, task: TaskState, stepId: string, actor: string, writerToken: string | null): RemediationModeRecoveryAssessment;
36
39
  export declare function appendRemediationModeRecovery(store: FileStateStore, task: TaskState, stepId: string, expectedRevision: number, actor: string, writerToken: string | null, now: Date): RemediationModeRecoveryEvent;
37
40
  export declare function appendCorrectiveDecisionEvent(store: FileStateStore, task: TaskState, stepId: string, correctiveAudit: CorrectivePlanAuditInput, planRiskAudit: CurrentPlanRiskAudit, planHash: string, now: Date): CorrectiveDecisionEvent;
41
+ export declare function prepareStopEscalateOverride(store: FileStateStore, task: TaskState, stepId: string, expectedRevision: number, actor: string, reason: string, headCommit: string): StopEscalateOverridePreparation;
42
+ export declare function appendStopEscalateOverride(store: FileStateStore, task: TaskState, stepId: string, expectedRevision: number, actor: string, reason: string, headCommit: string, confirmationCode: string, now: Date): StopEscalateOverrideEvent;
38
43
  export declare function appendPlanIntegrityRecoveryDecision(store: FileStateStore, task: TaskState, stepId: string, auditor: string, evidence: PlanIntegrityConflictEvidence, now: Date): CorrectiveDecisionEvent;
39
44
  export declare function assertGuardedRemediationAttemptAllowed(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit, recoveredDecisionEventId?: string | null): GuardedRemediationAttemptGate;
40
45
  export declare function readGuardedRemediationPostureForStep(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit): GuardedRemediationAttemptGate | null;
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { WorkflowError } from '../errors.js';
4
4
  import { hashCanonical } from '../lifecycle/fingerprint.js';
5
5
  import { createUlid } from '../ulid.js';
6
+ import { PACKAGE_VERSION } from '../version.js';
6
7
  import { assertHash64, canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
7
8
  import { readTaskC1Posture } from './handoff.js';
8
9
  import { buildCurrentStrictStepReviewCycle, latestPassedVerifiedStepReviewEvent, readReviewerAttestations, readStepReviewEvents, } from './review.js';
@@ -10,7 +11,10 @@ export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
10
11
  export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
11
12
  export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
12
13
  export const REMEDIATION_MODE_RECOVERIES_SIDECAR = 'remediation-mode-recoveries.jsonl';
14
+ export const STOP_ESCALATE_OVERRIDES_SIDECAR = 'stop-escalate-overrides.jsonl';
13
15
  const REMEDIATION_MODE_RECOVERY_LOCK = '.remediation-mode-recovery.lock';
16
+ const STOP_ESCALATE_OVERRIDE_LOCK = '.stop-escalate-override.lock';
17
+ const LEGACY_STOP_OVERRIDE_POLICY = 'beta11-attempt4-stop-compatibility-v1';
14
18
  const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
15
19
  const REMEDIATION_FAILURE_KINDS = new Set([
16
20
  'checks-failed',
@@ -71,11 +75,12 @@ function validateRemediationHistory(store, task) {
71
75
  const remediationEvents = readRawRemediationEvents(store, task);
72
76
  const correctiveDecisionEvents = readRawCorrectiveDecisionEvents(store, task);
73
77
  const modeRecoveries = readRemediationModeRecoveryEvents(store, task);
78
+ const overriddenStopDecisionEventIds = validateStopEscalateOverrideHistory(task, remediationEvents, correctiveDecisionEvents, readRawStopEscalateOverrideEvents(store, task));
74
79
  validateRemediationAttemptOrdinals(remediationEvents, task);
75
80
  validateRemediationReviewReferences(store, task, remediationEvents);
76
81
  validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
77
82
  const recoveredEventIds = validateRemediationModeRecoveries(store, task, remediationEvents, correctiveDecisionEvents, modeRecoveries);
78
- validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisionEvents, recoveredEventIds);
83
+ validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisionEvents, recoveredEventIds, overriddenStopDecisionEventIds);
79
84
  return {
80
85
  remediationEvents,
81
86
  correctiveDecisionEvents,
@@ -115,6 +120,32 @@ function readRawCorrectiveDecisionEvents(store, task) {
115
120
  validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
116
121
  });
117
122
  }
123
+ function readRawStopEscalateOverrideEvents(store, task) {
124
+ const root = store.taskRoot(task.projectId, task.id);
125
+ const sidecarFile = path.join(root, STOP_ESCALATE_OVERRIDES_SIDECAR);
126
+ if (!existsSync(sidecarFile))
127
+ return [];
128
+ return store.readSidecarEvents(root, STOP_ESCALATE_OVERRIDES_SIDECAR, {
129
+ validateEvent: (event, context) => validateStoredStopEscalateOverride(event, task, context.file, context.line),
130
+ });
131
+ }
132
+ export function readStopEscalateOverrideEvents(store, task) {
133
+ const remediations = readRawRemediationEvents(store, task);
134
+ const decisions = readRawCorrectiveDecisionEvents(store, task);
135
+ const overrides = readRawStopEscalateOverrideEvents(store, task);
136
+ validateRemediationAttemptOrdinals(remediations, task);
137
+ validateCorrectiveDecisionCoverage(task, decisions, remediations);
138
+ validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
139
+ return overrides;
140
+ }
141
+ export function stopEscalateOverrideDecisionEventIds(store, task) {
142
+ const remediations = readRawRemediationEvents(store, task);
143
+ const decisions = readRawCorrectiveDecisionEvents(store, task);
144
+ const overrides = readRawStopEscalateOverrideEvents(store, task);
145
+ validateRemediationAttemptOrdinals(remediations, task);
146
+ validateCorrectiveDecisionCoverage(task, decisions, remediations);
147
+ return validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
148
+ }
118
149
  export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now) {
119
150
  const recoveredDecisionEventId = findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId);
120
151
  const existing = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
@@ -132,6 +163,7 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
132
163
  const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
133
164
  const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task), {
134
165
  recoveredDecisionEventId,
166
+ overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
135
167
  });
136
168
  if (reused.attemptOrdinal !== gate.nextAttemptOrdinal || reused.mode !== gate.mode || reused.guardedCategory !== gate.guardedCategory) {
137
169
  throw new WorkflowError('STATE_CORRUPT', 'Existing remediation event conflicts with the current guarded retry binding.', {
@@ -148,6 +180,7 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
148
180
  }
149
181
  const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task), {
150
182
  recoveredDecisionEventId,
183
+ overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
151
184
  });
152
185
  return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, {
153
186
  eventId: `RME-${createUlid(now.getTime())}`,
@@ -513,6 +546,176 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
513
546
  validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
514
547
  });
515
548
  }
549
+ export function prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit) {
550
+ if (task.revision !== expectedRevision) {
551
+ throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
552
+ expectedRevision,
553
+ actualRevision: task.revision,
554
+ });
555
+ }
556
+ const normalizedActor = actor.trim();
557
+ const normalizedReason = reason.trim();
558
+ if (!normalizedActor)
559
+ throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a human actor.');
560
+ if (!normalizedReason)
561
+ throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a reason.');
562
+ if (!COMMIT_SHA_PATTERN.test(headCommit)) {
563
+ throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires the exact current Git HEAD.');
564
+ }
565
+ if (task.status !== 'needs_fix') {
566
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a needs_fix Task.', {
567
+ taskId: task.id,
568
+ taskStatus: task.status,
569
+ });
570
+ }
571
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
572
+ if (!step)
573
+ throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
574
+ if (step.status !== 'failed') {
575
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires the exact failed Step.', {
576
+ taskId: task.id,
577
+ stepId,
578
+ stepStatus: step.status,
579
+ });
580
+ }
581
+ if (!task.planHash)
582
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
583
+ const history = readOpenGuardedRemediationHistory(store, task, stepId);
584
+ const remediations = history.remediationEvents;
585
+ if (remediations.length !== 3
586
+ || remediations[0]?.attemptOrdinal !== 1
587
+ || remediations[0]?.mode !== 'ordinary'
588
+ || remediations[1]?.attemptOrdinal !== 2
589
+ || remediations[1]?.mode !== 'ordinary'
590
+ || remediations[2]?.attemptOrdinal !== 3
591
+ || remediations[2]?.mode !== 'corrective') {
592
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override is limited to the exact beta.11 three-failure history.', {
593
+ taskId: task.id,
594
+ stepId,
595
+ attempts: remediations.map((event) => ({ eventId: event.eventId, ordinal: event.attemptOrdinal, mode: event.mode })),
596
+ });
597
+ }
598
+ const priorContinueMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 3
599
+ && event.decision === 'continue-fix'
600
+ && event.planHash === task.planHash
601
+ && sameStringArray(event.coveredEventIds, remediations.slice(0, 2).map((candidate) => candidate.eventId)));
602
+ const stopMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 4
603
+ && event.decision === 'stop-escalate'
604
+ && event.planHash === task.planHash
605
+ && sameStringArray(event.coveredEventIds, remediations.map((candidate) => candidate.eventId)));
606
+ if (priorContinueMatches.length !== 1 || stopMatches.length !== 1) {
607
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires one exact attempt-3 continue decision and one exact attempt-4 stop decision.', {
608
+ taskId: task.id,
609
+ stepId,
610
+ priorContinueDecisionEventIds: priorContinueMatches.map((event) => event.eventId),
611
+ stopDecisionEventIds: stopMatches.map((event) => event.eventId),
612
+ });
613
+ }
614
+ const priorContinue = priorContinueMatches[0];
615
+ const stopDecision = stopMatches[0];
616
+ const binding = {
617
+ taskId: task.id,
618
+ taskRevision: task.revision,
619
+ stepId,
620
+ stopDecisionEventId: stopDecision.eventId,
621
+ stopDecisionEventHash: stopDecision.eventHash,
622
+ priorContinueDecisionEventId: priorContinue.eventId,
623
+ priorContinueDecisionEventHash: priorContinue.eventHash,
624
+ triggeringAttemptCount: 4,
625
+ coveredEventIds: remediations.map((event) => event.eventId),
626
+ planHash: task.planHash,
627
+ headCommit,
628
+ actor: normalizedActor,
629
+ reason: normalizedReason,
630
+ packageVersion: PACKAGE_VERSION,
631
+ policy: LEGACY_STOP_OVERRIDE_POLICY,
632
+ };
633
+ const confirmationCodeBindingHash = stopEscalateOverrideBindingHash(binding);
634
+ const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === stopDecision.eventId) ?? null;
635
+ if (existing && existing.confirmationCodeBindingHash !== confirmationCodeBindingHash) {
636
+ throw new WorkflowError('STATE_CONFLICT', 'The stop decision already has an override with a different human binding.', {
637
+ taskId: task.id,
638
+ stepId,
639
+ overrideEventId: existing.eventId,
640
+ });
641
+ }
642
+ return {
643
+ eligible: true,
644
+ action: 'task stop-override-apply',
645
+ ...binding,
646
+ confirmationCodeBindingHash,
647
+ confirmationCode: `SOO-${confirmationCodeBindingHash.slice(0, 24).toUpperCase()}`,
648
+ appendOnly: true,
649
+ preservesOriginalDecision: true,
650
+ delegatedApprovalAllowed: false,
651
+ };
652
+ }
653
+ export function appendStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit, confirmationCode, now) {
654
+ const prepared = prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit);
655
+ if (confirmationCode.trim() !== prepared.confirmationCode) {
656
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override confirmation code does not match the current state binding.', {
657
+ taskId: task.id,
658
+ stepId,
659
+ confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
660
+ });
661
+ }
662
+ const root = store.taskRoot(task.projectId, task.id);
663
+ const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === prepared.stopDecisionEventId) ?? null;
664
+ if (existing)
665
+ return existing;
666
+ const lockFile = path.join(root, STOP_ESCALATE_OVERRIDE_LOCK);
667
+ let descriptor;
668
+ try {
669
+ descriptor = openSync(lockFile, 'wx', 0o600);
670
+ closeSync(descriptor);
671
+ }
672
+ catch (error) {
673
+ if (existsSync(lockFile)) {
674
+ throw new WorkflowError('LOCKED', `Stop override for ${task.id} is already being serialized.`, {
675
+ taskId: task.id,
676
+ stepId,
677
+ });
678
+ }
679
+ throw error;
680
+ }
681
+ try {
682
+ const currentTask = store.readTask(task.projectId, task.id);
683
+ const underLock = prepareStopEscalateOverride(store, currentTask, stepId, expectedRevision, actor, reason, headCommit);
684
+ if (underLock.confirmationCode !== prepared.confirmationCode
685
+ || underLock.confirmationCodeBindingHash !== prepared.confirmationCodeBindingHash) {
686
+ throw new WorkflowError('STATE_CONFLICT', 'Stop override evidence changed after preparation.', {
687
+ taskId: task.id,
688
+ stepId,
689
+ });
690
+ }
691
+ return store.appendSidecarEvent(root, STOP_ESCALATE_OVERRIDES_SIDECAR, {
692
+ eventId: `SOO-${createUlid(now.getTime())}`,
693
+ recordedAt: now.toISOString(),
694
+ taskId: underLock.taskId,
695
+ taskRevision: underLock.taskRevision,
696
+ stepId: underLock.stepId,
697
+ stopDecisionEventId: underLock.stopDecisionEventId,
698
+ stopDecisionEventHash: underLock.stopDecisionEventHash,
699
+ priorContinueDecisionEventId: underLock.priorContinueDecisionEventId,
700
+ priorContinueDecisionEventHash: underLock.priorContinueDecisionEventHash,
701
+ triggeringAttemptCount: 4,
702
+ coveredEventIds: underLock.coveredEventIds,
703
+ planHash: underLock.planHash,
704
+ headCommit: underLock.headCommit,
705
+ actor: underLock.actor,
706
+ reason: underLock.reason,
707
+ packageVersion: underLock.packageVersion,
708
+ confirmationCodeBindingHash: underLock.confirmationCodeBindingHash,
709
+ policy: LEGACY_STOP_OVERRIDE_POLICY,
710
+ }, {
711
+ validateEvent: (event, context) => validateStoredStopEscalateOverride(event, currentTask, context.file, context.line),
712
+ });
713
+ }
714
+ finally {
715
+ if (existsSync(lockFile))
716
+ unlinkSync(lockFile);
717
+ }
718
+ }
516
719
  export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor, evidence, now) {
517
720
  if (!task.planHash) {
518
721
  throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no Plan hash for Plan-integrity recovery.`);
@@ -577,6 +780,7 @@ export function assertGuardedRemediationAttemptAllowed(store, task, stepId, plan
577
780
  return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
578
781
  enforceRuntimeGate: true,
579
782
  recoveredDecisionEventId,
783
+ overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
580
784
  });
581
785
  }
582
786
  export function readGuardedRemediationPostureForStep(store, task, stepId, planRiskAudit) {
@@ -597,14 +801,18 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
597
801
  if (remediationEvents.length < 2 && earlyPlanIntegrityDecision?.recoveryBasis !== 'plan-integrity')
598
802
  return null;
599
803
  if (step.status === 'failed' || step.status === 'planned') {
600
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions);
804
+ return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
805
+ overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
806
+ });
601
807
  }
602
808
  if (task.status !== 'blocked' || step.status !== 'in_progress' || !step.evidence)
603
809
  return null;
604
810
  const currentCycle = buildCurrentStrictStepReviewCycle(store, task, stepId);
605
811
  if (currentCycle.resolution !== 'unverified')
606
812
  return null;
607
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions);
813
+ return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
814
+ overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
815
+ });
608
816
  }
609
817
  function readOpenGuardedRemediationHistory(store, task, stepId) {
610
818
  const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
@@ -725,6 +933,18 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
725
933
  posture: 'decision-required',
726
934
  };
727
935
  }
936
+ if (correctiveDecision.decision === 'stop-escalate'
937
+ && options.overriddenStopDecisionEventIds?.has(correctiveDecision.eventId)) {
938
+ return {
939
+ stepId,
940
+ guardedCategory,
941
+ attemptCount,
942
+ nextAttemptOrdinal,
943
+ mode: 'corrective',
944
+ correctiveDecision,
945
+ posture: 'continue-fix',
946
+ };
947
+ }
728
948
  if (correctiveDecision.decision !== 'continue-fix' && options.enforceRuntimeGate) {
729
949
  throw correctiveDecisionBlockedError(task, stepId, attemptCount, correctiveDecision);
730
950
  }
@@ -916,6 +1136,133 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
916
1136
  });
917
1137
  }
918
1138
  }
1139
+ function validateStoredStopEscalateOverride(event, task, file, line) {
1140
+ if (!/^SOO-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)) {
1141
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override eventId is malformed.', { file, line });
1142
+ }
1143
+ if (event.taskId !== task.id
1144
+ || !Number.isInteger(event.taskRevision)
1145
+ || event.taskRevision < 1
1146
+ || event.taskRevision > task.revision
1147
+ || typeof event.stepId !== 'string'
1148
+ || !event.stepId.trim()
1149
+ || event.triggeringAttemptCount !== 4
1150
+ || event.policy !== LEGACY_STOP_OVERRIDE_POLICY) {
1151
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override does not match its Task or compatibility policy.', {
1152
+ file,
1153
+ line,
1154
+ taskId: task.id,
1155
+ eventTaskId: event.taskId,
1156
+ });
1157
+ }
1158
+ if (!/^CRD-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.stopDecisionEventId)
1159
+ || !/^CRD-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.priorContinueDecisionEventId)
1160
+ || !COMMIT_SHA_PATTERN.test(event.headCommit)
1161
+ || typeof event.actor !== 'string'
1162
+ || !event.actor.trim()
1163
+ || typeof event.reason !== 'string'
1164
+ || !event.reason.trim()
1165
+ || typeof event.packageVersion !== 'string'
1166
+ || !event.packageVersion.trim()) {
1167
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override identifiers or human evidence are malformed.', {
1168
+ file,
1169
+ line,
1170
+ taskId: task.id,
1171
+ });
1172
+ }
1173
+ const coveredEventIds = normalizeStringList(event.coveredEventIds, 'Stop override coveredEventIds');
1174
+ if (coveredEventIds.length !== 3 || new Set(coveredEventIds).size !== coveredEventIds.length) {
1175
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override must bind exactly three distinct remediation events.', {
1176
+ file,
1177
+ line,
1178
+ taskId: task.id,
1179
+ stepId: event.stepId,
1180
+ });
1181
+ }
1182
+ for (const [label, value] of [
1183
+ ['stopDecisionEventHash', event.stopDecisionEventHash],
1184
+ ['priorContinueDecisionEventHash', event.priorContinueDecisionEventHash],
1185
+ ['planHash', event.planHash],
1186
+ ['confirmationCodeBindingHash', event.confirmationCodeBindingHash],
1187
+ ]) {
1188
+ assertHash64(value, `Stop override ${label} at ${file}:${line}`);
1189
+ }
1190
+ const expectedBindingHash = stopEscalateOverrideBindingHash(event);
1191
+ if (event.confirmationCodeBindingHash !== expectedBindingHash) {
1192
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override confirmation binding hash is invalid.', {
1193
+ file,
1194
+ line,
1195
+ taskId: task.id,
1196
+ expectedBindingHash,
1197
+ actualBindingHash: event.confirmationCodeBindingHash,
1198
+ });
1199
+ }
1200
+ }
1201
+ function validateStopEscalateOverrideHistory(task, remediations, decisions, overrides) {
1202
+ const overriddenDecisionIds = new Set();
1203
+ for (const override of overrides) {
1204
+ if (overriddenDecisionIds.has(override.stopDecisionEventId)) {
1205
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple stop overrides target one corrective decision.', {
1206
+ taskId: task.id,
1207
+ stepId: override.stepId,
1208
+ stopDecisionEventId: override.stopDecisionEventId,
1209
+ });
1210
+ }
1211
+ const stepRemediations = remediations
1212
+ .filter((event) => event.stepId === override.stepId && event.attemptOrdinal <= 3)
1213
+ .sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
1214
+ const stopDecision = decisions.find((event) => event.eventId === override.stopDecisionEventId) ?? null;
1215
+ const priorContinue = decisions.find((event) => event.eventId === override.priorContinueDecisionEventId) ?? null;
1216
+ if (stepRemediations.length !== 3
1217
+ || stepRemediations[0]?.attemptOrdinal !== 1
1218
+ || stepRemediations[0]?.mode !== 'ordinary'
1219
+ || stepRemediations[1]?.attemptOrdinal !== 2
1220
+ || stepRemediations[1]?.mode !== 'ordinary'
1221
+ || stepRemediations[2]?.attemptOrdinal !== 3
1222
+ || stepRemediations[2]?.mode !== 'corrective'
1223
+ || !stopDecision
1224
+ || stopDecision.eventHash !== override.stopDecisionEventHash
1225
+ || stopDecision.decision !== 'stop-escalate'
1226
+ || stopDecision.triggeringAttemptCount !== 4
1227
+ || stopDecision.planHash !== override.planHash
1228
+ || !priorContinue
1229
+ || priorContinue.eventHash !== override.priorContinueDecisionEventHash
1230
+ || priorContinue.decision !== 'continue-fix'
1231
+ || priorContinue.triggeringAttemptCount !== 3
1232
+ || priorContinue.planHash !== override.planHash
1233
+ || !sameStringArray(override.coveredEventIds, stepRemediations.map((event) => event.eventId))
1234
+ || !sameStringArray(stopDecision.coveredEventIds, override.coveredEventIds)
1235
+ || !sameStringArray(priorContinue.coveredEventIds, override.coveredEventIds.slice(0, 2))) {
1236
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override does not bind the exact beta.11 attempt-4 stop history.', {
1237
+ taskId: task.id,
1238
+ stepId: override.stepId,
1239
+ overrideEventId: override.eventId,
1240
+ });
1241
+ }
1242
+ overriddenDecisionIds.add(override.stopDecisionEventId);
1243
+ }
1244
+ return overriddenDecisionIds;
1245
+ }
1246
+ function stopEscalateOverrideBindingHash(input) {
1247
+ return hashCanonical({
1248
+ domain: 'codex-workflow/stop-escalate-override/v1',
1249
+ taskId: input.taskId,
1250
+ taskRevision: input.taskRevision,
1251
+ stepId: input.stepId,
1252
+ stopDecisionEventId: input.stopDecisionEventId,
1253
+ stopDecisionEventHash: input.stopDecisionEventHash,
1254
+ priorContinueDecisionEventId: input.priorContinueDecisionEventId,
1255
+ priorContinueDecisionEventHash: input.priorContinueDecisionEventHash,
1256
+ triggeringAttemptCount: input.triggeringAttemptCount,
1257
+ coveredEventIds: [...input.coveredEventIds],
1258
+ planHash: input.planHash,
1259
+ headCommit: input.headCommit,
1260
+ actor: input.actor,
1261
+ reason: input.reason,
1262
+ packageVersion: input.packageVersion,
1263
+ policy: input.policy,
1264
+ });
1265
+ }
919
1266
  function validatePlanIntegrityDecision(event, task, file, line) {
920
1267
  const evidence = event.planIntegrityEvidence;
921
1268
  if (event.recoveryBasis !== 'plan-integrity'
@@ -1286,7 +1633,7 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
1286
1633
  }
1287
1634
  }
1288
1635
  }
1289
- function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds) {
1636
+ function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
1290
1637
  for (const event of remediationEvents) {
1291
1638
  if (event.attemptOrdinal <= 2) {
1292
1639
  if (event.mode !== 'ordinary') {
@@ -1314,7 +1661,9 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
1314
1661
  const matching = correctiveDecisions.filter((decision) => decision.taskId === task.id
1315
1662
  && decision.stepId === stepId
1316
1663
  && decision.triggeringAttemptCount === event.attemptOrdinal
1317
- && (decision.decision === 'continue-fix' || decision.decision === 'replan-required')
1664
+ && (decision.decision === 'continue-fix'
1665
+ || decision.decision === 'replan-required'
1666
+ || (decision.decision === 'stop-escalate' && overriddenStopDecisionEventIds.has(decision.eventId)))
1318
1667
  && sameStringArray(decision.coveredEventIds, priorEvents.map((candidate) => candidate.eventId)));
1319
1668
  if (matching.length > 1) {
1320
1669
  throw new WorkflowError('STATE_CORRUPT', 'Corrective remediation mode has conflicting decisions for the exact attempt ordinal.', {
@@ -1327,13 +1676,13 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
1327
1676
  }
1328
1677
  }
1329
1678
  }
1330
- function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds) {
1679
+ function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
1331
1680
  const stepIds = new Set(remediationEvents.map((event) => event.stepId));
1332
1681
  for (const stepId of stepIds) {
1333
1682
  const stepEvents = remediationEvents
1334
1683
  .filter((event) => event.stepId === stepId)
1335
1684
  .sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
1336
- validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds);
1685
+ validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds);
1337
1686
  }
1338
1687
  }
1339
1688
  function validateCorrectiveDecisionAudit(audit) {