codex-workflow-v2 2.0.0-beta.12 → 2.0.0-beta.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/src/alpha6/remediation.d.ts +8 -3
- package/dist/src/alpha6/remediation.js +374 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha7/corrective-recovery.d.ts +8 -1
- package/dist/src/alpha7/corrective-recovery.js +144 -26
- package/dist/src/alpha7/corrective-recovery.js.map +1 -1
- package/dist/src/cli.js +19 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +42 -0
- package/dist/src/dependency-provenance.js +5 -3
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +5 -1
- package/dist/src/workflow.js +158 -19
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +35 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +23 -5
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +17 -8
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +32 -14
- package/docs/release.md +9 -1
- package/docs/updating-existing-project.md +38 -0
- package/docs/validation-report.md +84 -80
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +21 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +24 -0
- 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.2
|
|
47
47
|
npx codex-workflow gateway handshake --repo .
|
|
48
48
|
npx codex-workflow doctor --repo .
|
|
49
49
|
```
|
|
@@ -302,9 +302,11 @@ 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.2 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,
|
|
307
|
+
Plan-integrity recovery, Task-local dependency provenance recovery, the human-confirmed legacy
|
|
308
|
+
attempt-four stop override (including its context-only Plan-rebind continuation), and the P04-A
|
|
309
|
+
structural-replacement stop boundary. Their reviewable
|
|
308
310
|
sources live in [`docs/pdf/sources`](docs/pdf/sources), and `npm run docs:pdf:check` proves the
|
|
309
311
|
tracked binaries were generated from the current source and package version. The PDFs remain
|
|
310
312
|
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,13 +31,17 @@ 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[];
|
|
33
|
-
export declare function
|
|
34
|
+
export declare function readStopEscalateOverrideEvents(store: FileStateStore, task: TaskState): StopEscalateOverrideEvent[];
|
|
35
|
+
export declare function stopEscalateOverrideDecisionEventIds(store: FileStateStore, task: TaskState): ReadonlySet<string>;
|
|
36
|
+
export declare function appendRemediationEvent(store: FileStateStore, task: TaskState, stepId: string, failureKind: RemediationFailureKind, failureEvidence: RemediationFailureEvidence, planRiskAudit: CurrentPlanRiskAudit, now: Date, reboundStopOverrideDecisionEventId?: string | null): 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
|
-
export declare function assertGuardedRemediationAttemptAllowed(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit, recoveredDecisionEventId?: string | null): GuardedRemediationAttemptGate;
|
|
44
|
+
export declare function assertGuardedRemediationAttemptAllowed(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit, recoveredDecisionEventId?: string | null, reboundStopOverrideDecisionEventId?: string | null): GuardedRemediationAttemptGate;
|
|
40
45
|
export declare function readGuardedRemediationPostureForStep(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit): GuardedRemediationAttemptGate | null;
|
|
41
46
|
export declare function findCurrentGuardedRemediationPosture(store: FileStateStore, task: TaskState, planRiskAudit: CurrentPlanRiskAudit): GuardedRemediationAttemptGate | null;
|
|
42
47
|
export declare function findFailedGuardedStepRequiringCorrectiveDecision(store: FileStateStore, task: TaskState, 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,7 +120,33 @@ function readRawCorrectiveDecisionEvents(store, task) {
|
|
|
115
120
|
validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
|
|
116
121
|
});
|
|
117
122
|
}
|
|
118
|
-
|
|
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
|
+
}
|
|
149
|
+
export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null) {
|
|
119
150
|
const recoveredDecisionEventId = findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId);
|
|
120
151
|
const existing = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
121
152
|
const matching = existing.filter((event) => remediationEventMatchesFailure(event, failureKind, failureEvidence));
|
|
@@ -132,6 +163,8 @@ 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
|
+
reboundStopOverrideDecisionEventId,
|
|
167
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
135
168
|
});
|
|
136
169
|
if (reused.attemptOrdinal !== gate.nextAttemptOrdinal || reused.mode !== gate.mode || reused.guardedCategory !== gate.guardedCategory) {
|
|
137
170
|
throw new WorkflowError('STATE_CORRUPT', 'Existing remediation event conflicts with the current guarded retry binding.', {
|
|
@@ -148,6 +181,8 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
148
181
|
}
|
|
149
182
|
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task), {
|
|
150
183
|
recoveredDecisionEventId,
|
|
184
|
+
reboundStopOverrideDecisionEventId,
|
|
185
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
151
186
|
});
|
|
152
187
|
return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, {
|
|
153
188
|
eventId: `RME-${createUlid(now.getTime())}`,
|
|
@@ -513,6 +548,176 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
|
|
|
513
548
|
validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
|
|
514
549
|
});
|
|
515
550
|
}
|
|
551
|
+
export function prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit) {
|
|
552
|
+
if (task.revision !== expectedRevision) {
|
|
553
|
+
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
554
|
+
expectedRevision,
|
|
555
|
+
actualRevision: task.revision,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
const normalizedActor = actor.trim();
|
|
559
|
+
const normalizedReason = reason.trim();
|
|
560
|
+
if (!normalizedActor)
|
|
561
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a human actor.');
|
|
562
|
+
if (!normalizedReason)
|
|
563
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a reason.');
|
|
564
|
+
if (!COMMIT_SHA_PATTERN.test(headCommit)) {
|
|
565
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires the exact current Git HEAD.');
|
|
566
|
+
}
|
|
567
|
+
if (task.status !== 'needs_fix') {
|
|
568
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a needs_fix Task.', {
|
|
569
|
+
taskId: task.id,
|
|
570
|
+
taskStatus: task.status,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
574
|
+
if (!step)
|
|
575
|
+
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
576
|
+
if (step.status !== 'failed') {
|
|
577
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires the exact failed Step.', {
|
|
578
|
+
taskId: task.id,
|
|
579
|
+
stepId,
|
|
580
|
+
stepStatus: step.status,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (!task.planHash)
|
|
584
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
|
|
585
|
+
const history = readOpenGuardedRemediationHistory(store, task, stepId);
|
|
586
|
+
const remediations = history.remediationEvents;
|
|
587
|
+
if (remediations.length !== 3
|
|
588
|
+
|| remediations[0]?.attemptOrdinal !== 1
|
|
589
|
+
|| remediations[0]?.mode !== 'ordinary'
|
|
590
|
+
|| remediations[1]?.attemptOrdinal !== 2
|
|
591
|
+
|| remediations[1]?.mode !== 'ordinary'
|
|
592
|
+
|| remediations[2]?.attemptOrdinal !== 3
|
|
593
|
+
|| remediations[2]?.mode !== 'corrective') {
|
|
594
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override is limited to the exact beta.11 three-failure history.', {
|
|
595
|
+
taskId: task.id,
|
|
596
|
+
stepId,
|
|
597
|
+
attempts: remediations.map((event) => ({ eventId: event.eventId, ordinal: event.attemptOrdinal, mode: event.mode })),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const priorContinueMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 3
|
|
601
|
+
&& event.decision === 'continue-fix'
|
|
602
|
+
&& event.planHash === task.planHash
|
|
603
|
+
&& sameStringArray(event.coveredEventIds, remediations.slice(0, 2).map((candidate) => candidate.eventId)));
|
|
604
|
+
const stopMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 4
|
|
605
|
+
&& event.decision === 'stop-escalate'
|
|
606
|
+
&& event.planHash === task.planHash
|
|
607
|
+
&& sameStringArray(event.coveredEventIds, remediations.map((candidate) => candidate.eventId)));
|
|
608
|
+
if (priorContinueMatches.length !== 1 || stopMatches.length !== 1) {
|
|
609
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires one exact attempt-3 continue decision and one exact attempt-4 stop decision.', {
|
|
610
|
+
taskId: task.id,
|
|
611
|
+
stepId,
|
|
612
|
+
priorContinueDecisionEventIds: priorContinueMatches.map((event) => event.eventId),
|
|
613
|
+
stopDecisionEventIds: stopMatches.map((event) => event.eventId),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const priorContinue = priorContinueMatches[0];
|
|
617
|
+
const stopDecision = stopMatches[0];
|
|
618
|
+
const binding = {
|
|
619
|
+
taskId: task.id,
|
|
620
|
+
taskRevision: task.revision,
|
|
621
|
+
stepId,
|
|
622
|
+
stopDecisionEventId: stopDecision.eventId,
|
|
623
|
+
stopDecisionEventHash: stopDecision.eventHash,
|
|
624
|
+
priorContinueDecisionEventId: priorContinue.eventId,
|
|
625
|
+
priorContinueDecisionEventHash: priorContinue.eventHash,
|
|
626
|
+
triggeringAttemptCount: 4,
|
|
627
|
+
coveredEventIds: remediations.map((event) => event.eventId),
|
|
628
|
+
planHash: task.planHash,
|
|
629
|
+
headCommit,
|
|
630
|
+
actor: normalizedActor,
|
|
631
|
+
reason: normalizedReason,
|
|
632
|
+
packageVersion: PACKAGE_VERSION,
|
|
633
|
+
policy: LEGACY_STOP_OVERRIDE_POLICY,
|
|
634
|
+
};
|
|
635
|
+
const confirmationCodeBindingHash = stopEscalateOverrideBindingHash(binding);
|
|
636
|
+
const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === stopDecision.eventId) ?? null;
|
|
637
|
+
if (existing && existing.confirmationCodeBindingHash !== confirmationCodeBindingHash) {
|
|
638
|
+
throw new WorkflowError('STATE_CONFLICT', 'The stop decision already has an override with a different human binding.', {
|
|
639
|
+
taskId: task.id,
|
|
640
|
+
stepId,
|
|
641
|
+
overrideEventId: existing.eventId,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
eligible: true,
|
|
646
|
+
action: 'task stop-override-apply',
|
|
647
|
+
...binding,
|
|
648
|
+
confirmationCodeBindingHash,
|
|
649
|
+
confirmationCode: `SOO-${confirmationCodeBindingHash.slice(0, 24).toUpperCase()}`,
|
|
650
|
+
appendOnly: true,
|
|
651
|
+
preservesOriginalDecision: true,
|
|
652
|
+
delegatedApprovalAllowed: false,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
export function appendStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit, confirmationCode, now) {
|
|
656
|
+
const prepared = prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit);
|
|
657
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
658
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override confirmation code does not match the current state binding.', {
|
|
659
|
+
taskId: task.id,
|
|
660
|
+
stepId,
|
|
661
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
665
|
+
const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === prepared.stopDecisionEventId) ?? null;
|
|
666
|
+
if (existing)
|
|
667
|
+
return existing;
|
|
668
|
+
const lockFile = path.join(root, STOP_ESCALATE_OVERRIDE_LOCK);
|
|
669
|
+
let descriptor;
|
|
670
|
+
try {
|
|
671
|
+
descriptor = openSync(lockFile, 'wx', 0o600);
|
|
672
|
+
closeSync(descriptor);
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
if (existsSync(lockFile)) {
|
|
676
|
+
throw new WorkflowError('LOCKED', `Stop override for ${task.id} is already being serialized.`, {
|
|
677
|
+
taskId: task.id,
|
|
678
|
+
stepId,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const currentTask = store.readTask(task.projectId, task.id);
|
|
685
|
+
const underLock = prepareStopEscalateOverride(store, currentTask, stepId, expectedRevision, actor, reason, headCommit);
|
|
686
|
+
if (underLock.confirmationCode !== prepared.confirmationCode
|
|
687
|
+
|| underLock.confirmationCodeBindingHash !== prepared.confirmationCodeBindingHash) {
|
|
688
|
+
throw new WorkflowError('STATE_CONFLICT', 'Stop override evidence changed after preparation.', {
|
|
689
|
+
taskId: task.id,
|
|
690
|
+
stepId,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return store.appendSidecarEvent(root, STOP_ESCALATE_OVERRIDES_SIDECAR, {
|
|
694
|
+
eventId: `SOO-${createUlid(now.getTime())}`,
|
|
695
|
+
recordedAt: now.toISOString(),
|
|
696
|
+
taskId: underLock.taskId,
|
|
697
|
+
taskRevision: underLock.taskRevision,
|
|
698
|
+
stepId: underLock.stepId,
|
|
699
|
+
stopDecisionEventId: underLock.stopDecisionEventId,
|
|
700
|
+
stopDecisionEventHash: underLock.stopDecisionEventHash,
|
|
701
|
+
priorContinueDecisionEventId: underLock.priorContinueDecisionEventId,
|
|
702
|
+
priorContinueDecisionEventHash: underLock.priorContinueDecisionEventHash,
|
|
703
|
+
triggeringAttemptCount: 4,
|
|
704
|
+
coveredEventIds: underLock.coveredEventIds,
|
|
705
|
+
planHash: underLock.planHash,
|
|
706
|
+
headCommit: underLock.headCommit,
|
|
707
|
+
actor: underLock.actor,
|
|
708
|
+
reason: underLock.reason,
|
|
709
|
+
packageVersion: underLock.packageVersion,
|
|
710
|
+
confirmationCodeBindingHash: underLock.confirmationCodeBindingHash,
|
|
711
|
+
policy: LEGACY_STOP_OVERRIDE_POLICY,
|
|
712
|
+
}, {
|
|
713
|
+
validateEvent: (event, context) => validateStoredStopEscalateOverride(event, currentTask, context.file, context.line),
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
if (existsSync(lockFile))
|
|
718
|
+
unlinkSync(lockFile);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
516
721
|
export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor, evidence, now) {
|
|
517
722
|
if (!task.planHash) {
|
|
518
723
|
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no Plan hash for Plan-integrity recovery.`);
|
|
@@ -572,11 +777,13 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
572
777
|
validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
|
|
573
778
|
});
|
|
574
779
|
}
|
|
575
|
-
export function assertGuardedRemediationAttemptAllowed(store, task, stepId, planRiskAudit, recoveredDecisionEventId = null) {
|
|
780
|
+
export function assertGuardedRemediationAttemptAllowed(store, task, stepId, planRiskAudit, recoveredDecisionEventId = null, reboundStopOverrideDecisionEventId = null) {
|
|
576
781
|
const history = readOpenGuardedRemediationHistory(store, task, stepId);
|
|
577
782
|
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
|
|
578
783
|
enforceRuntimeGate: true,
|
|
579
784
|
recoveredDecisionEventId,
|
|
785
|
+
reboundStopOverrideDecisionEventId,
|
|
786
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
580
787
|
});
|
|
581
788
|
}
|
|
582
789
|
export function readGuardedRemediationPostureForStep(store, task, stepId, planRiskAudit) {
|
|
@@ -597,14 +804,18 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
|
|
|
597
804
|
if (remediationEvents.length < 2 && earlyPlanIntegrityDecision?.recoveryBasis !== 'plan-integrity')
|
|
598
805
|
return null;
|
|
599
806
|
if (step.status === 'failed' || step.status === 'planned') {
|
|
600
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions
|
|
807
|
+
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
808
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
809
|
+
});
|
|
601
810
|
}
|
|
602
811
|
if (task.status !== 'blocked' || step.status !== 'in_progress' || !step.evidence)
|
|
603
812
|
return null;
|
|
604
813
|
const currentCycle = buildCurrentStrictStepReviewCycle(store, task, stepId);
|
|
605
814
|
if (currentCycle.resolution !== 'unverified')
|
|
606
815
|
return null;
|
|
607
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions
|
|
816
|
+
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
817
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
818
|
+
});
|
|
608
819
|
}
|
|
609
820
|
function readOpenGuardedRemediationHistory(store, task, stepId) {
|
|
610
821
|
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
@@ -694,6 +905,19 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
694
905
|
posture: 'continue-fix',
|
|
695
906
|
};
|
|
696
907
|
}
|
|
908
|
+
if (correctiveDecision.decision === 'stop-escalate'
|
|
909
|
+
&& options.reboundStopOverrideDecisionEventId === correctiveDecision.eventId
|
|
910
|
+
&& options.overriddenStopDecisionEventIds?.has(correctiveDecision.eventId)) {
|
|
911
|
+
return {
|
|
912
|
+
stepId,
|
|
913
|
+
guardedCategory,
|
|
914
|
+
attemptCount,
|
|
915
|
+
nextAttemptOrdinal,
|
|
916
|
+
mode: 'corrective',
|
|
917
|
+
correctiveDecision,
|
|
918
|
+
posture: 'continue-fix',
|
|
919
|
+
};
|
|
920
|
+
}
|
|
697
921
|
if (correctiveDecision.decision === 'replan-required') {
|
|
698
922
|
return {
|
|
699
923
|
stepId,
|
|
@@ -725,6 +949,18 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
725
949
|
posture: 'decision-required',
|
|
726
950
|
};
|
|
727
951
|
}
|
|
952
|
+
if (correctiveDecision.decision === 'stop-escalate'
|
|
953
|
+
&& options.overriddenStopDecisionEventIds?.has(correctiveDecision.eventId)) {
|
|
954
|
+
return {
|
|
955
|
+
stepId,
|
|
956
|
+
guardedCategory,
|
|
957
|
+
attemptCount,
|
|
958
|
+
nextAttemptOrdinal,
|
|
959
|
+
mode: 'corrective',
|
|
960
|
+
correctiveDecision,
|
|
961
|
+
posture: 'continue-fix',
|
|
962
|
+
};
|
|
963
|
+
}
|
|
728
964
|
if (correctiveDecision.decision !== 'continue-fix' && options.enforceRuntimeGate) {
|
|
729
965
|
throw correctiveDecisionBlockedError(task, stepId, attemptCount, correctiveDecision);
|
|
730
966
|
}
|
|
@@ -916,6 +1152,133 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
|
|
|
916
1152
|
});
|
|
917
1153
|
}
|
|
918
1154
|
}
|
|
1155
|
+
function validateStoredStopEscalateOverride(event, task, file, line) {
|
|
1156
|
+
if (!/^SOO-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)) {
|
|
1157
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override eventId is malformed.', { file, line });
|
|
1158
|
+
}
|
|
1159
|
+
if (event.taskId !== task.id
|
|
1160
|
+
|| !Number.isInteger(event.taskRevision)
|
|
1161
|
+
|| event.taskRevision < 1
|
|
1162
|
+
|| event.taskRevision > task.revision
|
|
1163
|
+
|| typeof event.stepId !== 'string'
|
|
1164
|
+
|| !event.stepId.trim()
|
|
1165
|
+
|| event.triggeringAttemptCount !== 4
|
|
1166
|
+
|| event.policy !== LEGACY_STOP_OVERRIDE_POLICY) {
|
|
1167
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override does not match its Task or compatibility policy.', {
|
|
1168
|
+
file,
|
|
1169
|
+
line,
|
|
1170
|
+
taskId: task.id,
|
|
1171
|
+
eventTaskId: event.taskId,
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
if (!/^CRD-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.stopDecisionEventId)
|
|
1175
|
+
|| !/^CRD-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.priorContinueDecisionEventId)
|
|
1176
|
+
|| !COMMIT_SHA_PATTERN.test(event.headCommit)
|
|
1177
|
+
|| typeof event.actor !== 'string'
|
|
1178
|
+
|| !event.actor.trim()
|
|
1179
|
+
|| typeof event.reason !== 'string'
|
|
1180
|
+
|| !event.reason.trim()
|
|
1181
|
+
|| typeof event.packageVersion !== 'string'
|
|
1182
|
+
|| !event.packageVersion.trim()) {
|
|
1183
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override identifiers or human evidence are malformed.', {
|
|
1184
|
+
file,
|
|
1185
|
+
line,
|
|
1186
|
+
taskId: task.id,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
const coveredEventIds = normalizeStringList(event.coveredEventIds, 'Stop override coveredEventIds');
|
|
1190
|
+
if (coveredEventIds.length !== 3 || new Set(coveredEventIds).size !== coveredEventIds.length) {
|
|
1191
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override must bind exactly three distinct remediation events.', {
|
|
1192
|
+
file,
|
|
1193
|
+
line,
|
|
1194
|
+
taskId: task.id,
|
|
1195
|
+
stepId: event.stepId,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
for (const [label, value] of [
|
|
1199
|
+
['stopDecisionEventHash', event.stopDecisionEventHash],
|
|
1200
|
+
['priorContinueDecisionEventHash', event.priorContinueDecisionEventHash],
|
|
1201
|
+
['planHash', event.planHash],
|
|
1202
|
+
['confirmationCodeBindingHash', event.confirmationCodeBindingHash],
|
|
1203
|
+
]) {
|
|
1204
|
+
assertHash64(value, `Stop override ${label} at ${file}:${line}`);
|
|
1205
|
+
}
|
|
1206
|
+
const expectedBindingHash = stopEscalateOverrideBindingHash(event);
|
|
1207
|
+
if (event.confirmationCodeBindingHash !== expectedBindingHash) {
|
|
1208
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override confirmation binding hash is invalid.', {
|
|
1209
|
+
file,
|
|
1210
|
+
line,
|
|
1211
|
+
taskId: task.id,
|
|
1212
|
+
expectedBindingHash,
|
|
1213
|
+
actualBindingHash: event.confirmationCodeBindingHash,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function validateStopEscalateOverrideHistory(task, remediations, decisions, overrides) {
|
|
1218
|
+
const overriddenDecisionIds = new Set();
|
|
1219
|
+
for (const override of overrides) {
|
|
1220
|
+
if (overriddenDecisionIds.has(override.stopDecisionEventId)) {
|
|
1221
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple stop overrides target one corrective decision.', {
|
|
1222
|
+
taskId: task.id,
|
|
1223
|
+
stepId: override.stepId,
|
|
1224
|
+
stopDecisionEventId: override.stopDecisionEventId,
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
const stepRemediations = remediations
|
|
1228
|
+
.filter((event) => event.stepId === override.stepId && event.attemptOrdinal <= 3)
|
|
1229
|
+
.sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
|
|
1230
|
+
const stopDecision = decisions.find((event) => event.eventId === override.stopDecisionEventId) ?? null;
|
|
1231
|
+
const priorContinue = decisions.find((event) => event.eventId === override.priorContinueDecisionEventId) ?? null;
|
|
1232
|
+
if (stepRemediations.length !== 3
|
|
1233
|
+
|| stepRemediations[0]?.attemptOrdinal !== 1
|
|
1234
|
+
|| stepRemediations[0]?.mode !== 'ordinary'
|
|
1235
|
+
|| stepRemediations[1]?.attemptOrdinal !== 2
|
|
1236
|
+
|| stepRemediations[1]?.mode !== 'ordinary'
|
|
1237
|
+
|| stepRemediations[2]?.attemptOrdinal !== 3
|
|
1238
|
+
|| stepRemediations[2]?.mode !== 'corrective'
|
|
1239
|
+
|| !stopDecision
|
|
1240
|
+
|| stopDecision.eventHash !== override.stopDecisionEventHash
|
|
1241
|
+
|| stopDecision.decision !== 'stop-escalate'
|
|
1242
|
+
|| stopDecision.triggeringAttemptCount !== 4
|
|
1243
|
+
|| stopDecision.planHash !== override.planHash
|
|
1244
|
+
|| !priorContinue
|
|
1245
|
+
|| priorContinue.eventHash !== override.priorContinueDecisionEventHash
|
|
1246
|
+
|| priorContinue.decision !== 'continue-fix'
|
|
1247
|
+
|| priorContinue.triggeringAttemptCount !== 3
|
|
1248
|
+
|| priorContinue.planHash !== override.planHash
|
|
1249
|
+
|| !sameStringArray(override.coveredEventIds, stepRemediations.map((event) => event.eventId))
|
|
1250
|
+
|| !sameStringArray(stopDecision.coveredEventIds, override.coveredEventIds)
|
|
1251
|
+
|| !sameStringArray(priorContinue.coveredEventIds, override.coveredEventIds.slice(0, 2))) {
|
|
1252
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override does not bind the exact beta.11 attempt-4 stop history.', {
|
|
1253
|
+
taskId: task.id,
|
|
1254
|
+
stepId: override.stepId,
|
|
1255
|
+
overrideEventId: override.eventId,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
overriddenDecisionIds.add(override.stopDecisionEventId);
|
|
1259
|
+
}
|
|
1260
|
+
return overriddenDecisionIds;
|
|
1261
|
+
}
|
|
1262
|
+
function stopEscalateOverrideBindingHash(input) {
|
|
1263
|
+
return hashCanonical({
|
|
1264
|
+
domain: 'codex-workflow/stop-escalate-override/v1',
|
|
1265
|
+
taskId: input.taskId,
|
|
1266
|
+
taskRevision: input.taskRevision,
|
|
1267
|
+
stepId: input.stepId,
|
|
1268
|
+
stopDecisionEventId: input.stopDecisionEventId,
|
|
1269
|
+
stopDecisionEventHash: input.stopDecisionEventHash,
|
|
1270
|
+
priorContinueDecisionEventId: input.priorContinueDecisionEventId,
|
|
1271
|
+
priorContinueDecisionEventHash: input.priorContinueDecisionEventHash,
|
|
1272
|
+
triggeringAttemptCount: input.triggeringAttemptCount,
|
|
1273
|
+
coveredEventIds: [...input.coveredEventIds],
|
|
1274
|
+
planHash: input.planHash,
|
|
1275
|
+
headCommit: input.headCommit,
|
|
1276
|
+
actor: input.actor,
|
|
1277
|
+
reason: input.reason,
|
|
1278
|
+
packageVersion: input.packageVersion,
|
|
1279
|
+
policy: input.policy,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
919
1282
|
function validatePlanIntegrityDecision(event, task, file, line) {
|
|
920
1283
|
const evidence = event.planIntegrityEvidence;
|
|
921
1284
|
if (event.recoveryBasis !== 'plan-integrity'
|
|
@@ -1286,7 +1649,7 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
|
|
|
1286
1649
|
}
|
|
1287
1650
|
}
|
|
1288
1651
|
}
|
|
1289
|
-
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds) {
|
|
1652
|
+
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
|
|
1290
1653
|
for (const event of remediationEvents) {
|
|
1291
1654
|
if (event.attemptOrdinal <= 2) {
|
|
1292
1655
|
if (event.mode !== 'ordinary') {
|
|
@@ -1314,7 +1677,9 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
|
|
|
1314
1677
|
const matching = correctiveDecisions.filter((decision) => decision.taskId === task.id
|
|
1315
1678
|
&& decision.stepId === stepId
|
|
1316
1679
|
&& decision.triggeringAttemptCount === event.attemptOrdinal
|
|
1317
|
-
&& (decision.decision === 'continue-fix'
|
|
1680
|
+
&& (decision.decision === 'continue-fix'
|
|
1681
|
+
|| decision.decision === 'replan-required'
|
|
1682
|
+
|| (decision.decision === 'stop-escalate' && overriddenStopDecisionEventIds.has(decision.eventId)))
|
|
1318
1683
|
&& sameStringArray(decision.coveredEventIds, priorEvents.map((candidate) => candidate.eventId)));
|
|
1319
1684
|
if (matching.length > 1) {
|
|
1320
1685
|
throw new WorkflowError('STATE_CORRUPT', 'Corrective remediation mode has conflicting decisions for the exact attempt ordinal.', {
|
|
@@ -1327,13 +1692,13 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
|
|
|
1327
1692
|
}
|
|
1328
1693
|
}
|
|
1329
1694
|
}
|
|
1330
|
-
function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds) {
|
|
1695
|
+
function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
|
|
1331
1696
|
const stepIds = new Set(remediationEvents.map((event) => event.stepId));
|
|
1332
1697
|
for (const stepId of stepIds) {
|
|
1333
1698
|
const stepEvents = remediationEvents
|
|
1334
1699
|
.filter((event) => event.stepId === stepId)
|
|
1335
1700
|
.sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
|
|
1336
|
-
validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds);
|
|
1701
|
+
validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds);
|
|
1337
1702
|
}
|
|
1338
1703
|
}
|
|
1339
1704
|
function validateCorrectiveDecisionAudit(audit) {
|