codex-workflow-v2 2.0.0-beta.11 → 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.
- package/README.md +7 -6
- package/dist/src/alpha6/remediation.d.ts +7 -2
- package/dist/src/alpha6/remediation.js +361 -64
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +5 -0
- package/dist/src/alpha6/review.js +110 -2
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/cli.js +19 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +55 -0
- package/dist/src/dependency-provenance.js +5 -3
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/reviewer.d.ts +26 -0
- package/dist/src/reviewer.js +54 -1
- package/dist/src/reviewer.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 +4 -1
- package/dist/src/workflow.js +163 -71
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +43 -34
- package/docs/decisions.md +5 -4
- package/docs/delegated-approval.md +5 -4
- package/docs/development-flow.md +7 -6
- package/docs/lifecycle/state-machine-stabilization.md +1 -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 +21 -9
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +15 -13
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +39 -24
- package/docs/release.md +8 -1
- package/docs/updating-existing-project.md +25 -0
- package/docs/validation-report.md +90 -115
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +27 -8
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +23 -3
- package/references/state-machine.md +9 -8
- package/references/validation-and-review.md +11 -4
- package/schemas/review-result.schema.json +17 -1
- package/schemas/step-review-event.schema.json +15 -0
- package/schemas/stop-escalate-override-event.schema.json +45 -0
- package/scripts/generate-pdf-docs.py +2 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ automation do not import anything from the parent V1 project.
|
|
|
19
19
|
- one writer lease and one atomic Git commit per completed Step;
|
|
20
20
|
- execution authorization with a bounded, versioned mechanical Plan-feasibility payload, independent
|
|
21
21
|
review, and a state-bound human final-acceptance gate;
|
|
22
|
-
- high-risk Plan guidance
|
|
22
|
+
- high-risk Plan guidance plus evidence-bound `fix`/`replan` review routing without an attempt-count stop;
|
|
23
23
|
- executable proof obligations with hostile counterexamples for every guarded failure mode;
|
|
24
24
|
- read-only update preflight for clean checkout, running-Step, and writer-lease blockers;
|
|
25
25
|
- beta.10 bounded recovery for one exact dependency-only HEAD, including retained product commits
|
|
@@ -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.
|
|
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
|
```
|
|
@@ -200,8 +200,8 @@ corrective attempt was persisted as `mode=ordinary`. `next` exposes
|
|
|
200
200
|
returned command with its exact Task revision, actor, and writer token. Recovery
|
|
201
201
|
appends a bound event to `remediation-mode-recoveries.jsonl`; it never edits the
|
|
202
202
|
historical remediation event. An identical retry is idempotent. After recovery,
|
|
203
|
-
the
|
|
204
|
-
|
|
203
|
+
the historical attempt is interpreted as `corrective`, but no attempt-count hard stop is
|
|
204
|
+
created: after any required Knowledge refresh, `next` may continue the same failed Step.
|
|
205
205
|
|
|
206
206
|
For a legacy authorization without P01-A evidence, or a contradiction that genuinely becomes
|
|
207
207
|
observable only after execution starts, the first guarded `checks-failed` event may cause `next` to advertise
|
|
@@ -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.
|
|
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,
|
|
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;
|
|
@@ -12,7 +13,7 @@ export interface GuardedRemediationAttemptGate {
|
|
|
12
13
|
nextAttemptOrdinal: number;
|
|
13
14
|
mode: RemediationMode;
|
|
14
15
|
correctiveDecision: CorrectiveDecisionEvent | null;
|
|
15
|
-
posture: 'ordinary' | 'decision-required' | 'continue-fix' | 'replan-required' | 'replan-ready' | 'split-required' | 'stop-escalate'
|
|
16
|
+
posture: 'ordinary' | 'decision-required' | 'continue-fix' | 'replan-required' | 'replan-ready' | 'split-required' | 'stop-escalate';
|
|
16
17
|
}
|
|
17
18
|
export interface RemediationModeRecoveryAssessment {
|
|
18
19
|
task: TaskState;
|
|
@@ -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())}`,
|
|
@@ -458,17 +491,6 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
|
|
|
458
491
|
triggeringAttemptCount,
|
|
459
492
|
});
|
|
460
493
|
}
|
|
461
|
-
if (triggeringAttemptCount > 3
|
|
462
|
-
&& correctiveAudit.decision !== 'split-required'
|
|
463
|
-
&& correctiveAudit.decision !== 'stop-escalate') {
|
|
464
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} reached the hard stop after its third failed remediation review.`, {
|
|
465
|
-
taskId: task.id,
|
|
466
|
-
stepId,
|
|
467
|
-
failedAttemptCount: remediationEvents.length,
|
|
468
|
-
requestedDecision: correctiveAudit.decision,
|
|
469
|
-
allowedDecisions: ['split-required', 'stop-escalate'],
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
494
|
const decisions = readCorrectiveDecisionEvents(store, task);
|
|
473
495
|
const matching = decisions.filter((event) => event.taskId === task.id
|
|
474
496
|
&& event.stepId === stepId
|
|
@@ -524,6 +546,176 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
|
|
|
524
546
|
validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
|
|
525
547
|
});
|
|
526
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
|
+
}
|
|
527
719
|
export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor, evidence, now) {
|
|
528
720
|
if (!task.planHash) {
|
|
529
721
|
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no Plan hash for Plan-integrity recovery.`);
|
|
@@ -588,6 +780,7 @@ export function assertGuardedRemediationAttemptAllowed(store, task, stepId, plan
|
|
|
588
780
|
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
|
|
589
781
|
enforceRuntimeGate: true,
|
|
590
782
|
recoveredDecisionEventId,
|
|
783
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
591
784
|
});
|
|
592
785
|
}
|
|
593
786
|
export function readGuardedRemediationPostureForStep(store, task, stepId, planRiskAudit) {
|
|
@@ -608,14 +801,18 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
|
|
|
608
801
|
if (remediationEvents.length < 2 && earlyPlanIntegrityDecision?.recoveryBasis !== 'plan-integrity')
|
|
609
802
|
return null;
|
|
610
803
|
if (step.status === 'failed' || step.status === 'planned') {
|
|
611
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions
|
|
804
|
+
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
805
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
806
|
+
});
|
|
612
807
|
}
|
|
613
808
|
if (task.status !== 'blocked' || step.status !== 'in_progress' || !step.evidence)
|
|
614
809
|
return null;
|
|
615
810
|
const currentCycle = buildCurrentStrictStepReviewCycle(store, task, stepId);
|
|
616
811
|
if (currentCycle.resolution !== 'unverified')
|
|
617
812
|
return null;
|
|
618
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions
|
|
813
|
+
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
814
|
+
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
815
|
+
});
|
|
619
816
|
}
|
|
620
817
|
function readOpenGuardedRemediationHistory(store, task, stepId) {
|
|
621
818
|
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
@@ -646,7 +843,7 @@ export function findFailedGuardedStepRequiringCorrectiveDecision(store, task, pl
|
|
|
646
843
|
const posture = findCurrentGuardedRemediationPosture(store, task, planRiskAudit);
|
|
647
844
|
if (!posture)
|
|
648
845
|
return null;
|
|
649
|
-
return posture.posture === 'decision-required'
|
|
846
|
+
return posture.posture === 'decision-required'
|
|
650
847
|
? posture
|
|
651
848
|
: null;
|
|
652
849
|
}
|
|
@@ -681,56 +878,15 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
681
878
|
};
|
|
682
879
|
}
|
|
683
880
|
const correctiveDecision = findCurrentCorrectiveDecision(task, stepId, nextAttemptOrdinal, correctiveDecisions);
|
|
684
|
-
if (attemptCount >= 3) {
|
|
685
|
-
if (correctiveDecision?.decision === 'split-required' || correctiveDecision?.decision === 'stop-escalate') {
|
|
686
|
-
return {
|
|
687
|
-
stepId,
|
|
688
|
-
guardedCategory,
|
|
689
|
-
attemptCount,
|
|
690
|
-
nextAttemptOrdinal,
|
|
691
|
-
mode: 'ordinary',
|
|
692
|
-
correctiveDecision,
|
|
693
|
-
posture: correctiveDecision.decision,
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
if (options.enforceRuntimeGate) {
|
|
697
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} reached the hard stop after its third failed remediation review.`, {
|
|
698
|
-
taskId: task.id,
|
|
699
|
-
stepId,
|
|
700
|
-
failedAttemptCount: attemptCount,
|
|
701
|
-
requiredAction: 'Record split-required or stop-escalate; another remediation run is prohibited.',
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
return {
|
|
705
|
-
stepId,
|
|
706
|
-
guardedCategory,
|
|
707
|
-
attemptCount,
|
|
708
|
-
nextAttemptOrdinal,
|
|
709
|
-
mode: 'ordinary',
|
|
710
|
-
correctiveDecision,
|
|
711
|
-
posture: 'hard-stop',
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
881
|
if (!correctiveDecision) {
|
|
715
|
-
if (options.enforceRuntimeGate) {
|
|
716
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} exhausted two ordinary remediation attempts and requires a corrective decision before another run.`, {
|
|
717
|
-
taskId: task.id,
|
|
718
|
-
stepId,
|
|
719
|
-
attemptCount,
|
|
720
|
-
requiredSidecar: CORRECTIVE_DECISIONS_SIDECAR,
|
|
721
|
-
nextAttemptOrdinal,
|
|
722
|
-
allowedDecision: 'continue-fix',
|
|
723
|
-
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
882
|
return {
|
|
727
883
|
stepId,
|
|
728
884
|
guardedCategory,
|
|
729
885
|
attemptCount,
|
|
730
886
|
nextAttemptOrdinal,
|
|
731
|
-
mode: '
|
|
887
|
+
mode: 'corrective',
|
|
732
888
|
correctiveDecision: null,
|
|
733
|
-
posture: '
|
|
889
|
+
posture: 'continue-fix',
|
|
734
890
|
};
|
|
735
891
|
}
|
|
736
892
|
if (correctiveDecision.planHash !== task.planHash) {
|
|
@@ -777,6 +933,18 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
777
933
|
posture: 'decision-required',
|
|
778
934
|
};
|
|
779
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
|
+
}
|
|
780
948
|
if (correctiveDecision.decision !== 'continue-fix' && options.enforceRuntimeGate) {
|
|
781
949
|
throw correctiveDecisionBlockedError(task, stepId, attemptCount, correctiveDecision);
|
|
782
950
|
}
|
|
@@ -968,6 +1136,133 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
|
|
|
968
1136
|
});
|
|
969
1137
|
}
|
|
970
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
|
+
}
|
|
971
1266
|
function validatePlanIntegrityDecision(event, task, file, line) {
|
|
972
1267
|
const evidence = event.planIntegrityEvidence;
|
|
973
1268
|
if (event.recoveryBasis !== 'plan-integrity'
|
|
@@ -1338,7 +1633,7 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
|
|
|
1338
1633
|
}
|
|
1339
1634
|
}
|
|
1340
1635
|
}
|
|
1341
|
-
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds) {
|
|
1636
|
+
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
|
|
1342
1637
|
for (const event of remediationEvents) {
|
|
1343
1638
|
if (event.attemptOrdinal <= 2) {
|
|
1344
1639
|
if (event.mode !== 'ordinary') {
|
|
@@ -1366,10 +1661,12 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
|
|
|
1366
1661
|
const matching = correctiveDecisions.filter((decision) => decision.taskId === task.id
|
|
1367
1662
|
&& decision.stepId === stepId
|
|
1368
1663
|
&& decision.triggeringAttemptCount === event.attemptOrdinal
|
|
1369
|
-
&& (decision.decision === 'continue-fix'
|
|
1664
|
+
&& (decision.decision === 'continue-fix'
|
|
1665
|
+
|| decision.decision === 'replan-required'
|
|
1666
|
+
|| (decision.decision === 'stop-escalate' && overriddenStopDecisionEventIds.has(decision.eventId)))
|
|
1370
1667
|
&& sameStringArray(decision.coveredEventIds, priorEvents.map((candidate) => candidate.eventId)));
|
|
1371
|
-
if (matching.length
|
|
1372
|
-
throw new WorkflowError('STATE_CORRUPT', 'Corrective remediation mode
|
|
1668
|
+
if (matching.length > 1) {
|
|
1669
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective remediation mode has conflicting decisions for the exact attempt ordinal.', {
|
|
1373
1670
|
taskId: task.id,
|
|
1374
1671
|
stepId,
|
|
1375
1672
|
eventId: event.eventId,
|
|
@@ -1379,13 +1676,13 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
|
|
|
1379
1676
|
}
|
|
1380
1677
|
}
|
|
1381
1678
|
}
|
|
1382
|
-
function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds) {
|
|
1679
|
+
function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
|
|
1383
1680
|
const stepIds = new Set(remediationEvents.map((event) => event.stepId));
|
|
1384
1681
|
for (const stepId of stepIds) {
|
|
1385
1682
|
const stepEvents = remediationEvents
|
|
1386
1683
|
.filter((event) => event.stepId === stepId)
|
|
1387
1684
|
.sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
|
|
1388
|
-
validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds);
|
|
1685
|
+
validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds);
|
|
1389
1686
|
}
|
|
1390
1687
|
}
|
|
1391
1688
|
function validateCorrectiveDecisionAudit(audit) {
|