codex-workflow-v2 2.0.0-beta.12.9 → 2.0.0-beta.13
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 +58 -16
- package/dist/reviewer-runtime-build.json +1092 -0
- package/dist/src/alpha6/check-support-anchor.d.ts +24 -0
- package/dist/src/alpha6/check-support-anchor.js +102 -0
- package/dist/src/alpha6/check-support-anchor.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +28 -0
- package/dist/src/alpha6/component-owner.js +95 -0
- package/dist/src/alpha6/component-owner.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.js +4 -2
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.js +42 -12
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -1
- package/dist/src/alpha6/milestone.d.ts +6 -2
- package/dist/src/alpha6/milestone.js +164 -10
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/npm-check-contract.d.ts +11 -0
- package/dist/src/alpha6/npm-check-contract.js +36 -0
- package/dist/src/alpha6/npm-check-contract.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +157 -27
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +3 -1
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation-cause.d.ts +13 -0
- package/dist/src/alpha6/remediation-cause.js +77 -0
- package/dist/src/alpha6/remediation-cause.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +15 -4
- package/dist/src/alpha6/remediation.js +430 -67
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.js +34 -4
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/artifacts.js +6 -0
- package/dist/src/artifacts.js.map +1 -1
- package/dist/src/cli-actions.d.ts +25 -0
- package/dist/src/cli-actions.js +109 -0
- package/dist/src/cli-actions.js.map +1 -0
- package/dist/src/cli.js +216 -258
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +166 -5
- package/dist/src/credential-transport.d.ts +38 -0
- package/dist/src/credential-transport.js +156 -0
- package/dist/src/credential-transport.js.map +1 -0
- package/dist/src/dependency-provenance.d.ts +23 -2
- package/dist/src/dependency-provenance.js +221 -34
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/completed-step-carryover.d.ts +13 -0
- package/dist/src/domain/completed-step-carryover.js +71 -0
- package/dist/src/domain/completed-step-carryover.js.map +1 -0
- package/dist/src/domain/validation.d.ts +9 -1
- package/dist/src/domain/validation.js +86 -0
- package/dist/src/domain/validation.js.map +1 -1
- package/dist/src/gateway-handshake.d.ts +11 -0
- package/dist/src/gateway-handshake.js +92 -0
- package/dist/src/gateway-handshake.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +1 -1
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/lifecycle/semantic-registry.js +1 -1
- package/dist/src/observation.js +1 -2
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.d.ts +114 -0
- package/dist/src/observed-routes.js +545 -0
- package/dist/src/observed-routes.js.map +1 -0
- package/dist/src/operational-contract.d.ts +26 -0
- package/dist/src/operational-contract.js +94 -0
- package/dist/src/operational-contract.js.map +1 -0
- package/dist/src/reviewer.d.ts +218 -8
- package/dist/src/reviewer.js +927 -25
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-executor.js +18 -15
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +35 -10
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.js +12 -4
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.js +11 -2
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -0
- package/dist/src/state/lock.js +24 -0
- package/dist/src/state/lock.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 +65 -4
- package/dist/src/workflow.js +1704 -155
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +41 -14
- package/docs/delegated-approval.md +6 -1
- package/docs/development-flow.md +42 -12
- 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 +181 -22
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +148 -26
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +326 -52
- package/docs/problem-briefs/01-pre-implementation-integrity.md +11 -7
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +13 -6
- package/docs/problem-briefs/README.md +11 -11
- package/docs/release.md +83 -9
- package/docs/updating-existing-project.md +205 -0
- package/docs/validation-report.md +156 -99
- package/package.json +12 -3
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +42 -1
- package/schemas/authorization-event.schema.json +10 -3
- package/schemas/corrective-decision-event.schema.json +164 -9
- package/schemas/corrective-replan-credentials.private.schema.json +4 -2
- package/schemas/milestone-scope-change-event.schema.json +25 -1
- package/schemas/remediation-event.schema.json +17 -1
- package/schemas/review-result.schema.json +1 -0
- package/schemas/step-review-event.schema.json +15 -0
- package/schemas/task.schema.json +61 -1
- package/schemas/transition-definition.schema.json +8 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { pathAllowed } from '../domain/validation.js';
|
|
3
4
|
import { WorkflowError } from '../errors.js';
|
|
4
5
|
import { hashCanonical } from '../lifecycle/fingerprint.js';
|
|
5
6
|
import { createUlid } from '../ulid.js';
|
|
6
7
|
import { PACKAGE_VERSION } from '../version.js';
|
|
7
|
-
import { assertHash64, canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
|
|
8
|
+
import { assertHash64, canonicalJsonStringify, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
|
|
8
9
|
import { readTaskC1Posture } from './handoff.js';
|
|
9
10
|
import { buildCurrentStrictStepReviewCycle, latestPassedVerifiedStepReviewEvent, readReviewerAttestations, readStepReviewEvents, } from './review.js';
|
|
11
|
+
import { semanticBoundaryHash } from './component-owner.js';
|
|
12
|
+
import { reviewerCauseFingerprint } from './remediation-cause.js';
|
|
10
13
|
export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
|
|
11
14
|
export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
|
|
12
15
|
export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
|
|
@@ -146,9 +149,18 @@ export function stopEscalateOverrideDecisionEventIds(store, task) {
|
|
|
146
149
|
validateCorrectiveDecisionCoverage(task, decisions, remediations);
|
|
147
150
|
return validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
|
|
148
151
|
}
|
|
149
|
-
export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null) {
|
|
152
|
+
export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null, causeBindings = []) {
|
|
153
|
+
const prepared = prepareRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId, causeBindings);
|
|
154
|
+
if (prepared.postimage === null)
|
|
155
|
+
return prepared.event;
|
|
156
|
+
return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, prepared.event, {
|
|
157
|
+
validateEvent: (event, context) => validateStoredRemediationEvent(event, task, context.file, context.line),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
export function prepareRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null, causeBindings = []) {
|
|
150
161
|
const recoveredDecisionEventId = findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId);
|
|
151
|
-
const
|
|
162
|
+
const allExisting = readRemediationEvents(store, task);
|
|
163
|
+
const existing = allExisting.filter((event) => event.stepId === stepId);
|
|
152
164
|
const matching = existing.filter((event) => remediationEventMatchesFailure(event, failureKind, failureEvidence));
|
|
153
165
|
if (matching.length > 1) {
|
|
154
166
|
throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation events match the same failure identity.', {
|
|
@@ -160,6 +172,14 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
160
172
|
}
|
|
161
173
|
if (matching.length === 1) {
|
|
162
174
|
const reused = matching[0];
|
|
175
|
+
if (canonicalJsonStringify(reused.failureEvidence) !== canonicalJsonStringify(failureEvidence)) {
|
|
176
|
+
throw new WorkflowError('STATE_CONFLICT', 'Remediation failure evidence changed within the same execution identity.', {
|
|
177
|
+
taskId: task.id,
|
|
178
|
+
stepId,
|
|
179
|
+
eventId: reused.eventId,
|
|
180
|
+
executionTaskRevision: failureEvidence.executionTaskRevision ?? null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
163
183
|
const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
|
|
164
184
|
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task), {
|
|
165
185
|
recoveredDecisionEventId,
|
|
@@ -177,14 +197,14 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
177
197
|
actualMode: reused.mode,
|
|
178
198
|
});
|
|
179
199
|
}
|
|
180
|
-
return reused;
|
|
200
|
+
return { event: reused, postimage: null };
|
|
181
201
|
}
|
|
182
202
|
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task), {
|
|
183
203
|
recoveredDecisionEventId,
|
|
184
204
|
reboundStopOverrideDecisionEventId,
|
|
185
205
|
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
186
206
|
});
|
|
187
|
-
|
|
207
|
+
const prepared = prepareSidecarAppend(allExisting, {
|
|
188
208
|
eventId: `RME-${createUlid(now.getTime())}`,
|
|
189
209
|
recordedAt: now.toISOString(),
|
|
190
210
|
taskId: task.id,
|
|
@@ -194,9 +214,11 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
194
214
|
failureEvidence,
|
|
195
215
|
attemptOrdinal: gate.nextAttemptOrdinal,
|
|
196
216
|
mode: gate.mode,
|
|
217
|
+
causeBindings: causeBindings.map((binding) => ({ ...binding })),
|
|
197
218
|
}, {
|
|
198
219
|
validateEvent: (event, context) => validateStoredRemediationEvent(event, task, context.file, context.line),
|
|
199
220
|
});
|
|
221
|
+
return { event: prepared.event, postimage: prepared.content };
|
|
200
222
|
}
|
|
201
223
|
function findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId) {
|
|
202
224
|
const remediations = readRawRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
@@ -222,7 +244,7 @@ function findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId) {
|
|
|
222
244
|
}
|
|
223
245
|
return recoveries[0]?.correctiveDecisionEventId ?? null;
|
|
224
246
|
}
|
|
225
|
-
export function findRemediationModeRecoveryCandidate(store, task) {
|
|
247
|
+
export function findRemediationModeRecoveryCandidate(store, task, options = {}) {
|
|
226
248
|
const raw = readRawRemediationEvents(store, task);
|
|
227
249
|
const candidates = raw.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
|
|
228
250
|
if (candidates.length === 0)
|
|
@@ -236,9 +258,9 @@ export function findRemediationModeRecoveryCandidate(store, task) {
|
|
|
236
258
|
const posture = readTaskC1Posture(store, task);
|
|
237
259
|
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
238
260
|
const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
|
|
239
|
-
return assessRemediationModeRecovery(store, task, candidates[0].stepId, actor, lease?.token ?? null);
|
|
261
|
+
return assessRemediationModeRecovery(store, task, candidates[0].stepId, actor, lease?.token ?? null, options);
|
|
240
262
|
}
|
|
241
|
-
export function assessRemediationModeRecovery(store, task, stepId, actor, writerToken) {
|
|
263
|
+
export function assessRemediationModeRecovery(store, task, stepId, actor, writerToken, options = {}) {
|
|
242
264
|
const normalizedActor = actor.trim();
|
|
243
265
|
if (!normalizedActor)
|
|
244
266
|
throw new WorkflowError('INVALID_ARGUMENT', 'Remediation mode recovery actor is required.');
|
|
@@ -303,7 +325,9 @@ export function assessRemediationModeRecovery(store, task, stepId, actor, writer
|
|
|
303
325
|
});
|
|
304
326
|
}
|
|
305
327
|
const correctiveRecovery = correctiveRecoveries[0];
|
|
306
|
-
const authority =
|
|
328
|
+
const authority = options.authorityMode === 'observe'
|
|
329
|
+
? observeRemediationModeRecoveryAuthority(store, task)
|
|
330
|
+
: requireRemediationModeRecoveryAuthority(store, task, normalizedActor, writerToken);
|
|
307
331
|
const existing = readRemediationModeRecoveryEvents(store, task).filter((event) => event.remediationEventId === malformed[0].eventId);
|
|
308
332
|
if (existing.length > 1) {
|
|
309
333
|
throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation mode recoveries target one event.', {
|
|
@@ -353,6 +377,14 @@ export function assessRemediationModeRecovery(store, task, stepId, actor, writer
|
|
|
353
377
|
evidenceFingerprint,
|
|
354
378
|
};
|
|
355
379
|
}
|
|
380
|
+
function observeRemediationModeRecoveryAuthority(store, task) {
|
|
381
|
+
const posture = readTaskC1Posture(store, task);
|
|
382
|
+
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
383
|
+
return {
|
|
384
|
+
c1ClaimEventHash: posture.state === 'none' ? null : posture.latestEvent.eventHash,
|
|
385
|
+
writerLeaseTokenHash: lease ? sha256Hex(lease.token) : null,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
356
388
|
export function appendRemediationModeRecovery(store, task, stepId, expectedRevision, actor, writerToken, now) {
|
|
357
389
|
if (task.revision !== expectedRevision) {
|
|
358
390
|
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
@@ -726,18 +758,34 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
726
758
|
throw new WorkflowError('INVALID_ARGUMENT', 'Plan-integrity recovery requires a derived auditor identity.');
|
|
727
759
|
}
|
|
728
760
|
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
761
|
+
const expectedBindings = evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
762
|
+
? [{
|
|
763
|
+
eventId: evidence.remediationEventId,
|
|
764
|
+
eventHash: evidence.remediationEventHash,
|
|
765
|
+
attemptOrdinal: 1,
|
|
766
|
+
}]
|
|
767
|
+
: evidence.remediationEvents;
|
|
768
|
+
if (remediationEvents.length !== expectedBindings.length
|
|
769
|
+
|| remediationEvents.some((event, index) => {
|
|
770
|
+
const binding = expectedBindings[index];
|
|
771
|
+
return event.attemptOrdinal !== index + 1
|
|
772
|
+
|| event.failureKind !== 'checks-failed'
|
|
773
|
+
|| event.eventId !== binding?.eventId
|
|
774
|
+
|| event.eventHash !== binding.eventHash
|
|
775
|
+
|| event.attemptOrdinal !== binding.attemptOrdinal;
|
|
776
|
+
})) {
|
|
777
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the exact checks-failed remediation history.', {
|
|
735
778
|
taskId: task.id,
|
|
736
779
|
stepId,
|
|
737
780
|
remediationEventIds: remediationEvents.map((event) => event.eventId),
|
|
738
781
|
});
|
|
739
782
|
}
|
|
740
|
-
const
|
|
783
|
+
const triggeringAttemptCount = remediationEvents.length + 1;
|
|
784
|
+
const recoveryDecision = evidence.kind === 'check-support-anchor-outside-allowed-writes'
|
|
785
|
+
|| evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
786
|
+
? 'continue-fix'
|
|
787
|
+
: 'replan-required';
|
|
788
|
+
const decisions = readCorrectiveDecisionEvents(store, task).filter((event) => event.stepId === stepId && event.triggeringAttemptCount === triggeringAttemptCount);
|
|
741
789
|
const findingId = `PLAN-INTEGRITY-${evidence.conflictHash.slice(0, 16).toUpperCase()}`;
|
|
742
790
|
if (decisions.length > 1) {
|
|
743
791
|
throw new WorkflowError('STATE_CORRUPT', 'Multiple Plan-integrity recovery decisions exist for the same Step.', {
|
|
@@ -749,7 +797,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
749
797
|
if (decisions.length === 1) {
|
|
750
798
|
const existing = decisions[0];
|
|
751
799
|
if (existing.planHash !== task.planHash
|
|
752
|
-
|| existing.decision !==
|
|
800
|
+
|| existing.decision !== recoveryDecision
|
|
753
801
|
|| existing.auditor !== auditor.trim()
|
|
754
802
|
|| existing.planIntegrityEvidence?.conflictHash !== evidence.conflictHash) {
|
|
755
803
|
throw new WorkflowError('STATE_CONFLICT', 'Existing Plan-integrity recovery decision does not match the requested conflict.', {
|
|
@@ -765,11 +813,11 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
765
813
|
recordedAt: now.toISOString(),
|
|
766
814
|
taskId: task.id,
|
|
767
815
|
stepId,
|
|
768
|
-
triggeringAttemptCount
|
|
769
|
-
decision:
|
|
816
|
+
triggeringAttemptCount,
|
|
817
|
+
decision: recoveryDecision,
|
|
770
818
|
auditor: auditor.trim(),
|
|
771
819
|
coveredFindingIds: [findingId],
|
|
772
|
-
coveredEventIds:
|
|
820
|
+
coveredEventIds: remediationEvents.map((event) => event.eventId),
|
|
773
821
|
planHash: task.planHash,
|
|
774
822
|
recoveryBasis: 'plan-integrity',
|
|
775
823
|
planIntegrityEvidence: evidence,
|
|
@@ -854,22 +902,71 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
854
902
|
const attemptCount = remediationEvents.length;
|
|
855
903
|
const nextAttemptOrdinal = attemptCount + 1;
|
|
856
904
|
const guardedCategory = requireGuardedCategory(planRiskAudit, stepId);
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
905
|
+
const rollingCauseDecision = deriveRollingCauseDecision(remediationEvents);
|
|
906
|
+
const currentCorrectiveDecision = findCurrentCorrectiveDecision(task, stepId, nextAttemptOrdinal, correctiveDecisions);
|
|
907
|
+
const planIntegrityDecision = currentCorrectiveDecision;
|
|
908
|
+
if (planIntegrityDecision?.recoveryBasis === 'plan-integrity'
|
|
909
|
+
&& (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required')) {
|
|
910
|
+
if (planIntegrityDecision.planHash !== task.planHash) {
|
|
911
|
+
return {
|
|
912
|
+
stepId,
|
|
913
|
+
guardedCategory,
|
|
914
|
+
attemptCount,
|
|
915
|
+
nextAttemptOrdinal,
|
|
916
|
+
mode: 'corrective',
|
|
917
|
+
correctiveDecision: planIntegrityDecision,
|
|
918
|
+
posture: planIntegrityDecision.decision === 'replan-required' ? 'replan-ready' : 'continue-fix',
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (planIntegrityDecision.decision === 'continue-fix'
|
|
922
|
+
&& (planIntegrityDecision.planIntegrityEvidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
923
|
+
|| planIntegrityDecision.planIntegrityEvidence?.kind === 'failed-check-component-owner-outside-allowed-writes')) {
|
|
863
924
|
return {
|
|
864
925
|
stepId,
|
|
865
926
|
guardedCategory,
|
|
866
927
|
attemptCount,
|
|
867
928
|
nextAttemptOrdinal,
|
|
868
929
|
mode: 'ordinary',
|
|
869
|
-
correctiveDecision:
|
|
870
|
-
posture: '
|
|
930
|
+
correctiveDecision: planIntegrityDecision,
|
|
931
|
+
posture: 'continue-fix',
|
|
871
932
|
};
|
|
872
933
|
}
|
|
934
|
+
if (planIntegrityDecision.decision !== 'replan-required') {
|
|
935
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity decision has an unsupported disposition.', {
|
|
936
|
+
taskId: task.id,
|
|
937
|
+
stepId,
|
|
938
|
+
decisionEventId: planIntegrityDecision.eventId,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
stepId,
|
|
943
|
+
guardedCategory,
|
|
944
|
+
attemptCount,
|
|
945
|
+
nextAttemptOrdinal,
|
|
946
|
+
mode: 'ordinary',
|
|
947
|
+
correctiveDecision: planIntegrityDecision,
|
|
948
|
+
posture: 'replan-required',
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
// A recorded corrective decision remains authoritative over an ordinary
|
|
952
|
+
// rolling continuation. A repeated authoritative cause is fresh evidence
|
|
953
|
+
// that the current Plan itself is no longer viable and still forces replan.
|
|
954
|
+
if (rollingCauseDecision !== null
|
|
955
|
+
&& !(rollingCauseDecision === 'same-task-fix' && currentCorrectiveDecision)) {
|
|
956
|
+
if (rollingCauseDecision === 'root-cause-replan' && options.enforceRuntimeGate) {
|
|
957
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} repeated the same authoritative cause after a claimed fix and requires root-cause replan.`, { taskId: task.id, stepId, attemptCount, requiredAction: 'task plan-set' });
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
stepId,
|
|
961
|
+
guardedCategory,
|
|
962
|
+
attemptCount,
|
|
963
|
+
nextAttemptOrdinal,
|
|
964
|
+
mode: 'ordinary',
|
|
965
|
+
correctiveDecision: null,
|
|
966
|
+
posture: rollingCauseDecision === 'root-cause-replan' ? 'replan-required' : 'ordinary',
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (attemptCount < 2) {
|
|
873
970
|
return {
|
|
874
971
|
stepId,
|
|
875
972
|
guardedCategory,
|
|
@@ -880,7 +977,7 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
880
977
|
posture: 'ordinary',
|
|
881
978
|
};
|
|
882
979
|
}
|
|
883
|
-
const correctiveDecision =
|
|
980
|
+
const correctiveDecision = currentCorrectiveDecision;
|
|
884
981
|
if (!correctiveDecision) {
|
|
885
982
|
return {
|
|
886
983
|
stepId,
|
|
@@ -974,6 +1071,16 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
|
|
|
974
1071
|
posture: correctiveDecision.decision === 'continue-fix' ? 'continue-fix' : correctiveDecision.decision,
|
|
975
1072
|
};
|
|
976
1073
|
}
|
|
1074
|
+
export function deriveRollingCauseDecision(remediationEvents) {
|
|
1075
|
+
if (remediationEvents.length === 0 || remediationEvents.some((event) => event.causeBindings === undefined))
|
|
1076
|
+
return null;
|
|
1077
|
+
const latest = remediationEvents.at(-1);
|
|
1078
|
+
const priorFingerprints = new Set(remediationEvents.slice(0, -1)
|
|
1079
|
+
.flatMap((event) => event.causeBindings.map((cause) => cause.causeFingerprint)));
|
|
1080
|
+
const authoritativeRepeat = latest.causeBindings.some((cause) => priorFingerprints.has(cause.causeFingerprint)
|
|
1081
|
+
&& (cause.authority === 'mechanical' || (cause.authority === 'external-reviewer' && cause.planConflict)));
|
|
1082
|
+
return authoritativeRepeat ? 'root-cause-replan' : 'same-task-fix';
|
|
1083
|
+
}
|
|
977
1084
|
function findCurrentCorrectiveDecision(task, stepId, triggeringAttemptOrdinal, events) {
|
|
978
1085
|
const matches = events.filter((event) => event.taskId === task.id
|
|
979
1086
|
&& event.stepId === stepId
|
|
@@ -1068,6 +1175,32 @@ function validateStoredRemediationEvent(event, task, file, line) {
|
|
|
1068
1175
|
if (event.failureEvidence.reviewerAttestationHash !== null) {
|
|
1069
1176
|
assertHash64(event.failureEvidence.reviewerAttestationHash, `Remediation failure evidence reviewerAttestationHash at ${file}:${line}`);
|
|
1070
1177
|
}
|
|
1178
|
+
const hasExecutionRevision = event.failureEvidence.executionTaskRevision !== undefined;
|
|
1179
|
+
const hasDirtyWorktreeHash = event.failureEvidence.dirtyWorktreeHash !== undefined;
|
|
1180
|
+
if (hasExecutionRevision !== hasDirtyWorktreeHash) {
|
|
1181
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation execution revision and dirty-worktree hash must be recorded together.', {
|
|
1182
|
+
file,
|
|
1183
|
+
line,
|
|
1184
|
+
taskId: task.id,
|
|
1185
|
+
stepId: event.stepId,
|
|
1186
|
+
eventId: event.eventId,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
if (hasExecutionRevision
|
|
1190
|
+
&& (!Number.isInteger(event.failureEvidence.executionTaskRevision)
|
|
1191
|
+
|| event.failureEvidence.executionTaskRevision <= 0)) {
|
|
1192
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation execution task revision must be a positive integer.', {
|
|
1193
|
+
file,
|
|
1194
|
+
line,
|
|
1195
|
+
taskId: task.id,
|
|
1196
|
+
stepId: event.stepId,
|
|
1197
|
+
eventId: event.eventId,
|
|
1198
|
+
executionTaskRevision: event.failureEvidence.executionTaskRevision,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (hasDirtyWorktreeHash) {
|
|
1202
|
+
assertHash64(event.failureEvidence.dirtyWorktreeHash, `Remediation dirty-worktree hash at ${file}:${line}`);
|
|
1203
|
+
}
|
|
1071
1204
|
if (!Number.isInteger(event.attemptOrdinal) || event.attemptOrdinal <= 0) {
|
|
1072
1205
|
throw new WorkflowError('STATE_CORRUPT', 'Remediation event attemptOrdinal must be a positive integer.', {
|
|
1073
1206
|
file,
|
|
@@ -1077,8 +1210,33 @@ function validateStoredRemediationEvent(event, task, file, line) {
|
|
|
1077
1210
|
attemptOrdinal: event.attemptOrdinal,
|
|
1078
1211
|
});
|
|
1079
1212
|
}
|
|
1213
|
+
if (event.causeBindings !== undefined) {
|
|
1214
|
+
const fingerprints = new Set();
|
|
1215
|
+
for (const cause of event.causeBindings) {
|
|
1216
|
+
if (!['mechanical', 'external-reviewer'].includes(cause.authority)
|
|
1217
|
+
|| !cause.causeId?.trim()
|
|
1218
|
+
|| !/^[a-f0-9]{64}$/.test(cause.causeFingerprint)
|
|
1219
|
+
|| typeof cause.planConflict !== 'boolean'
|
|
1220
|
+
|| (cause.authority === 'mechanical'
|
|
1221
|
+
&& (cause.planConflict || cause.causeId !== `mechanical:${cause.causeFingerprint}`))
|
|
1222
|
+
|| (cause.authority === 'external-reviewer'
|
|
1223
|
+
&& reviewerCauseFingerprint(cause.causeId) !== cause.causeFingerprint)) {
|
|
1224
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation cause binding is invalid.', {
|
|
1225
|
+
file, line, eventId: event.eventId,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (fingerprints.has(cause.causeFingerprint)) {
|
|
1229
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation cause fingerprints must be unique per event.', {
|
|
1230
|
+
file, line, eventId: event.eventId,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
fingerprints.add(cause.causeFingerprint);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1080
1236
|
if (event.failureKind === 'checks-failed') {
|
|
1081
|
-
if (event.failureEvidence.reviewEventHash !== null
|
|
1237
|
+
if (event.failureEvidence.reviewEventHash !== null
|
|
1238
|
+
|| event.failureEvidence.reviewerAttestationHash !== null
|
|
1239
|
+
|| event.causeBindings?.some((cause) => cause.authority !== 'mechanical')) {
|
|
1082
1240
|
throw new WorkflowError('STATE_CORRUPT', 'checks-failed remediation events must not reference strict review sidecars.', {
|
|
1083
1241
|
file,
|
|
1084
1242
|
line,
|
|
@@ -1087,8 +1245,21 @@ function validateStoredRemediationEvent(event, task, file, line) {
|
|
|
1087
1245
|
eventId: event.eventId,
|
|
1088
1246
|
});
|
|
1089
1247
|
}
|
|
1248
|
+
if (event.causeBindings !== undefined && (!hasExecutionRevision || !hasDirtyWorktreeHash)) {
|
|
1249
|
+
throw new WorkflowError('STATE_CORRUPT', 'Native checks-failed remediation events must bind the declared execution and dirty product bytes.', {
|
|
1250
|
+
file,
|
|
1251
|
+
line,
|
|
1252
|
+
taskId: task.id,
|
|
1253
|
+
stepId: event.stepId,
|
|
1254
|
+
eventId: event.eventId,
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1090
1257
|
}
|
|
1091
|
-
else if (event.failureEvidence.reviewEventHash === null
|
|
1258
|
+
else if (event.failureEvidence.reviewEventHash === null
|
|
1259
|
+
|| event.failureEvidence.reviewerAttestationHash === null
|
|
1260
|
+
|| event.causeBindings?.some((cause) => cause.authority !== 'external-reviewer')
|
|
1261
|
+
|| hasExecutionRevision
|
|
1262
|
+
|| hasDirtyWorktreeHash) {
|
|
1092
1263
|
throw new WorkflowError('STATE_CORRUPT', 'review-failed and review-unverified remediation events must reference both strict review sidecars.', {
|
|
1093
1264
|
file,
|
|
1094
1265
|
line,
|
|
@@ -1139,16 +1310,12 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
|
|
|
1139
1310
|
normalizeStringList(event.coveredFindingIds, 'Corrective decision coveredFindingIds');
|
|
1140
1311
|
normalizeStringList(event.coveredEventIds, 'Corrective decision coveredEventIds');
|
|
1141
1312
|
assertHash64(event.planHash, `Corrective decision planHash at ${file}:${line}`);
|
|
1142
|
-
if (event.
|
|
1313
|
+
if (event.recoveryBasis === 'plan-integrity' || event.planIntegrityEvidence !== undefined) {
|
|
1143
1314
|
validatePlanIntegrityDecision(event, task, file, line);
|
|
1144
1315
|
}
|
|
1145
|
-
else if (event.
|
|
1146
|
-
throw new WorkflowError('STATE_CORRUPT', '
|
|
1147
|
-
file,
|
|
1148
|
-
line,
|
|
1149
|
-
taskId: task.id,
|
|
1150
|
-
stepId: event.stepId,
|
|
1151
|
-
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
1316
|
+
else if (event.triggeringAttemptCount === 2) {
|
|
1317
|
+
throw new WorkflowError('STATE_CORRUPT', 'A second-attempt corrective decision requires bounded Plan-integrity evidence.', {
|
|
1318
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1152
1319
|
});
|
|
1153
1320
|
}
|
|
1154
1321
|
}
|
|
@@ -1282,43 +1449,173 @@ function stopEscalateOverrideBindingHash(input) {
|
|
|
1282
1449
|
function validatePlanIntegrityDecision(event, task, file, line) {
|
|
1283
1450
|
const evidence = event.planIntegrityEvidence;
|
|
1284
1451
|
if (event.recoveryBasis !== 'plan-integrity'
|
|
1285
|
-
|| event.decision !== 'replan-required'
|
|
1286
1452
|
|| !evidence
|
|
1287
|
-
|| evidence.kind !== '
|
|
1288
|
-
|
|
1453
|
+
|| (evidence.kind !== 'failed-check-component-owner-outside-allowed-writes'
|
|
1454
|
+
&& evidence.manifestPath !== 'package.json')
|
|
1289
1455
|
|| !Number.isInteger(evidence.taskRevision)
|
|
1290
1456
|
|| evidence.taskRevision < 1
|
|
1291
1457
|
|| evidence.taskRevision > task.revision
|
|
1292
|
-
|| typeof evidence.remediationEventId !== 'string'
|
|
1293
|
-
|| !evidence.remediationEventId
|
|
1294
1458
|
|| !/^[a-f0-9]{40,64}$/.test(evidence.gitHead)) {
|
|
1295
|
-
throw new WorkflowError('STATE_CORRUPT', '
|
|
1459
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective decision is not a valid bounded Plan-integrity recovery.', {
|
|
1296
1460
|
file,
|
|
1297
1461
|
line,
|
|
1298
1462
|
taskId: task.id,
|
|
1299
1463
|
stepId: event.stepId,
|
|
1300
1464
|
});
|
|
1301
1465
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1466
|
+
if (evidence.kind !== 'failed-check-component-owner-outside-allowed-writes'
|
|
1467
|
+
&& evidence.manifestHash !== null) {
|
|
1304
1468
|
assertHash64(evidence.manifestHash, `Plan-integrity manifestHash at ${file}:${line}`);
|
|
1305
1469
|
}
|
|
1306
1470
|
assertHash64(evidence.stepDefinitionHash, `Plan-integrity stepDefinitionHash at ${file}:${line}`);
|
|
1307
1471
|
assertHash64(evidence.conflictHash, `Plan-integrity conflictHash at ${file}:${line}`);
|
|
1308
|
-
const missingScripts = normalizeStringList(evidence.missingScripts, 'Plan-integrity missingScripts');
|
|
1309
|
-
const checkCommands = normalizeStringList(evidence.checkCommands, 'Plan-integrity checkCommands');
|
|
1310
1472
|
const changedFiles = normalizeStringList(evidence.changedFiles, 'Plan-integrity changedFiles');
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1473
|
+
let domain;
|
|
1474
|
+
if (evidence.kind === 'missing-npm-script-outside-allowed-writes') {
|
|
1475
|
+
if (event.decision !== 'replan-required'
|
|
1476
|
+
|| event.triggeringAttemptCount !== 2
|
|
1477
|
+
|| typeof evidence.remediationEventId !== 'string'
|
|
1478
|
+
|| !evidence.remediationEventId) {
|
|
1479
|
+
throw new WorkflowError('STATE_CORRUPT', 'Legacy Plan-integrity evidence is valid only for the exact second-attempt boundary.', {
|
|
1480
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
assertHash64(evidence.remediationEventHash, `Plan-integrity remediationEventHash at ${file}:${line}`);
|
|
1484
|
+
const missingScripts = normalizeStringList(evidence.missingScripts, 'Plan-integrity missingScripts');
|
|
1485
|
+
const checkCommands = normalizeStringList(evidence.checkCommands, 'Plan-integrity checkCommands');
|
|
1486
|
+
if (missingScripts.length === 0 || checkCommands.length === 0) {
|
|
1487
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity evidence requires at least one missing script and check command.', {
|
|
1488
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
domain = 'codex-workflow/plan-integrity-conflict/v1';
|
|
1492
|
+
}
|
|
1493
|
+
else if (evidence.kind === 'non-executable-npm-check-outside-allowed-writes') {
|
|
1494
|
+
if (event.decision !== 'replan-required') {
|
|
1495
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v2 evidence requires replan-required.', {
|
|
1496
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (evidence.remediationEvents.length === 0
|
|
1500
|
+
|| event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
|
|
1501
|
+
|| evidence.remediationEvents.some((binding, index) => {
|
|
1502
|
+
if (!binding.eventId || binding.attemptOrdinal !== index + 1)
|
|
1503
|
+
return true;
|
|
1504
|
+
assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
|
|
1505
|
+
return false;
|
|
1506
|
+
})
|
|
1507
|
+
|| evidence.conflicts.length === 0
|
|
1508
|
+
|| evidence.conflicts.some((conflict) => {
|
|
1509
|
+
if (!['MISSING_NPM_SCRIPT', 'REQUIRED_SELECTOR_MISSING'].includes(conflict.reasonCode)
|
|
1510
|
+
|| !conflict.script.trim()
|
|
1511
|
+
|| !conflict.command.trim())
|
|
1512
|
+
return true;
|
|
1513
|
+
if (conflict.runnerHash !== null) {
|
|
1514
|
+
assertHash64(conflict.runnerHash, `Plan-integrity runnerHash at ${file}:${line}`);
|
|
1515
|
+
}
|
|
1516
|
+
return conflict.reasonCode === 'MISSING_NPM_SCRIPT'
|
|
1517
|
+
? conflict.scriptCommand !== null || conflict.runnerPath !== null || conflict.runnerHash !== null
|
|
1518
|
+
: conflict.scriptCommand === null || conflict.runnerPath === null || conflict.runnerHash === null;
|
|
1519
|
+
})) {
|
|
1520
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v2 evidence has invalid remediation or command-conflict bindings.', {
|
|
1521
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
domain = 'codex-workflow/plan-integrity-conflict/v2';
|
|
1525
|
+
}
|
|
1526
|
+
else if (evidence.kind === 'check-support-anchor-outside-allowed-writes') {
|
|
1527
|
+
const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
|
|
1528
|
+
const supportPaths = [...new Set(evidence.conflicts.map((conflict) => conflict.supportPath))].sort();
|
|
1529
|
+
const validateCurrentPlanBinding = event.planHash === task.planHash;
|
|
1530
|
+
if (event.decision !== 'continue-fix'
|
|
1531
|
+
|| evidence.remediationEvents.length === 0
|
|
1532
|
+
|| event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
|
|
1533
|
+
|| evidence.remediationEvents.some((binding, index) => {
|
|
1534
|
+
if (!binding.eventId || binding.attemptOrdinal !== index + 1)
|
|
1535
|
+
return true;
|
|
1536
|
+
assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
|
|
1537
|
+
return false;
|
|
1538
|
+
})
|
|
1539
|
+
|| evidence.conflicts.length === 0
|
|
1540
|
+
|| !sameStringArray(evidence.scopeExtensions, supportPaths)
|
|
1541
|
+
|| (validateCurrentPlanBinding && (!step
|
|
1542
|
+
|| evidence.scopeExtensions.some((scopePath) => pathAllowed(scopePath, step.allowedWrites)
|
|
1543
|
+
|| pathAllowed(scopePath, step.forbiddenScope))
|
|
1544
|
+
|| changedFiles.some((changedPath) => !pathAllowed(changedPath, step.allowedWrites))))
|
|
1545
|
+
|| evidence.conflicts.some((conflict) => {
|
|
1546
|
+
assertHash64(conflict.runnerHash, `Plan-integrity runnerHash at ${file}:${line}`);
|
|
1547
|
+
assertHash64(conflict.supportHash, `Plan-integrity supportHash at ${file}:${line}`);
|
|
1548
|
+
return conflict.reasonCode !== 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES'
|
|
1549
|
+
|| !conflict.script.trim()
|
|
1550
|
+
|| !conflict.command.trim()
|
|
1551
|
+
|| !conflict.scriptCommand.trim()
|
|
1552
|
+
|| !conflict.runnerPath.trim()
|
|
1553
|
+
|| !conflict.supportPath.trim()
|
|
1554
|
+
|| !conflict.importSpecifier.startsWith('.')
|
|
1555
|
+
|| normalizeStringList(conflict.expectedOutputPaths, 'Plan-integrity expectedOutputPaths').length === 0
|
|
1556
|
+
|| normalizeStringList(conflict.anchoredMigrationNames, 'Plan-integrity anchoredMigrationNames').length === 0
|
|
1557
|
+
|| !evidence.scopeExtensions.includes(conflict.supportPath)
|
|
1558
|
+
|| (validateCurrentPlanBinding && (!step || conflict.expectedOutputPaths.some((output) => !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === output)
|
|
1559
|
+
|| !pathAllowed(output, step.allowedWrites))));
|
|
1560
|
+
})) {
|
|
1561
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v3 evidence has invalid check-support bindings.', {
|
|
1562
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
domain = 'codex-workflow/plan-integrity-conflict/v3';
|
|
1566
|
+
}
|
|
1567
|
+
else if (evidence.kind === 'failed-check-component-owner-outside-allowed-writes') {
|
|
1568
|
+
const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
|
|
1569
|
+
const ownerPaths = [...new Set(evidence.conflicts.map((conflict) => conflict.ownerPath))].sort();
|
|
1570
|
+
const validateCurrentPlanBinding = event.planHash === task.planHash;
|
|
1571
|
+
assertHash64(evidence.semanticBoundaryHash, `Plan-integrity semanticBoundaryHash at ${file}:${line}`);
|
|
1572
|
+
assertHash64(evidence.failedCheckCauseFingerprint, `Plan-integrity failedCheckCauseFingerprint at ${file}:${line}`);
|
|
1573
|
+
if (event.decision !== 'continue-fix'
|
|
1574
|
+
|| evidence.remediationEvents.length === 0
|
|
1575
|
+
|| event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
|
|
1576
|
+
|| evidence.remediationEvents.some((binding, index) => {
|
|
1577
|
+
if (!binding.eventId || binding.attemptOrdinal !== index + 1)
|
|
1578
|
+
return true;
|
|
1579
|
+
assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
|
|
1580
|
+
return false;
|
|
1581
|
+
})
|
|
1582
|
+
|| evidence.conflicts.length === 0
|
|
1583
|
+
|| !sameStringArray(evidence.scopeExtensions, ownerPaths)
|
|
1584
|
+
|| (validateCurrentPlanBinding && (!step
|
|
1585
|
+
|| evidence.semanticBoundaryHash !== semanticBoundaryHash(task)
|
|
1586
|
+
|| evidence.scopeExtensions.some((scopePath) => pathAllowed(scopePath, step.allowedWrites)
|
|
1587
|
+
|| pathAllowed(scopePath, step.forbiddenScope))
|
|
1588
|
+
|| changedFiles.some((changedPath) => !pathAllowed(changedPath, step.allowedWrites))))
|
|
1589
|
+
|| evidence.conflicts.some((conflict) => {
|
|
1590
|
+
assertHash64(conflict.ownerHash, `Plan-integrity component owner hash at ${file}:${line}`);
|
|
1591
|
+
const importChain = normalizeStringList(conflict.importChain, 'Plan-integrity importChain');
|
|
1592
|
+
return conflict.reasonCode !== 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES'
|
|
1593
|
+
|| !conflict.command.trim()
|
|
1594
|
+
|| !conflict.runnerPath.trim()
|
|
1595
|
+
|| !conflict.ownerPath.trim()
|
|
1596
|
+
|| conflict.diagnosticPath !== conflict.ownerPath
|
|
1597
|
+
|| !conflict.componentRoot.trim()
|
|
1598
|
+
|| !(conflict.ownerPath === conflict.componentRoot
|
|
1599
|
+
|| conflict.ownerPath.startsWith(`${conflict.componentRoot}/`))
|
|
1600
|
+
|| importChain.length < 2
|
|
1601
|
+
|| importChain[0] !== conflict.runnerPath
|
|
1602
|
+
|| importChain.at(-1) !== conflict.ownerPath
|
|
1603
|
+
|| !evidence.scopeExtensions.includes(conflict.ownerPath);
|
|
1604
|
+
})) {
|
|
1605
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v4 evidence has invalid component-owner bindings.', {
|
|
1606
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
domain = 'codex-workflow/plan-integrity-conflict/v4';
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity evidence kind is unsupported.', {
|
|
1613
|
+
file, line, taskId: task.id, stepId: event.stepId,
|
|
1317
1614
|
});
|
|
1318
1615
|
}
|
|
1319
1616
|
const { conflictHash: _conflictHash, ...evidenceBase } = evidence;
|
|
1320
1617
|
const expectedConflictHash = hashCanonical({
|
|
1321
|
-
domain
|
|
1618
|
+
domain,
|
|
1322
1619
|
taskId: task.id,
|
|
1323
1620
|
planHash: event.planHash,
|
|
1324
1621
|
stepId: event.stepId,
|
|
@@ -1338,6 +1635,21 @@ function validatePlanIntegrityDecision(event, task, file, line) {
|
|
|
1338
1635
|
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity changedFiles must be normalized.', { file, line });
|
|
1339
1636
|
}
|
|
1340
1637
|
}
|
|
1638
|
+
export function effectiveStepAllowedWrites(store, task, stepId) {
|
|
1639
|
+
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
1640
|
+
if (!step)
|
|
1641
|
+
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
1642
|
+
const extensions = readCorrectiveDecisionEvents(store, task)
|
|
1643
|
+
.filter((event) => event.stepId === stepId
|
|
1644
|
+
&& event.planHash === task.planHash
|
|
1645
|
+
&& event.decision === 'continue-fix'
|
|
1646
|
+
&& event.recoveryBasis === 'plan-integrity')
|
|
1647
|
+
.flatMap((event) => event.planIntegrityEvidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
1648
|
+
|| event.planIntegrityEvidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
1649
|
+
? event.planIntegrityEvidence.scopeExtensions
|
|
1650
|
+
: []);
|
|
1651
|
+
return [...step.allowedWrites, ...new Set(extensions.filter((value) => !pathAllowed(value, step.allowedWrites)).sort())];
|
|
1652
|
+
}
|
|
1341
1653
|
function validateStoredCorrectiveDecisionRecoveryReference(event, task, file, line) {
|
|
1342
1654
|
if (!/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)
|
|
1343
1655
|
|| event.taskId !== task.id
|
|
@@ -1583,6 +1895,20 @@ function validateRemediationReviewReferences(store, task, events) {
|
|
|
1583
1895
|
reviewDecision: reviewEvent.decision,
|
|
1584
1896
|
});
|
|
1585
1897
|
}
|
|
1898
|
+
const expectedCauses = (reviewEvent.remediationCauses ?? []).map((cause) => ({
|
|
1899
|
+
authority: 'external-reviewer',
|
|
1900
|
+
causeId: cause.causeId,
|
|
1901
|
+
causeFingerprint: cause.causeFingerprint,
|
|
1902
|
+
planConflict: cause.planConflict,
|
|
1903
|
+
}));
|
|
1904
|
+
if (canonicalJsonStringify(event.causeBindings ?? []) !== canonicalJsonStringify(expectedCauses)) {
|
|
1905
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation reviewer causes do not match the exact strict review event.', {
|
|
1906
|
+
taskId: task.id,
|
|
1907
|
+
stepId: event.stepId,
|
|
1908
|
+
eventId: event.eventId,
|
|
1909
|
+
reviewEventId: reviewEvent.eventId,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1586
1912
|
}
|
|
1587
1913
|
}
|
|
1588
1914
|
function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents) {
|
|
@@ -1631,16 +1957,27 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
|
|
|
1631
1957
|
});
|
|
1632
1958
|
}
|
|
1633
1959
|
}
|
|
1634
|
-
if (decision.
|
|
1960
|
+
if (decision.recoveryBasis === 'plan-integrity') {
|
|
1635
1961
|
const evidence = decision.planIntegrityEvidence;
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
||
|
|
1641
|
-
||
|
|
1642
|
-
|
|
1643
|
-
|
|
1962
|
+
const bindings = evidence?.kind === 'missing-npm-script-outside-allowed-writes'
|
|
1963
|
+
? [{ eventId: evidence.remediationEventId, eventHash: evidence.remediationEventHash, attemptOrdinal: 1 }]
|
|
1964
|
+
: evidence?.remediationEvents ?? [];
|
|
1965
|
+
if (!evidence
|
|
1966
|
+
|| bindings.length !== priorEvents.length
|
|
1967
|
+
|| priorEvents.some((linked, index) => linked.failureKind !== 'checks-failed'
|
|
1968
|
+
|| linked.eventId !== bindings[index]?.eventId
|
|
1969
|
+
|| linked.eventHash !== bindings[index]?.eventHash
|
|
1970
|
+
|| linked.attemptOrdinal !== bindings[index]?.attemptOrdinal)) {
|
|
1971
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity recovery must bind the exact checks-failed remediation history.', {
|
|
1972
|
+
taskId: task.id,
|
|
1973
|
+
stepId: decision.stepId,
|
|
1974
|
+
decisionEventId: decision.eventId,
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
if (evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
1978
|
+
&& !priorEvents.some((linked) => linked.causeBindings?.some((cause) => cause.authority === 'mechanical'
|
|
1979
|
+
&& cause.causeFingerprint === evidence.failedCheckCauseFingerprint))) {
|
|
1980
|
+
throw new WorkflowError('STATE_CORRUPT', 'Component-owner recovery must bind a machine-derived failed-check cause.', {
|
|
1644
1981
|
taskId: task.id,
|
|
1645
1982
|
stepId: decision.stepId,
|
|
1646
1983
|
decisionEventId: decision.eventId,
|
|
@@ -1650,6 +1987,21 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
|
|
|
1650
1987
|
}
|
|
1651
1988
|
}
|
|
1652
1989
|
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds, overriddenStopDecisionEventIds) {
|
|
1990
|
+
const causeAwareHistory = remediationEvents.length > 0
|
|
1991
|
+
&& remediationEvents.every((event) => event.causeBindings !== undefined);
|
|
1992
|
+
if (causeAwareHistory) {
|
|
1993
|
+
for (let index = 0; index < remediationEvents.length; index += 1) {
|
|
1994
|
+
const prefix = remediationEvents.slice(0, index + 1);
|
|
1995
|
+
if (deriveRollingCauseDecision(prefix) === 'root-cause-replan' && index < remediationEvents.length - 1) {
|
|
1996
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation continued after an authoritative repeated cause required replan.', {
|
|
1997
|
+
taskId: task.id,
|
|
1998
|
+
stepId,
|
|
1999
|
+
eventId: remediationEvents[index].eventId,
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
1653
2005
|
for (const event of remediationEvents) {
|
|
1654
2006
|
if (event.attemptOrdinal <= 2) {
|
|
1655
2007
|
if (event.mode !== 'ordinary') {
|
|
@@ -1728,13 +2080,24 @@ function normalizeStringList(values, label) {
|
|
|
1728
2080
|
return normalized;
|
|
1729
2081
|
}
|
|
1730
2082
|
function remediationEventMatchesFailure(event, failureKind, failureEvidence) {
|
|
1731
|
-
|
|
1732
|
-
|
|
2083
|
+
if (event.failureKind !== failureKind)
|
|
2084
|
+
return false;
|
|
2085
|
+
const eventExecutionRevision = event.failureEvidence.executionTaskRevision;
|
|
2086
|
+
const candidateExecutionRevision = failureEvidence.executionTaskRevision;
|
|
2087
|
+
if (eventExecutionRevision !== undefined || candidateExecutionRevision !== undefined) {
|
|
2088
|
+
return eventExecutionRevision !== undefined
|
|
2089
|
+
&& candidateExecutionRevision !== undefined
|
|
2090
|
+
&& eventExecutionRevision === candidateExecutionRevision;
|
|
2091
|
+
}
|
|
2092
|
+
return event.failureEvidence.completionCommit === failureEvidence.completionCommit
|
|
1733
2093
|
&& event.failureEvidence.checksHash === failureEvidence.checksHash
|
|
1734
2094
|
&& event.failureEvidence.reviewEventHash === failureEvidence.reviewEventHash
|
|
1735
2095
|
&& event.failureEvidence.reviewerAttestationHash === failureEvidence.reviewerAttestationHash;
|
|
1736
2096
|
}
|
|
1737
2097
|
function remediationFailureIdentityKey(failureKind, failureEvidence) {
|
|
2098
|
+
if (failureEvidence.executionTaskRevision !== undefined) {
|
|
2099
|
+
return ['execution', failureKind, String(failureEvidence.executionTaskRevision)].join('\0');
|
|
2100
|
+
}
|
|
1738
2101
|
return [
|
|
1739
2102
|
failureKind,
|
|
1740
2103
|
failureEvidence.completionCommit,
|