codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14.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 +27 -388
- package/dist/reviewer-runtime-build.json +98 -34
- package/dist/src/alpha6/adoption.d.ts +3 -1
- package/dist/src/alpha6/adoption.js +48 -8
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
- package/dist/src/alpha6/captured-check-evidence.js +152 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +15 -1
- package/dist/src/alpha6/component-owner.js +44 -2
- package/dist/src/alpha6/component-owner.js.map +1 -1
- package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
- package/dist/src/alpha6/corrective-decision-boundary.js +53 -0
- package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.d.ts +1 -25
- package/dist/src/alpha6/downstream-proof.js +0 -171
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
- package/dist/src/alpha6/literal-test-invocation.js +106 -0
- package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +2 -0
- package/dist/src/alpha6/milestone.js +10 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +7 -5
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +8 -21
- package/dist/src/alpha6/remediation.js +225 -474
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/alpha7/autonomy.d.ts +9 -0
- package/dist/src/alpha7/autonomy.js +40 -1
- package/dist/src/alpha7/autonomy.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +20 -6
- package/dist/src/beta1/project-transaction.js.map +1 -1
- package/dist/src/checks/runner.d.ts +26 -0
- package/dist/src/checks/runner.js +223 -0
- package/dist/src/checks/runner.js.map +1 -0
- package/dist/src/checks/task-sync.d.ts +10 -0
- package/dist/src/checks/task-sync.js +41 -0
- package/dist/src/checks/task-sync.js.map +1 -0
- package/dist/src/checks/worker.d.ts +1 -0
- package/dist/src/checks/worker.js +154 -0
- package/dist/src/checks/worker.js.map +1 -0
- package/dist/src/cli-actions.d.ts +3 -3
- package/dist/src/cli-actions.js +8 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +43 -97
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +54 -34
- package/dist/src/delegation.d.ts +5 -1
- package/dist/src/delegation.js +4 -0
- package/dist/src/delegation.js.map +1 -1
- package/dist/src/dependency-provenance.d.ts +2 -2
- package/dist/src/dependency-provenance.js +12 -69
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/base-sync-conflict.d.ts +13 -0
- package/dist/src/domain/base-sync-conflict.js +32 -0
- package/dist/src/domain/base-sync-conflict.js.map +1 -0
- package/dist/src/domain/step-start-admission.d.ts +4 -0
- package/dist/src/domain/step-start-admission.js +9 -0
- package/dist/src/domain/step-start-admission.js.map +1 -0
- package/dist/src/domain/virgin-registration.d.ts +3 -0
- package/dist/src/domain/virgin-registration.js +65 -0
- package/dist/src/domain/virgin-registration.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/failed-checkpoint-update.d.ts +69 -0
- package/dist/src/failed-checkpoint-update.js +171 -0
- package/dist/src/failed-checkpoint-update.js.map +1 -0
- package/dist/src/gateway-handshake.js +0 -8
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/git.d.ts +3 -1
- package/dist/src/git.js +41 -14
- package/dist/src/git.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +2 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.js +45 -9
- package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +12 -8
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/lifecycle/semantic-registry.js +4 -2
- package/dist/src/lifecycle/semantic-registry.js.map +1 -1
- package/dist/src/navigation-actions.d.ts +9 -0
- package/dist/src/navigation-actions.js +72 -0
- package/dist/src/navigation-actions.js.map +1 -0
- package/dist/src/navigation-update-artifact.d.ts +5 -0
- package/dist/src/navigation-update-artifact.js +227 -0
- package/dist/src/navigation-update-artifact.js.map +1 -0
- package/dist/src/navigation-update.d.ts +39 -0
- package/dist/src/navigation-update.js +82 -0
- package/dist/src/navigation-update.js.map +1 -0
- package/dist/src/observation.js +52 -29
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.js +10 -6
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +0 -13
- package/dist/src/pending-review-update.js +1 -6
- package/dist/src/pending-review-update.js.map +1 -1
- package/dist/src/reviewer.d.ts +1 -1
- package/dist/src/reviewer.js +20 -17
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.d.ts +10 -0
- package/dist/src/state/corrective-replan-executor.js +38 -1
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public-schema.js +15 -2
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
- package/dist/src/state/corrective-replan-public.d.ts +9 -1
- package/dist/src/state/corrective-replan-public.js +50 -8
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
- package/dist/src/state/corrective-replan-transaction.js +12 -11
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
- package/dist/src/state/corrective-yield-transaction.js +11 -10
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +4 -0
- package/dist/src/state/lock.js +21 -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/workflow-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +88 -64
- package/dist/src/workflow.js +832 -1456
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +26 -304
- package/docs/decisions.md +14 -112
- package/docs/development-flow.md +36 -237
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +101 -388
- package/docs/updating-existing-project.md +65 -726
- package/package.json +11 -13
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +88 -200
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +51 -458
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +20 -4
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +21 -34
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +92 -778
- package/references/state-machine.md +7 -5
- package/roles/delivery-coordinator.md +31 -11
- package/roles/scope-lead.md +1 -1
- package/roles/technical-planner.md +5 -2
- package/schemas/adoption-posture-event.schema.json +11 -0
- package/schemas/authorization-event.schema.json +1 -1
- package/schemas/corrective-decision-event.schema.json +26 -4
- package/schemas/corrective-replan-credentials.private.schema.json +8 -5
- package/schemas/corrective-replan-execution-event.private.schema.json +9 -0
- package/schemas/corrective-replan-public.schema.json +2 -1
- package/schemas/delegation-grant.schema.json +1 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +64 -2
- package/schemas/transition-definition.schema.json +4 -0
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1146 -0
- package/src/alpha6/captured-check-evidence.ts +132 -0
- package/src/alpha6/check-support-anchor.ts +128 -0
- package/src/alpha6/component-owner.ts +168 -0
- package/src/alpha6/corrective-decision-boundary.ts +45 -0
- package/src/alpha6/downstream-proof.ts +520 -0
- package/src/alpha6/failed-step-planning-recovery.ts +88 -0
- package/src/alpha6/handoff.ts +1443 -0
- package/src/alpha6/journal.ts +473 -0
- package/src/alpha6/literal-test-invocation.ts +84 -0
- package/src/alpha6/mechanical-feasibility.ts +488 -0
- package/src/alpha6/milestone.ts +2192 -0
- package/src/alpha6/navigation-dirty-carryover.ts +154 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity.ts +298 -0
- package/src/alpha6/plan-risk.ts +1480 -0
- package/src/alpha6/preexecution-replan.ts +187 -0
- package/src/alpha6/remediation-cause.ts +98 -0
- package/src/alpha6/remediation.ts +2505 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +491 -0
- package/src/alpha6/store-sidecars.ts +335 -0
- package/src/alpha7/autonomy.ts +449 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +364 -0
- package/src/change-explanation.ts +153 -0
- package/src/checks/runner.ts +245 -0
- package/src/checks/task-sync.ts +41 -0
- package/src/checks/worker.ts +149 -0
- package/src/cli-actions.ts +112 -0
- package/src/cli.ts +1490 -0
- package/src/contracts.ts +1512 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +194 -0
- package/src/dependency-provenance.ts +472 -0
- package/src/diagnostics.ts +93 -0
- package/src/domain/base-sync-conflict.ts +35 -0
- package/src/domain/completed-step-carryover.ts +76 -0
- package/src/domain/discovery.ts +27 -0
- package/src/domain/plan-semantics.ts +58 -0
- package/src/domain/step-start-admission.ts +10 -0
- package/src/domain/validation.ts +177 -0
- package/src/domain/virgin-registration.ts +46 -0
- package/src/errors.ts +24 -0
- package/src/failed-checkpoint-update.ts +203 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +95 -0
- package/src/git.ts +183 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +23 -0
- package/src/lifecycle/canonical-hash.ts +28 -0
- package/src/lifecycle/catalog.ts +202 -0
- package/src/lifecycle/compiler-inspection.ts +29 -0
- package/src/lifecycle/core-static-readiness.ts +132 -0
- package/src/lifecycle/corrective-replan-authority.ts +136 -0
- package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
- package/src/lifecycle/corrective-replan-credential-core.ts +437 -0
- package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
- package/src/lifecycle/corrective-replan-credentials.ts +48 -0
- package/src/lifecycle/corrective-replan.ts +847 -0
- package/src/lifecycle/evaluator.ts +48 -0
- package/src/lifecycle/fingerprint.ts +488 -0
- package/src/lifecycle/immutable.ts +8 -0
- package/src/lifecycle/implementation-table.ts +118 -0
- package/src/lifecycle/index.ts +8 -0
- package/src/lifecycle/schema-artifact.ts +263 -0
- package/src/lifecycle/semantic-registry.ts +574 -0
- package/src/lifecycle/types.ts +838 -0
- package/src/memory.ts +273 -0
- package/src/migration.ts +161 -0
- package/src/navigation-actions.ts +70 -0
- package/src/navigation-update-artifact.ts +198 -0
- package/src/navigation-update.ts +121 -0
- package/src/observation.ts +225 -0
- package/src/observed-routes.ts +665 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +175 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1879 -0
- package/src/runtime.ts +6 -0
- package/src/state/corrective-replan-executor.ts +819 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +952 -0
- package/src/state/corrective-replan-transaction.ts +949 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +730 -0
- package/src/state/lock.ts +902 -0
- package/src/state/store.ts +567 -0
- package/src/transition-core.ts +330 -0
- package/src/ulid.ts +24 -0
- package/src/version.ts +2 -0
- package/src/workflow-blocker-route.ts +109 -0
- package/src/workflow.ts +10342 -0
- package/docs/alpha7.1-implementation-brief.md +0 -268
- package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
- package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
- package/docs/beta1-stabilization-brief.md +0 -165
- package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
- package/docs/beta13.2-signal-review-recovery.md +0 -38
- package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
- package/docs/change-model.md +0 -118
- package/docs/delegated-approval.md +0 -254
- package/docs/lifecycle/state-machine-stabilization.md +0 -641
- package/docs/pdf/README.md +0 -24
- 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/requirements.txt +0 -1
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
- package/docs/pending-review-update.md +0 -15
- package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
- package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
- package/docs/problem-briefs/README.md +0 -120
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
- package/docs/split-required-recovery.md +0 -47
- package/docs/stable-release-defect-register.md +0 -730
- package/docs/validation-report.md +0 -182
- package/scripts/generate-pdf-docs.py +0 -524
- package/scripts/run-pdf-docs.mjs +0 -62
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from '../domain/plan-semantics.js';
|
|
3
|
+
import { assertKnowledgeBindingOnlyPlanChange, normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from '../domain/plan-semantics.js';
|
|
4
4
|
import { pathAllowed } from '../domain/validation.js';
|
|
5
5
|
import { WorkflowError } from '../errors.js';
|
|
6
6
|
import { hashCanonical } from '../lifecycle/fingerprint.js';
|
|
7
7
|
import { createUlid } from '../ulid.js';
|
|
8
|
-
import { PACKAGE_VERSION } from '../version.js';
|
|
9
8
|
import { readPlanRiskAuditEvents } from './plan-risk.js';
|
|
10
9
|
import { assertHash64, canonicalJsonStringify, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
|
|
11
|
-
import { readTaskC1Posture } from './handoff.js';
|
|
12
10
|
import { buildCurrentStrictStepReviewCycle, latestPassedVerifiedStepReviewEvent, readReviewerAttestations, readStepReviewEvents, } from './review.js';
|
|
13
|
-
import {
|
|
11
|
+
import { correctiveReplanBoundaryMatchesStep, readCompletedCorrectiveReplanHistory } from '../state/corrective-replan-executor.js';
|
|
12
|
+
import { semanticBoundaryHash, validLiteralTestInvocationEvidence } from './component-owner.js';
|
|
14
13
|
import { reviewerCauseFingerprint } from './remediation-cause.js';
|
|
14
|
+
import { readFailedStepPlanningRecovery } from './failed-step-planning-recovery.js';
|
|
15
15
|
export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
|
|
16
16
|
export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
|
|
17
17
|
export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
|
|
18
18
|
export const REMEDIATION_MODE_RECOVERIES_SIDECAR = 'remediation-mode-recoveries.jsonl';
|
|
19
19
|
export const STOP_ESCALATE_OVERRIDES_SIDECAR = 'stop-escalate-overrides.jsonl';
|
|
20
|
-
const REMEDIATION_MODE_RECOVERY_LOCK = '.remediation-mode-recovery.lock';
|
|
21
20
|
const STOP_ESCALATE_OVERRIDE_LOCK = '.stop-escalate-override.lock';
|
|
22
21
|
const LEGACY_STOP_OVERRIDE_POLICY = 'beta11-attempt4-stop-compatibility-v1';
|
|
23
22
|
const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
@@ -83,7 +82,7 @@ function validateRemediationHistory(store, task) {
|
|
|
83
82
|
const overriddenStopDecisionEventIds = validateStopEscalateOverrideHistory(task, remediationEvents, correctiveDecisionEvents, readRawStopEscalateOverrideEvents(store, task));
|
|
84
83
|
validateRemediationAttemptOrdinals(remediationEvents, task);
|
|
85
84
|
validateRemediationReviewReferences(store, task, remediationEvents);
|
|
86
|
-
validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
|
|
85
|
+
validateCorrectiveDecisionCoverage(store, task, correctiveDecisionEvents, remediationEvents);
|
|
87
86
|
const recoveredEventIds = validateRemediationModeRecoveries(store, task, remediationEvents, correctiveDecisionEvents, modeRecoveries);
|
|
88
87
|
validateHistoricalRemediationModesForAllSteps(store, task, remediationEvents, correctiveDecisionEvents, recoveredEventIds, overriddenStopDecisionEventIds);
|
|
89
88
|
return {
|
|
@@ -139,7 +138,7 @@ export function readStopEscalateOverrideEvents(store, task) {
|
|
|
139
138
|
const decisions = readRawCorrectiveDecisionEvents(store, task);
|
|
140
139
|
const overrides = readRawStopEscalateOverrideEvents(store, task);
|
|
141
140
|
validateRemediationAttemptOrdinals(remediations, task);
|
|
142
|
-
validateCorrectiveDecisionCoverage(task, decisions, remediations);
|
|
141
|
+
validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
|
|
143
142
|
validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
|
|
144
143
|
return overrides;
|
|
145
144
|
}
|
|
@@ -148,7 +147,7 @@ export function stopEscalateOverrideDecisionEventIds(store, task) {
|
|
|
148
147
|
const decisions = readRawCorrectiveDecisionEvents(store, task);
|
|
149
148
|
const overrides = readRawStopEscalateOverrideEvents(store, task);
|
|
150
149
|
validateRemediationAttemptOrdinals(remediations, task);
|
|
151
|
-
validateCorrectiveDecisionCoverage(task, decisions, remediations);
|
|
150
|
+
validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
|
|
152
151
|
return validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
|
|
153
152
|
}
|
|
154
153
|
export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null, causeBindings = []) {
|
|
@@ -185,7 +184,7 @@ export function prepareRemediationEvent(store, task, stepId, failureKind, failur
|
|
|
185
184
|
}
|
|
186
185
|
const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
|
|
187
186
|
const cycle = selectCurrentPlanRemediationCycle(store, task, priorEvents, correctiveDecisions);
|
|
188
|
-
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
|
|
187
|
+
const gate = deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
|
|
189
188
|
recoveredDecisionEventId,
|
|
190
189
|
reboundStopOverrideDecisionEventId,
|
|
191
190
|
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
@@ -206,7 +205,7 @@ export function prepareRemediationEvent(store, task, stepId, failureKind, failur
|
|
|
206
205
|
return { event: reused, postimage: null };
|
|
207
206
|
}
|
|
208
207
|
const cycle = selectCurrentPlanRemediationCycle(store, task, existing, correctiveDecisions);
|
|
209
|
-
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
|
|
208
|
+
const gate = deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
|
|
210
209
|
recoveredDecisionEventId,
|
|
211
210
|
reboundStopOverrideDecisionEventId,
|
|
212
211
|
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
@@ -253,281 +252,31 @@ function findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId) {
|
|
|
253
252
|
}
|
|
254
253
|
return recoveries[0]?.correctiveDecisionEventId ?? null;
|
|
255
254
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
255
|
+
/** A first failed execution plus immutable rebound obstruction is planning evidence, not permission to retry. */
|
|
256
|
+
export function firstFailurePlanningObstructionEvidence(store, task, stepId) {
|
|
257
|
+
const failures = readRemediationEvents(store, task).filter(event => event.stepId === stepId);
|
|
258
|
+
if (failures.length !== 1 || task.steps.find(step => step.id === stepId)?.status !== 'failed')
|
|
259
|
+
return null;
|
|
260
|
+
const recovery = readFailedStepPlanningRecovery(store, task);
|
|
261
|
+
if (!recovery || !recovery.current.reviewRequiredStepIds.includes(stepId))
|
|
260
262
|
return null;
|
|
261
|
-
if (candidates.length !== 1) {
|
|
262
|
-
throw new WorkflowError('STATE_CORRUPT', 'Multiple malformed remediation modes are not recoverable.', {
|
|
263
|
-
taskId: task.id,
|
|
264
|
-
eventIds: candidates.map((event) => event.eventId),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
const posture = readTaskC1Posture(store, task);
|
|
268
|
-
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
269
|
-
const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
|
|
270
|
-
return assessRemediationModeRecovery(store, task, candidates[0].stepId, actor, lease?.token ?? null, options);
|
|
271
|
-
}
|
|
272
|
-
export function assessRemediationModeRecovery(store, task, stepId, actor, writerToken, options = {}) {
|
|
273
|
-
const normalizedActor = actor.trim();
|
|
274
|
-
if (!normalizedActor)
|
|
275
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Remediation mode recovery actor is required.');
|
|
276
|
-
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
277
|
-
if (!step)
|
|
278
|
-
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
279
|
-
if (task.status !== 'needs_fix' || step.status !== 'failed') {
|
|
280
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the exact failed Task/Step posture.', {
|
|
281
|
-
taskId: task.id,
|
|
282
|
-
taskStatus: task.status,
|
|
283
|
-
stepId,
|
|
284
|
-
stepStatus: step.status,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
if (!task.planHash)
|
|
288
|
-
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
|
|
289
|
-
const remediations = readRawRemediationEvents(store, task);
|
|
290
|
-
const decisions = readRawCorrectiveDecisionEvents(store, task);
|
|
291
|
-
validateRemediationAttemptOrdinals(remediations, task);
|
|
292
|
-
validateRemediationReviewReferences(store, task, remediations);
|
|
293
|
-
validateCorrectiveDecisionCoverage(task, decisions, remediations);
|
|
294
|
-
const malformed = remediations.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
|
|
295
|
-
if (malformed.length !== 1 || malformed[0].stepId !== stepId || malformed[0].attemptOrdinal !== 3) {
|
|
296
|
-
throw new WorkflowError('STATE_CORRUPT', 'State does not contain the one exact alpha.7.2 remediation mode omission.', {
|
|
297
|
-
taskId: task.id,
|
|
298
|
-
stepId,
|
|
299
|
-
malformedEventIds: malformed.map((event) => event.eventId),
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
const stepRemediations = remediations.filter((event) => event.stepId === stepId);
|
|
303
|
-
if (stepRemediations.length !== 3
|
|
304
|
-
|| stepRemediations[0].mode !== 'ordinary'
|
|
305
|
-
|| stepRemediations[1].mode !== 'ordinary'
|
|
306
|
-
|| stepRemediations[2].eventId !== malformed[0].eventId) {
|
|
307
|
-
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires exactly two ordinary attempts and one malformed third attempt.', {
|
|
308
|
-
taskId: task.id,
|
|
309
|
-
stepId,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
const decisionMatches = decisions.filter((event) => event.stepId === stepId
|
|
313
|
-
&& event.triggeringAttemptCount === 3
|
|
314
|
-
&& event.decision === 'continue-fix'
|
|
315
|
-
&& sameStringArray(event.coveredEventIds, stepRemediations.slice(0, 2).map((event) => event.eventId)));
|
|
316
|
-
if (decisionMatches.length !== 1) {
|
|
317
|
-
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact continue-fix decision.', {
|
|
318
|
-
taskId: task.id,
|
|
319
|
-
stepId,
|
|
320
|
-
decisionEventIds: decisionMatches.map((event) => event.eventId),
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
const decision = decisionMatches[0];
|
|
324
|
-
const correctiveRecoveries = readRawCorrectiveDecisionRecoveries(store, task).filter((event) => event.stepId === stepId
|
|
325
|
-
&& event.triggeringAttemptCount === 3
|
|
326
|
-
&& event.correctiveDecisionEventId === decision.eventId
|
|
327
|
-
&& event.correctiveDecisionEventHash === decision.eventHash
|
|
328
|
-
&& event.reboundPlanHash === task.planHash);
|
|
329
|
-
if (correctiveRecoveries.length !== 1) {
|
|
330
|
-
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact corrective-decision recovery.', {
|
|
331
|
-
taskId: task.id,
|
|
332
|
-
stepId,
|
|
333
|
-
recoveryEventIds: correctiveRecoveries.map((event) => event.eventId),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
const correctiveRecovery = correctiveRecoveries[0];
|
|
337
|
-
const authority = options.authorityMode === 'observe'
|
|
338
|
-
? observeRemediationModeRecoveryAuthority(store, task)
|
|
339
|
-
: requireRemediationModeRecoveryAuthority(store, task, normalizedActor, writerToken);
|
|
340
|
-
const existing = readRemediationModeRecoveryEvents(store, task).filter((event) => event.remediationEventId === malformed[0].eventId);
|
|
341
|
-
if (existing.length > 1) {
|
|
342
|
-
throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation mode recoveries target one event.', {
|
|
343
|
-
taskId: task.id,
|
|
344
|
-
remediationEventId: malformed[0].eventId,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
if (existing[0]) {
|
|
348
|
-
validateRemediationModeRecoveryBinding(existing[0], malformed[0], decision, correctiveRecovery);
|
|
349
|
-
const retryMismatches = [
|
|
350
|
-
existing[0].taskRevision !== task.revision ? 'taskRevision' : null,
|
|
351
|
-
existing[0].planHash !== task.planHash ? 'planHash' : null,
|
|
352
|
-
existing[0].actor !== normalizedActor ? 'actor' : null,
|
|
353
|
-
existing[0].c1ClaimEventHash !== authority.c1ClaimEventHash ? 'c1ClaimEventHash' : null,
|
|
354
|
-
existing[0].writerLeaseTokenHash !== authority.writerLeaseTokenHash ? 'writerLeaseTokenHash' : null,
|
|
355
|
-
].filter((value) => value !== null);
|
|
356
|
-
if (retryMismatches.length > 0) {
|
|
357
|
-
throw new WorkflowError('STATE_CONFLICT', 'Existing remediation mode recovery conflicts with the requested retry binding.', {
|
|
358
|
-
taskId: task.id,
|
|
359
|
-
stepId,
|
|
360
|
-
recoveryEventId: existing[0].eventId,
|
|
361
|
-
mismatches: retryMismatches,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
const evidenceFingerprint = sha256Hex(canonicalJsonStringify({
|
|
366
|
-
taskId: task.id,
|
|
367
|
-
taskRevision: task.revision,
|
|
368
|
-
stepId,
|
|
369
|
-
planHash: task.planHash,
|
|
370
|
-
remediationEventHash: malformed[0].eventHash,
|
|
371
|
-
correctiveDecisionEventHash: decision.eventHash,
|
|
372
|
-
correctiveDecisionRecoveryEventHash: correctiveRecovery.eventHash,
|
|
373
|
-
actor: normalizedActor,
|
|
374
|
-
c1ClaimEventHash: authority.c1ClaimEventHash,
|
|
375
|
-
writerLeaseTokenHash: authority.writerLeaseTokenHash,
|
|
376
|
-
}));
|
|
377
|
-
return {
|
|
378
|
-
task,
|
|
379
|
-
remediation: malformed[0],
|
|
380
|
-
decision,
|
|
381
|
-
correctiveRecovery,
|
|
382
|
-
existingRecovery: existing[0] ?? null,
|
|
383
|
-
actor: normalizedActor,
|
|
384
|
-
c1ClaimEventHash: authority.c1ClaimEventHash,
|
|
385
|
-
writerLeaseTokenHash: authority.writerLeaseTokenHash,
|
|
386
|
-
evidenceFingerprint,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
function observeRemediationModeRecoveryAuthority(store, task) {
|
|
390
|
-
const posture = readTaskC1Posture(store, task);
|
|
391
|
-
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
392
|
-
return {
|
|
393
|
-
c1ClaimEventHash: posture.state === 'none' ? null : posture.latestEvent.eventHash,
|
|
394
|
-
writerLeaseTokenHash: lease ? sha256Hex(lease.token) : null,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
export function appendRemediationModeRecovery(store, task, stepId, expectedRevision, actor, writerToken, now) {
|
|
398
|
-
if (task.revision !== expectedRevision) {
|
|
399
|
-
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
400
|
-
expectedRevision,
|
|
401
|
-
actualRevision: task.revision,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
const preflight = assessRemediationModeRecovery(store, task, stepId, actor, writerToken);
|
|
405
|
-
const root = store.taskRoot(task.projectId, task.id);
|
|
406
|
-
const lockFile = path.join(root, REMEDIATION_MODE_RECOVERY_LOCK);
|
|
407
|
-
let descriptor;
|
|
408
|
-
try {
|
|
409
|
-
descriptor = openSync(lockFile, 'wx', 0o600);
|
|
410
|
-
closeSync(descriptor);
|
|
411
|
-
}
|
|
412
|
-
catch (error) {
|
|
413
|
-
if (existsSync(lockFile)) {
|
|
414
|
-
throw new WorkflowError('LOCKED', `Remediation mode recovery for ${task.id} is already being serialized.`, {
|
|
415
|
-
taskId: task.id,
|
|
416
|
-
stepId,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
throw error;
|
|
420
|
-
}
|
|
421
|
-
try {
|
|
422
|
-
const currentTask = store.readTask(task.projectId, task.id);
|
|
423
|
-
if (currentTask.revision !== expectedRevision) {
|
|
424
|
-
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
425
|
-
expectedRevision,
|
|
426
|
-
actualRevision: currentTask.revision,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
let underLock;
|
|
430
|
-
try {
|
|
431
|
-
underLock = assessRemediationModeRecovery(store, currentTask, stepId, actor, writerToken);
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
|
|
435
|
-
taskId: task.id,
|
|
436
|
-
stepId,
|
|
437
|
-
cause: error instanceof Error ? error.message : String(error),
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
if (underLock.existingRecovery)
|
|
441
|
-
return underLock.existingRecovery;
|
|
442
|
-
if (underLock.evidenceFingerprint !== preflight.evidenceFingerprint) {
|
|
443
|
-
throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
|
|
444
|
-
taskId: task.id,
|
|
445
|
-
stepId,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
return store.appendSidecarEvent(root, REMEDIATION_MODE_RECOVERIES_SIDECAR, {
|
|
449
|
-
eventId: `RMR-${createUlid(now.getTime())}`,
|
|
450
|
-
recordedAt: now.toISOString(),
|
|
451
|
-
taskId: task.id,
|
|
452
|
-
stepId,
|
|
453
|
-
taskRevision: expectedRevision,
|
|
454
|
-
remediationEventId: underLock.remediation.eventId,
|
|
455
|
-
remediationEventHash: underLock.remediation.eventHash,
|
|
456
|
-
correctiveDecisionEventId: underLock.decision.eventId,
|
|
457
|
-
correctiveDecisionEventHash: underLock.decision.eventHash,
|
|
458
|
-
correctiveDecisionRecoveryEventId: underLock.correctiveRecovery.eventId,
|
|
459
|
-
correctiveDecisionRecoveryEventHash: underLock.correctiveRecovery.eventHash,
|
|
460
|
-
attemptOrdinal: 3,
|
|
461
|
-
planHash: currentTask.planHash,
|
|
462
|
-
actor: underLock.actor,
|
|
463
|
-
c1ClaimEventHash: underLock.c1ClaimEventHash,
|
|
464
|
-
writerLeaseTokenHash: underLock.writerLeaseTokenHash,
|
|
465
|
-
fromMode: 'ordinary',
|
|
466
|
-
toMode: 'corrective',
|
|
467
|
-
reason: 'alpha7.2-recovered-continue-fix-mode-omission',
|
|
468
|
-
}, {
|
|
469
|
-
validateEvent: (event, context) => validateStoredRemediationModeRecovery(event, currentTask, context.file, context.line),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
finally {
|
|
473
|
-
if (existsSync(lockFile))
|
|
474
|
-
unlinkSync(lockFile);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function requireRemediationModeRecoveryAuthority(store, task, actor, writerToken) {
|
|
478
|
-
const posture = readTaskC1Posture(store, task);
|
|
479
|
-
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
480
|
-
if (posture.state === 'pending') {
|
|
481
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending C1 handoff must be claimed before remediation mode recovery.');
|
|
482
|
-
}
|
|
483
|
-
if (posture.state === 'claimed') {
|
|
484
|
-
const tokenHash = writerToken ? sha256Hex(writerToken) : null;
|
|
485
|
-
if (!lease
|
|
486
|
-
|| actor !== posture.claimant
|
|
487
|
-
|| lease.owner !== posture.claimant
|
|
488
|
-
|| !writerToken
|
|
489
|
-
|| lease.token !== writerToken
|
|
490
|
-
|| tokenHash !== posture.writerLeaseTokenHash) {
|
|
491
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the claimed C1 actor and exact writer token.', {
|
|
492
|
-
taskId: task.id,
|
|
493
|
-
claimant: posture.claimant,
|
|
494
|
-
leaseOwner: lease?.owner ?? null,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
return { c1ClaimEventHash: posture.latestEvent.eventHash, writerLeaseTokenHash: tokenHash };
|
|
498
|
-
}
|
|
499
|
-
if (lease && (lease.owner !== actor || !writerToken || lease.token !== writerToken)) {
|
|
500
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Existing writer lease is not owned by the remediation recovery actor/token.');
|
|
501
|
-
}
|
|
502
|
-
if (!lease && writerToken) {
|
|
503
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'A writer token was provided without an existing writer lease.');
|
|
504
|
-
}
|
|
505
263
|
return {
|
|
506
|
-
|
|
507
|
-
|
|
264
|
+
kind: 'first-failure-rebound-planning-obstruction', taskRevision: task.revision,
|
|
265
|
+
sourcePlanHash: task.planHash, baselinePlanHash: recovery.baseline.planHash,
|
|
266
|
+
baselineAuditEventId: recovery.baseline.eventId, baselineAuditEventHash: recovery.baseline.eventHash,
|
|
267
|
+
obstructionAuditEventId: recovery.current.audit.eventId, obstructionAuditEventHash: recovery.current.audit.eventHash,
|
|
268
|
+
priorAuthorizationHash: hashCanonical(recovery.priorAuthorization),
|
|
269
|
+
remediationEventId: failures[0].eventId, remediationEventHash: failures[0].eventHash,
|
|
270
|
+
knowledgeRebindCount: task.knowledgeRebinds.length, knowledgeRebindHash: hashCanonical(task.knowledgeRebinds),
|
|
508
271
|
};
|
|
509
272
|
}
|
|
510
|
-
function readWriterLeaseForModeRecovery(store, task) {
|
|
511
|
-
const file = path.join(store.lockRoot(task.projectId), `${task.id}.json`);
|
|
512
|
-
if (!existsSync(file))
|
|
513
|
-
return null;
|
|
514
|
-
let lease;
|
|
515
|
-
try {
|
|
516
|
-
lease = JSON.parse(readFileSync(file, 'utf8'));
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
throw new WorkflowError('STATE_CORRUPT', `Writer lease is unreadable: ${file}`);
|
|
520
|
-
}
|
|
521
|
-
if (lease.entityId !== task.id || typeof lease.owner !== 'string' || typeof lease.token !== 'string') {
|
|
522
|
-
throw new WorkflowError('STATE_CORRUPT', `Writer lease does not match Task ${task.id}.`);
|
|
523
|
-
}
|
|
524
|
-
return lease;
|
|
525
|
-
}
|
|
526
273
|
export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAudit, planRiskAudit, planHash, now) {
|
|
527
274
|
validateCorrectiveDecisionAudit(correctiveAudit);
|
|
528
275
|
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
529
276
|
const triggeringAttemptCount = remediationEvents.length + 1;
|
|
530
|
-
|
|
277
|
+
const planningObstructionEvidence = triggeringAttemptCount === 2 && correctiveAudit.decision === 'replan-required'
|
|
278
|
+
? firstFailurePlanningObstructionEvidence(store, task, stepId) : null;
|
|
279
|
+
if (triggeringAttemptCount < 3 && !planningObstructionEvidence) {
|
|
531
280
|
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision is not available for ${stepId} before two guarded remediation failures exist.`, {
|
|
532
281
|
taskId: task.id,
|
|
533
282
|
stepId,
|
|
@@ -581,6 +330,7 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
|
|
|
581
330
|
stepId,
|
|
582
331
|
triggeringAttemptCount,
|
|
583
332
|
decision: correctiveAudit.decision,
|
|
333
|
+
...(planningObstructionEvidence ? { recoveryBasis: 'planning-obstruction', planningObstructionEvidence } : {}),
|
|
584
334
|
auditor: correctiveAudit.auditor.trim(),
|
|
585
335
|
coveredFindingIds,
|
|
586
336
|
coveredEventIds,
|
|
@@ -589,176 +339,6 @@ export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAud
|
|
|
589
339
|
validateEvent: (event, context) => validateStoredCorrectiveDecisionEvent(event, task, context.file, context.line),
|
|
590
340
|
});
|
|
591
341
|
}
|
|
592
|
-
export function prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit) {
|
|
593
|
-
if (task.revision !== expectedRevision) {
|
|
594
|
-
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
595
|
-
expectedRevision,
|
|
596
|
-
actualRevision: task.revision,
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
const normalizedActor = actor.trim();
|
|
600
|
-
const normalizedReason = reason.trim();
|
|
601
|
-
if (!normalizedActor)
|
|
602
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a human actor.');
|
|
603
|
-
if (!normalizedReason)
|
|
604
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires a reason.');
|
|
605
|
-
if (!COMMIT_SHA_PATTERN.test(headCommit)) {
|
|
606
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Stop override requires the exact current Git HEAD.');
|
|
607
|
-
}
|
|
608
|
-
if (task.status !== 'needs_fix') {
|
|
609
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a needs_fix Task.', {
|
|
610
|
-
taskId: task.id,
|
|
611
|
-
taskStatus: task.status,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
615
|
-
if (!step)
|
|
616
|
-
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
617
|
-
if (step.status !== 'failed') {
|
|
618
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires the exact failed Step.', {
|
|
619
|
-
taskId: task.id,
|
|
620
|
-
stepId,
|
|
621
|
-
stepStatus: step.status,
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
if (!task.planHash)
|
|
625
|
-
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
|
|
626
|
-
const history = readOpenGuardedRemediationHistory(store, task, stepId);
|
|
627
|
-
const remediations = history.remediationEvents;
|
|
628
|
-
if (remediations.length !== 3
|
|
629
|
-
|| remediations[0]?.attemptOrdinal !== 1
|
|
630
|
-
|| remediations[0]?.mode !== 'ordinary'
|
|
631
|
-
|| remediations[1]?.attemptOrdinal !== 2
|
|
632
|
-
|| remediations[1]?.mode !== 'ordinary'
|
|
633
|
-
|| remediations[2]?.attemptOrdinal !== 3
|
|
634
|
-
|| remediations[2]?.mode !== 'corrective') {
|
|
635
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override is limited to the exact beta.11 three-failure history.', {
|
|
636
|
-
taskId: task.id,
|
|
637
|
-
stepId,
|
|
638
|
-
attempts: remediations.map((event) => ({ eventId: event.eventId, ordinal: event.attemptOrdinal, mode: event.mode })),
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
const priorContinueMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 3
|
|
642
|
-
&& event.decision === 'continue-fix'
|
|
643
|
-
&& event.planHash === task.planHash
|
|
644
|
-
&& sameStringArray(event.coveredEventIds, remediations.slice(0, 2).map((candidate) => candidate.eventId)));
|
|
645
|
-
const stopMatches = history.correctiveDecisions.filter((event) => event.triggeringAttemptCount === 4
|
|
646
|
-
&& event.decision === 'stop-escalate'
|
|
647
|
-
&& event.planHash === task.planHash
|
|
648
|
-
&& sameStringArray(event.coveredEventIds, remediations.map((candidate) => candidate.eventId)));
|
|
649
|
-
if (priorContinueMatches.length !== 1 || stopMatches.length !== 1) {
|
|
650
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires one exact attempt-3 continue decision and one exact attempt-4 stop decision.', {
|
|
651
|
-
taskId: task.id,
|
|
652
|
-
stepId,
|
|
653
|
-
priorContinueDecisionEventIds: priorContinueMatches.map((event) => event.eventId),
|
|
654
|
-
stopDecisionEventIds: stopMatches.map((event) => event.eventId),
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
const priorContinue = priorContinueMatches[0];
|
|
658
|
-
const stopDecision = stopMatches[0];
|
|
659
|
-
const binding = {
|
|
660
|
-
taskId: task.id,
|
|
661
|
-
taskRevision: task.revision,
|
|
662
|
-
stepId,
|
|
663
|
-
stopDecisionEventId: stopDecision.eventId,
|
|
664
|
-
stopDecisionEventHash: stopDecision.eventHash,
|
|
665
|
-
priorContinueDecisionEventId: priorContinue.eventId,
|
|
666
|
-
priorContinueDecisionEventHash: priorContinue.eventHash,
|
|
667
|
-
triggeringAttemptCount: 4,
|
|
668
|
-
coveredEventIds: remediations.map((event) => event.eventId),
|
|
669
|
-
planHash: task.planHash,
|
|
670
|
-
headCommit,
|
|
671
|
-
actor: normalizedActor,
|
|
672
|
-
reason: normalizedReason,
|
|
673
|
-
packageVersion: PACKAGE_VERSION,
|
|
674
|
-
policy: LEGACY_STOP_OVERRIDE_POLICY,
|
|
675
|
-
};
|
|
676
|
-
const confirmationCodeBindingHash = stopEscalateOverrideBindingHash(binding);
|
|
677
|
-
const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === stopDecision.eventId) ?? null;
|
|
678
|
-
if (existing && existing.confirmationCodeBindingHash !== confirmationCodeBindingHash) {
|
|
679
|
-
throw new WorkflowError('STATE_CONFLICT', 'The stop decision already has an override with a different human binding.', {
|
|
680
|
-
taskId: task.id,
|
|
681
|
-
stepId,
|
|
682
|
-
overrideEventId: existing.eventId,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
return {
|
|
686
|
-
eligible: true,
|
|
687
|
-
action: 'task stop-override-apply',
|
|
688
|
-
...binding,
|
|
689
|
-
confirmationCodeBindingHash,
|
|
690
|
-
confirmationCode: `SOO-${confirmationCodeBindingHash.slice(0, 24).toUpperCase()}`,
|
|
691
|
-
appendOnly: true,
|
|
692
|
-
preservesOriginalDecision: true,
|
|
693
|
-
delegatedApprovalAllowed: false,
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
export function appendStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit, confirmationCode, now) {
|
|
697
|
-
const prepared = prepareStopEscalateOverride(store, task, stepId, expectedRevision, actor, reason, headCommit);
|
|
698
|
-
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
699
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override confirmation code does not match the current state binding.', {
|
|
700
|
-
taskId: task.id,
|
|
701
|
-
stepId,
|
|
702
|
-
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
const root = store.taskRoot(task.projectId, task.id);
|
|
706
|
-
const existing = readStopEscalateOverrideEvents(store, task).find((event) => event.stopDecisionEventId === prepared.stopDecisionEventId) ?? null;
|
|
707
|
-
if (existing)
|
|
708
|
-
return existing;
|
|
709
|
-
const lockFile = path.join(root, STOP_ESCALATE_OVERRIDE_LOCK);
|
|
710
|
-
let descriptor;
|
|
711
|
-
try {
|
|
712
|
-
descriptor = openSync(lockFile, 'wx', 0o600);
|
|
713
|
-
closeSync(descriptor);
|
|
714
|
-
}
|
|
715
|
-
catch (error) {
|
|
716
|
-
if (existsSync(lockFile)) {
|
|
717
|
-
throw new WorkflowError('LOCKED', `Stop override for ${task.id} is already being serialized.`, {
|
|
718
|
-
taskId: task.id,
|
|
719
|
-
stepId,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
throw error;
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
const currentTask = store.readTask(task.projectId, task.id);
|
|
726
|
-
const underLock = prepareStopEscalateOverride(store, currentTask, stepId, expectedRevision, actor, reason, headCommit);
|
|
727
|
-
if (underLock.confirmationCode !== prepared.confirmationCode
|
|
728
|
-
|| underLock.confirmationCodeBindingHash !== prepared.confirmationCodeBindingHash) {
|
|
729
|
-
throw new WorkflowError('STATE_CONFLICT', 'Stop override evidence changed after preparation.', {
|
|
730
|
-
taskId: task.id,
|
|
731
|
-
stepId,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
return store.appendSidecarEvent(root, STOP_ESCALATE_OVERRIDES_SIDECAR, {
|
|
735
|
-
eventId: `SOO-${createUlid(now.getTime())}`,
|
|
736
|
-
recordedAt: now.toISOString(),
|
|
737
|
-
taskId: underLock.taskId,
|
|
738
|
-
taskRevision: underLock.taskRevision,
|
|
739
|
-
stepId: underLock.stepId,
|
|
740
|
-
stopDecisionEventId: underLock.stopDecisionEventId,
|
|
741
|
-
stopDecisionEventHash: underLock.stopDecisionEventHash,
|
|
742
|
-
priorContinueDecisionEventId: underLock.priorContinueDecisionEventId,
|
|
743
|
-
priorContinueDecisionEventHash: underLock.priorContinueDecisionEventHash,
|
|
744
|
-
triggeringAttemptCount: 4,
|
|
745
|
-
coveredEventIds: underLock.coveredEventIds,
|
|
746
|
-
planHash: underLock.planHash,
|
|
747
|
-
headCommit: underLock.headCommit,
|
|
748
|
-
actor: underLock.actor,
|
|
749
|
-
reason: underLock.reason,
|
|
750
|
-
packageVersion: underLock.packageVersion,
|
|
751
|
-
confirmationCodeBindingHash: underLock.confirmationCodeBindingHash,
|
|
752
|
-
policy: LEGACY_STOP_OVERRIDE_POLICY,
|
|
753
|
-
}, {
|
|
754
|
-
validateEvent: (event, context) => validateStoredStopEscalateOverride(event, currentTask, context.file, context.line),
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
finally {
|
|
758
|
-
if (existsSync(lockFile))
|
|
759
|
-
unlinkSync(lockFile);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
342
|
export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor, evidence, now) {
|
|
763
343
|
if (!task.planHash) {
|
|
764
344
|
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no Plan hash for Plan-integrity recovery.`);
|
|
@@ -766,7 +346,11 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
766
346
|
if (!auditor.trim()) {
|
|
767
347
|
throw new WorkflowError('INVALID_ARGUMENT', 'Plan-integrity recovery requires a derived auditor identity.');
|
|
768
348
|
}
|
|
769
|
-
const
|
|
349
|
+
const allRemediationEvents = readRemediationEvents(store, task).filter(event => event.stepId === stepId);
|
|
350
|
+
const support = evidence.kind === 'check-support-anchor-outside-allowed-writes' || evidence.kind === 'failed-check-component-owner-outside-allowed-writes';
|
|
351
|
+
const remediationEvents = support ? selectPlanIntegrityRemediations(store, task, allRemediationEvents, readCorrectiveDecisionEvents(store, task).filter(event => event.stepId === stepId)) : allRemediationEvents;
|
|
352
|
+
if (!remediationEvents)
|
|
353
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Mechanical recovery lacks a verified current Plan cycle.');
|
|
770
354
|
const expectedBindings = evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
771
355
|
? [{
|
|
772
356
|
eventId: evidence.remediationEventId,
|
|
@@ -777,8 +361,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
777
361
|
if (remediationEvents.length !== expectedBindings.length
|
|
778
362
|
|| remediationEvents.some((event, index) => {
|
|
779
363
|
const binding = expectedBindings[index];
|
|
780
|
-
return event.
|
|
781
|
-
|| event.failureKind !== 'checks-failed'
|
|
364
|
+
return event.failureKind !== 'checks-failed'
|
|
782
365
|
|| event.eventId !== binding?.eventId
|
|
783
366
|
|| event.eventHash !== binding.eventHash
|
|
784
367
|
|| event.attemptOrdinal !== binding.attemptOrdinal;
|
|
@@ -789,7 +372,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
789
372
|
remediationEventIds: remediationEvents.map((event) => event.eventId),
|
|
790
373
|
});
|
|
791
374
|
}
|
|
792
|
-
const triggeringAttemptCount =
|
|
375
|
+
const triggeringAttemptCount = allRemediationEvents.length + 1;
|
|
793
376
|
const recoveryDecision = evidence.kind === 'check-support-anchor-outside-allowed-writes'
|
|
794
377
|
|| evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
795
378
|
? 'continue-fix'
|
|
@@ -826,7 +409,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
826
409
|
decision: recoveryDecision,
|
|
827
410
|
auditor: auditor.trim(),
|
|
828
411
|
coveredFindingIds: [findingId],
|
|
829
|
-
coveredEventIds:
|
|
412
|
+
coveredEventIds: allRemediationEvents.map((event) => event.eventId),
|
|
830
413
|
planHash: task.planHash,
|
|
831
414
|
recoveryBasis: 'plan-integrity',
|
|
832
415
|
planIntegrityEvidence: evidence,
|
|
@@ -836,7 +419,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
|
|
|
836
419
|
}
|
|
837
420
|
export function assertGuardedRemediationAttemptAllowed(store, task, stepId, planRiskAudit, recoveredDecisionEventId = null, reboundStopOverrideDecisionEventId = null) {
|
|
838
421
|
const history = readOpenGuardedRemediationHistory(store, task, stepId);
|
|
839
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
|
|
422
|
+
return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
|
|
840
423
|
enforceRuntimeGate: true,
|
|
841
424
|
recoveredDecisionEventId,
|
|
842
425
|
reboundStopOverrideDecisionEventId,
|
|
@@ -861,12 +444,17 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
|
|
|
861
444
|
? findCurrentCorrectiveDecision(task, stepId, 2, history.correctiveDecisions)
|
|
862
445
|
: null;
|
|
863
446
|
const currentDecision = findCurrentCorrectiveDecision(task, stepId, history.nextAttemptOrdinal, history.correctiveDecisions);
|
|
447
|
+
if (remediationEvents.length === 1 && currentDecision === null
|
|
448
|
+
&& firstFailurePlanningObstructionEvidence(store, task, stepId)) {
|
|
449
|
+
return { stepId, guardedCategory: requireGuardedCategory(planRiskAudit, stepId), attemptCount: 1,
|
|
450
|
+
nextAttemptOrdinal: history.nextAttemptOrdinal, mode: 'corrective', correctiveDecision: null, posture: 'decision-required' };
|
|
451
|
+
}
|
|
864
452
|
if (remediationEvents.length < 2 && earlyPlanIntegrityDecision?.recoveryBasis !== 'plan-integrity'
|
|
865
453
|
&& currentDecision === null
|
|
866
454
|
&& deriveCurrentCauseDecision(remediationEvents, history.priorRemediationEvents) !== 'root-cause-replan')
|
|
867
455
|
return null;
|
|
868
456
|
if (step.status === 'failed' || step.status === 'planned') {
|
|
869
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
457
|
+
return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
870
458
|
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
871
459
|
priorRemediationEvents: history.priorRemediationEvents,
|
|
872
460
|
nextAttemptOrdinal: history.nextAttemptOrdinal,
|
|
@@ -877,7 +465,7 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
|
|
|
877
465
|
const currentCycle = buildCurrentStrictStepReviewCycle(store, task, stepId);
|
|
878
466
|
if (currentCycle.resolution !== 'unverified')
|
|
879
467
|
return null;
|
|
880
|
-
return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
468
|
+
return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
|
|
881
469
|
overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
|
|
882
470
|
priorRemediationEvents: history.priorRemediationEvents,
|
|
883
471
|
nextAttemptOrdinal: history.nextAttemptOrdinal,
|
|
@@ -898,7 +486,7 @@ function readOpenGuardedRemediationHistory(store, task, stepId) {
|
|
|
898
486
|
nextAttemptOrdinal: allStepRemediations.length + 1,
|
|
899
487
|
};
|
|
900
488
|
}
|
|
901
|
-
function selectCurrentPlanRemediationCycle(store, task, remediationEvents, correctiveDecisions) {
|
|
489
|
+
function selectCurrentPlanRemediationCycle(store, task, remediationEvents, correctiveDecisions, canonicalPlanHash = task.planHash) {
|
|
902
490
|
const unchanged = () => ({
|
|
903
491
|
remediationEvents: [...remediationEvents],
|
|
904
492
|
priorRemediationEvents: [],
|
|
@@ -916,13 +504,13 @@ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, corre
|
|
|
916
504
|
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
917
505
|
let currentSemanticHash;
|
|
918
506
|
try {
|
|
919
|
-
currentSemanticHash = normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(taskRoot, task.planHash,
|
|
507
|
+
currentSemanticHash = normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(taskRoot, task.planHash, canonicalPlanHash), 'current').semanticHash;
|
|
920
508
|
}
|
|
921
509
|
catch {
|
|
922
510
|
return unchanged();
|
|
923
511
|
}
|
|
924
512
|
const reviews = readStepReviewEvents(store, task);
|
|
925
|
-
const bindings = remediationEvents.map((event) => resolveRemediationPlanCycleBinding(taskRoot,
|
|
513
|
+
const bindings = remediationEvents.map((event) => resolveRemediationPlanCycleBinding(taskRoot, canonicalPlanHash, event, reviews, audits));
|
|
926
514
|
if (bindings.some((binding) => binding === null))
|
|
927
515
|
return unchanged();
|
|
928
516
|
let cycleStart = remediationEvents.length;
|
|
@@ -939,6 +527,71 @@ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, corre
|
|
|
939
527
|
correctiveDecisions: [...correctiveDecisions],
|
|
940
528
|
};
|
|
941
529
|
}
|
|
530
|
+
/** Scope mechanical support evidence only; global corrective authority is never discarded. */
|
|
531
|
+
export function selectPlanIntegrityRemediations(store, task, events, decisions, canonicalPlanHash = task.planHash) {
|
|
532
|
+
if (events.length === 0)
|
|
533
|
+
return null;
|
|
534
|
+
const stepId = events[0].stepId;
|
|
535
|
+
if (events.some(event => event.stepId !== stepId) || decisions.some(event => event.stepId !== stepId))
|
|
536
|
+
return null;
|
|
537
|
+
const cycle = selectCurrentPlanRemediationCycle(store, task, events, decisions, canonicalPlanHash);
|
|
538
|
+
if (cycle.priorRemediationEvents.length === 0)
|
|
539
|
+
return decisions.length === 0 ? [...events] : null;
|
|
540
|
+
if (!task.planHash || cycle.remediationEvents.length === 0
|
|
541
|
+
|| decisions.some(d => d.decision === 'stop-escalate' || d.decision === 'split-required'))
|
|
542
|
+
return null;
|
|
543
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
544
|
+
const history = readCompletedCorrectiveReplanHistory(store, task);
|
|
545
|
+
const semantic = (hash) => {
|
|
546
|
+
const saved = history.flatMap(boundary => [boundary.beforePlan, boundary.afterPlan]).find(bytes => sha256Hex(bytes) === hash);
|
|
547
|
+
return normalizePlanKnowledgeBinding(saved ?? readVerifiedPlanArtifact(root, hash, canonicalPlanHash), 'cycle').semanticHash;
|
|
548
|
+
};
|
|
549
|
+
const currentSemantic = semantic(task.planHash);
|
|
550
|
+
const audits = readPlanRiskAuditEvents(store, task), reviews = readStepReviewEvents(store, task);
|
|
551
|
+
const priorBindings = cycle.priorRemediationEvents.map(event => resolveRemediationPlanCycleBinding(root, canonicalPlanHash, event, reviews, audits));
|
|
552
|
+
const currentBindings = cycle.remediationEvents.map(event => resolveRemediationPlanCycleBinding(root, canonicalPlanHash, event, reviews, audits));
|
|
553
|
+
if (priorBindings.some(b => !b) || currentBindings.some(b => !b))
|
|
554
|
+
return null;
|
|
555
|
+
const boundaries = history.filter(boundary => correctiveReplanBoundaryMatchesStep(boundary.before, stepId)
|
|
556
|
+
&& boundary.after.planHash && boundary.before.planHash
|
|
557
|
+
&& semantic(boundary.after.planHash) === currentSemantic && semantic(boundary.before.planHash) !== currentSemantic
|
|
558
|
+
&& boundary.after.revision <= task.revision
|
|
559
|
+
&& priorBindings.every(b => b.sourceTaskRevision < boundary.after.revision)
|
|
560
|
+
&& currentBindings.every(b => b.sourceTaskRevision > boundary.after.revision));
|
|
561
|
+
if (boundaries.length !== 1)
|
|
562
|
+
return null;
|
|
563
|
+
const boundary = boundaries[0];
|
|
564
|
+
let cursor = task.planHash;
|
|
565
|
+
const visited = new Set();
|
|
566
|
+
const planBytes = (hash) => history.flatMap(b => [b.beforePlan, b.afterPlan]).find(bytes => sha256Hex(bytes) === hash)
|
|
567
|
+
?? readVerifiedPlanArtifact(root, hash, canonicalPlanHash);
|
|
568
|
+
while (cursor !== boundary.after.planHash) {
|
|
569
|
+
if (visited.has(cursor))
|
|
570
|
+
return null;
|
|
571
|
+
visited.add(cursor);
|
|
572
|
+
const links = task.knowledgeRebinds.filter(link => link.reboundPlanHash === cursor);
|
|
573
|
+
if (links.length !== 1 || !links[0].previousPlanHash)
|
|
574
|
+
return null;
|
|
575
|
+
const link = links[0];
|
|
576
|
+
assertKnowledgeBindingOnlyPlanChange(planBytes(link.previousPlanHash), planBytes(cursor));
|
|
577
|
+
if (!planBytes(link.previousPlanHash).includes(`- Knowledge map: revision ${link.fromKnowledgeMapRevision}, hash ${link.fromKnowledgeMapHash}\n`)
|
|
578
|
+
|| !planBytes(cursor).includes(`- Knowledge map: revision ${link.toKnowledgeMapRevision}, hash ${link.toKnowledgeMapHash}\n`))
|
|
579
|
+
return null;
|
|
580
|
+
cursor = link.previousPlanHash;
|
|
581
|
+
}
|
|
582
|
+
if (!task.authorizations.some(a => a.kind === 'execution' && ['approved', 'superseded'].includes(a.decision)
|
|
583
|
+
&& a.planHash === task.planHash && a.briefHash === task.briefHash
|
|
584
|
+
&& a.recordedAt >= boundary.admittedAt))
|
|
585
|
+
return null;
|
|
586
|
+
if (decisions.length === 0 || decisions.some(d => d.recordedAt > boundary.admittedAt
|
|
587
|
+
|| semantic(d.planHash) === currentSemantic))
|
|
588
|
+
return null;
|
|
589
|
+
const last = decisions.at(-1);
|
|
590
|
+
if (last.decision !== 'replan-required' || last.triggeringAttemptCount !== cycle.priorRemediationEvents.length + 1
|
|
591
|
+
|| semantic(last.planHash) !== semantic(boundary.before.planHash))
|
|
592
|
+
return null;
|
|
593
|
+
return cycle.remediationEvents;
|
|
594
|
+
}
|
|
942
595
|
function resolveRemediationPlanCycleBinding(taskRoot, currentPlanHash, event, reviews, audits) {
|
|
943
596
|
let planHash = null;
|
|
944
597
|
let sourceTaskRevision = null;
|
|
@@ -994,16 +647,22 @@ export function findFailedGuardedStepRequiringCorrectiveDecision(store, task, pl
|
|
|
994
647
|
? posture
|
|
995
648
|
: null;
|
|
996
649
|
}
|
|
997
|
-
function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
|
|
650
|
+
function deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
|
|
998
651
|
const attemptCount = remediationEvents.length + (options.priorRemediationEvents?.length ?? 0);
|
|
999
652
|
const nextAttemptOrdinal = options.nextAttemptOrdinal ?? attemptCount + 1;
|
|
1000
653
|
const guardedCategory = requireGuardedCategory(planRiskAudit, stepId);
|
|
1001
654
|
const rollingCauseDecision = deriveCurrentCauseDecision(remediationEvents, options.priorRemediationEvents ?? []);
|
|
1002
655
|
const currentCorrectiveDecision = findCurrentCorrectiveDecision(task, stepId, nextAttemptOrdinal, correctiveDecisions);
|
|
656
|
+
if (currentCorrectiveDecision?.recoveryBasis === 'planning-obstruction') {
|
|
657
|
+
return { stepId, guardedCategory, attemptCount, nextAttemptOrdinal, mode: 'ordinary',
|
|
658
|
+
correctiveDecision: currentCorrectiveDecision,
|
|
659
|
+
posture: currentCorrectiveDecision.planHash === task.planHash ? 'replan-required' : 'replan-ready' };
|
|
660
|
+
}
|
|
1003
661
|
const planIntegrityDecision = currentCorrectiveDecision;
|
|
1004
662
|
if (planIntegrityDecision?.recoveryBasis === 'plan-integrity'
|
|
1005
|
-
&& (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required'
|
|
1006
|
-
|
|
663
|
+
&& (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required'
|
|
664
|
+
|| isKnowledgeReboundSupportDecision(store, task, planIntegrityDecision))) {
|
|
665
|
+
if (planIntegrityDecision.planHash !== task.planHash && planIntegrityDecision.decision === 'replan-required') {
|
|
1007
666
|
return {
|
|
1008
667
|
stepId,
|
|
1009
668
|
guardedCategory,
|
|
@@ -1310,6 +969,17 @@ function validateStoredRemediationEvent(event, task, file, line) {
|
|
|
1310
969
|
if (hasDirtyWorktreeHash) {
|
|
1311
970
|
assertHash64(event.failureEvidence.dirtyWorktreeHash, `Remediation dirty-worktree hash at ${file}:${line}`);
|
|
1312
971
|
}
|
|
972
|
+
const boundary = event.failureEvidence.unclaimedExecutionBoundary;
|
|
973
|
+
if (boundary !== undefined) {
|
|
974
|
+
if (!boundary || typeof boundary !== 'object' || Array.isArray(boundary)
|
|
975
|
+
|| Object.keys(boundary).sort().join(',') !== 'actor,kind,priorHandoffEventHash'
|
|
976
|
+
|| boundary.kind !== 'native-unclaimed-check-failure'
|
|
977
|
+
|| typeof boundary.actor !== 'string' || !boundary.actor.trim() || boundary.actor !== boundary.actor.trim()
|
|
978
|
+
|| !(boundary.priorHandoffEventHash === null || typeof boundary.priorHandoffEventHash === 'string' && /^[a-f0-9]{64}$/.test(boundary.priorHandoffEventHash))
|
|
979
|
+
|| event.failureKind !== 'checks-failed' || !hasExecutionRevision || !hasDirtyWorktreeHash) {
|
|
980
|
+
throw new WorkflowError('STATE_CORRUPT', 'Invalid native unclaimed execution boundary.', { file, line, eventId: event.eventId });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
1313
983
|
if (!Number.isInteger(event.attemptOrdinal) || event.attemptOrdinal <= 0) {
|
|
1314
984
|
throw new WorkflowError('STATE_CORRUPT', 'Remediation event attemptOrdinal must be a positive integer.', {
|
|
1315
985
|
file,
|
|
@@ -1419,7 +1089,21 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
|
|
|
1419
1089
|
normalizeStringList(event.coveredFindingIds, 'Corrective decision coveredFindingIds');
|
|
1420
1090
|
normalizeStringList(event.coveredEventIds, 'Corrective decision coveredEventIds');
|
|
1421
1091
|
assertHash64(event.planHash, `Corrective decision planHash at ${file}:${line}`);
|
|
1422
|
-
if (event.recoveryBasis === '
|
|
1092
|
+
if (event.recoveryBasis === 'planning-obstruction' || event.planningObstructionEvidence !== undefined) {
|
|
1093
|
+
const evidence = event.planningObstructionEvidence;
|
|
1094
|
+
if (!evidence || event.recoveryBasis !== 'planning-obstruction' || event.planIntegrityEvidence !== undefined
|
|
1095
|
+
|| evidence.kind !== 'first-failure-rebound-planning-obstruction' || event.decision !== 'replan-required'
|
|
1096
|
+
|| event.triggeringAttemptCount !== 2 || evidence.sourcePlanHash !== event.planHash
|
|
1097
|
+
|| !Number.isSafeInteger(evidence.taskRevision) || evidence.taskRevision < 1
|
|
1098
|
+
|| !Number.isSafeInteger(evidence.knowledgeRebindCount) || evidence.knowledgeRebindCount < 1
|
|
1099
|
+
|| event.coveredEventIds.length !== 1 || event.coveredEventIds[0] !== evidence.remediationEventId) {
|
|
1100
|
+
throw new WorkflowError('STATE_CORRUPT', 'First-failure planning obstruction requires exact bounded proof.', { file, line, taskId: task.id });
|
|
1101
|
+
}
|
|
1102
|
+
for (const key of ['sourcePlanHash', 'baselinePlanHash', 'baselineAuditEventHash', 'obstructionAuditEventHash',
|
|
1103
|
+
'priorAuthorizationHash', 'remediationEventHash', 'knowledgeRebindHash'])
|
|
1104
|
+
assertHash64(evidence[key], `Planning obstruction ${key}`);
|
|
1105
|
+
}
|
|
1106
|
+
else if (event.recoveryBasis === 'plan-integrity' || event.planIntegrityEvidence !== undefined) {
|
|
1423
1107
|
validatePlanIntegrityDecision(event, task, file, line);
|
|
1424
1108
|
}
|
|
1425
1109
|
else if (event.triggeringAttemptCount === 2) {
|
|
@@ -1638,9 +1322,9 @@ function validatePlanIntegrityDecision(event, task, file, line) {
|
|
|
1638
1322
|
const validateCurrentPlanBinding = event.planHash === task.planHash;
|
|
1639
1323
|
if (event.decision !== 'continue-fix'
|
|
1640
1324
|
|| evidence.remediationEvents.length === 0
|
|
1641
|
-
|| event.triggeringAttemptCount !== evidence.remediationEvents.
|
|
1325
|
+
|| event.triggeringAttemptCount !== evidence.remediationEvents.at(-1).attemptOrdinal + 1
|
|
1642
1326
|
|| evidence.remediationEvents.some((binding, index) => {
|
|
1643
|
-
if (!binding.eventId || binding.attemptOrdinal !==
|
|
1327
|
+
if (!binding.eventId || binding.attemptOrdinal !== evidence.remediationEvents[0].attemptOrdinal + index)
|
|
1644
1328
|
return true;
|
|
1645
1329
|
assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
|
|
1646
1330
|
return false;
|
|
@@ -1681,9 +1365,9 @@ function validatePlanIntegrityDecision(event, task, file, line) {
|
|
|
1681
1365
|
assertHash64(evidence.failedCheckCauseFingerprint, `Plan-integrity failedCheckCauseFingerprint at ${file}:${line}`);
|
|
1682
1366
|
if (event.decision !== 'continue-fix'
|
|
1683
1367
|
|| evidence.remediationEvents.length === 0
|
|
1684
|
-
|| event.triggeringAttemptCount !== evidence.remediationEvents.
|
|
1368
|
+
|| event.triggeringAttemptCount !== evidence.remediationEvents.at(-1).attemptOrdinal + 1
|
|
1685
1369
|
|| evidence.remediationEvents.some((binding, index) => {
|
|
1686
|
-
if (!binding.eventId || binding.attemptOrdinal !==
|
|
1370
|
+
if (!binding.eventId || binding.attemptOrdinal !== evidence.remediationEvents[0].attemptOrdinal + index)
|
|
1687
1371
|
return true;
|
|
1688
1372
|
assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
|
|
1689
1373
|
return false;
|
|
@@ -1706,9 +1390,10 @@ function validatePlanIntegrityDecision(event, task, file, line) {
|
|
|
1706
1390
|
|| !conflict.componentRoot.trim()
|
|
1707
1391
|
|| !(conflict.ownerPath === conflict.componentRoot
|
|
1708
1392
|
|| conflict.ownerPath.startsWith(`${conflict.componentRoot}/`))
|
|
1709
|
-
||
|
|
1393
|
+
|| (conflict.testInvocation
|
|
1394
|
+
? !validLiteralTestInvocationEvidence(conflict) || importChain.length !== 1
|
|
1395
|
+
: importChain.length < 2 || importChain.at(-1) !== conflict.ownerPath)
|
|
1710
1396
|
|| importChain[0] !== conflict.runnerPath
|
|
1711
|
-
|| importChain.at(-1) !== conflict.ownerPath
|
|
1712
1397
|
|| !evidence.scopeExtensions.includes(conflict.ownerPath);
|
|
1713
1398
|
})) {
|
|
1714
1399
|
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v4 evidence has invalid component-owner bindings.', {
|
|
@@ -1744,13 +1429,54 @@ function validatePlanIntegrityDecision(event, task, file, line) {
|
|
|
1744
1429
|
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity changedFiles must be normalized.', { file, line });
|
|
1745
1430
|
}
|
|
1746
1431
|
}
|
|
1432
|
+
/** A validated support decision survives only a hash-verified Knowledge-only Plan chain.
|
|
1433
|
+
* This preserves scope evidence, never execution authorization or a semantic replan. */
|
|
1434
|
+
export function isKnowledgeReboundSupportDecision(store, task, event) {
|
|
1435
|
+
const evidence = event.planIntegrityEvidence;
|
|
1436
|
+
if (!task.planHash || event.planHash === task.planHash || event.taskId !== task.id
|
|
1437
|
+
|| event.recoveryBasis !== 'plan-integrity' || event.decision !== 'continue-fix'
|
|
1438
|
+
|| (evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
|
|
1439
|
+
&& evidence?.kind !== 'failed-check-component-owner-outside-allowed-writes'))
|
|
1440
|
+
return false;
|
|
1441
|
+
const step = task.steps.find(s => s.id === event.stepId);
|
|
1442
|
+
if (!step || evidence.stepDefinitionHash !== hashCanonical({
|
|
1443
|
+
id: step.id, title: step.title, objective: step.objective, requirements: step.requirements,
|
|
1444
|
+
dependencies: step.dependencies, expectedOutputs: step.expectedOutputs,
|
|
1445
|
+
allowedWrites: step.allowedWrites, forbiddenScope: step.forbiddenScope, checks: step.checks,
|
|
1446
|
+
}) || evidence.scopeExtensions.some(p => pathAllowed(p, step.allowedWrites) || pathAllowed(p, step.forbiddenScope))
|
|
1447
|
+
|| (evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
1448
|
+
&& evidence.semanticBoundaryHash !== semanticBoundaryHash(task)))
|
|
1449
|
+
return false;
|
|
1450
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
1451
|
+
let cursor = task.planHash;
|
|
1452
|
+
const visited = new Set();
|
|
1453
|
+
while (cursor !== event.planHash) {
|
|
1454
|
+
if (visited.has(cursor))
|
|
1455
|
+
return false;
|
|
1456
|
+
visited.add(cursor);
|
|
1457
|
+
const links = task.knowledgeRebinds.filter(link => link.reboundPlanHash === cursor);
|
|
1458
|
+
if (links.length !== 1 || !links[0].previousPlanHash)
|
|
1459
|
+
return false;
|
|
1460
|
+
const link = links[0];
|
|
1461
|
+
const before = readVerifiedPlanArtifact(root, link.previousPlanHash, task.planHash);
|
|
1462
|
+
const after = readVerifiedPlanArtifact(root, cursor, task.planHash);
|
|
1463
|
+
assertKnowledgeBindingOnlyPlanChange(before, after);
|
|
1464
|
+
const from = normalizePlanKnowledgeBinding(before, 'source').binding;
|
|
1465
|
+
const to = normalizePlanKnowledgeBinding(after, 'target').binding;
|
|
1466
|
+
if (from.revision !== link.fromKnowledgeMapRevision || from.hash !== link.fromKnowledgeMapHash
|
|
1467
|
+
|| to.revision !== link.toKnowledgeMapRevision || to.hash !== link.toKnowledgeMapHash)
|
|
1468
|
+
return false;
|
|
1469
|
+
cursor = link.previousPlanHash;
|
|
1470
|
+
}
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1747
1473
|
export function effectiveStepAllowedWrites(store, task, stepId) {
|
|
1748
1474
|
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
1749
1475
|
if (!step)
|
|
1750
1476
|
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
1751
1477
|
const extensions = readCorrectiveDecisionEvents(store, task)
|
|
1752
1478
|
.filter((event) => event.stepId === stepId
|
|
1753
|
-
&& event.planHash === task.planHash
|
|
1479
|
+
&& (event.planHash === task.planHash || isKnowledgeReboundSupportDecision(store, task, event))
|
|
1754
1480
|
&& event.decision === 'continue-fix'
|
|
1755
1481
|
&& event.recoveryBasis === 'plan-integrity')
|
|
1756
1482
|
.flatMap((event) => event.planIntegrityEvidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
@@ -2020,7 +1746,7 @@ function validateRemediationReviewReferences(store, task, events) {
|
|
|
2020
1746
|
}
|
|
2021
1747
|
}
|
|
2022
1748
|
}
|
|
2023
|
-
function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents) {
|
|
1749
|
+
function validateCorrectiveDecisionCoverage(store, task, decisions, remediationEvents) {
|
|
2024
1750
|
const remediationEventsById = new Map(remediationEvents.map((event) => [event.eventId, event]));
|
|
2025
1751
|
const decisionBindings = new Set();
|
|
2026
1752
|
for (const decision of decisions) {
|
|
@@ -2066,14 +1792,39 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
|
|
|
2066
1792
|
});
|
|
2067
1793
|
}
|
|
2068
1794
|
}
|
|
1795
|
+
if (decision.recoveryBasis === 'planning-obstruction') {
|
|
1796
|
+
const proof = decision.planningObstructionEvidence;
|
|
1797
|
+
const audits = readPlanRiskAuditEvents(store, task);
|
|
1798
|
+
const baseline = audits.find(event => event.eventId === proof.baselineAuditEventId && event.eventHash === proof.baselineAuditEventHash);
|
|
1799
|
+
const obstruction = audits.find(event => event.eventId === proof.obstructionAuditEventId && event.eventHash === proof.obstructionAuditEventHash);
|
|
1800
|
+
const failed = priorEvents[0];
|
|
1801
|
+
if (!baseline || !obstruction || !['approved', 'approved-with-rationale'].includes(baseline.decision)
|
|
1802
|
+
|| obstruction.decision !== 'stop-escalate' || audits.indexOf(baseline) >= audits.indexOf(obstruction)
|
|
1803
|
+
|| baseline.planHash !== proof.baselinePlanHash || obstruction.planHash !== proof.sourcePlanHash
|
|
1804
|
+
|| obstruction.taskRevision !== proof.taskRevision
|
|
1805
|
+
|| baseline.briefHash !== obstruction.briefHash || baseline.briefHash !== task.briefHash
|
|
1806
|
+
|| !failed || priorEvents.length !== 1 || failed.eventId !== proof.remediationEventId || failed.eventHash !== proof.remediationEventHash
|
|
1807
|
+
|| !task.authorizations.some(authorization => authorization.kind === 'execution' && authorization.decision === 'superseded'
|
|
1808
|
+
&& authorization.planHash === baseline.planHash && authorization.briefHash === baseline.briefHash
|
|
1809
|
+
&& hashCanonical(authorization) === proof.priorAuthorizationHash)
|
|
1810
|
+
|| task.knowledgeRebinds.length < proof.knowledgeRebindCount
|
|
1811
|
+
|| hashCanonical(task.knowledgeRebinds.slice(0, proof.knowledgeRebindCount)) !== proof.knowledgeRebindHash) {
|
|
1812
|
+
throw new WorkflowError('STATE_CORRUPT', 'Planning-obstruction decision does not match immutable audit, authorization, rebind and failure history.', { taskId: task.id, decisionEventId: decision.eventId });
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
2069
1815
|
if (decision.recoveryBasis === 'plan-integrity') {
|
|
2070
1816
|
const evidence = decision.planIntegrityEvidence;
|
|
2071
1817
|
const bindings = evidence?.kind === 'missing-npm-script-outside-allowed-writes'
|
|
2072
1818
|
? [{ eventId: evidence.remediationEventId, eventHash: evidence.remediationEventHash, attemptOrdinal: 1 }]
|
|
2073
1819
|
: evidence?.remediationEvents ?? [];
|
|
2074
|
-
|
|
2075
|
-
||
|
|
2076
|
-
|
|
1820
|
+
const support = evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
1821
|
+
|| evidence?.kind === 'failed-check-component-owner-outside-allowed-writes';
|
|
1822
|
+
const scoped = support && bindings[0]?.attemptOrdinal !== 1
|
|
1823
|
+
? selectPlanIntegrityRemediations(store, { ...task, planHash: decision.planHash, revision: evidence.taskRevision }, priorEvents, decisions.filter(d => d.stepId === decision.stepId && d.triggeringAttemptCount < decision.triggeringAttemptCount), task.planHash)
|
|
1824
|
+
: priorEvents;
|
|
1825
|
+
if (!evidence || !scoped
|
|
1826
|
+
|| bindings.length !== scoped.length
|
|
1827
|
+
|| scoped.some((linked, index) => linked.failureKind !== 'checks-failed'
|
|
2077
1828
|
|| linked.eventId !== bindings[index]?.eventId
|
|
2078
1829
|
|| linked.eventHash !== bindings[index]?.eventHash
|
|
2079
1830
|
|| linked.attemptOrdinal !== bindings[index]?.attemptOrdinal)) {
|