codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14
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 +89 -29
- package/dist/src/alpha6/adoption.d.ts +2 -0
- package/dist/src/alpha6/adoption.js +22 -0
- 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 +47 -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 +6 -21
- package/dist/src/alpha6/remediation.js +161 -476
- 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/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +6 -2
- 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 +2 -2
- package/dist/src/cli-actions.js +3 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +14 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -32
- 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/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/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 +5 -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 +37 -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.js +1 -1
- 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/version.js.map +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 +48 -63
- package/dist/src/workflow.js +666 -1460
- 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 +35 -238
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +95 -388
- package/docs/updating-existing-project.md +19 -717
- 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 +73 -197
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
- package/references/state-machine.md +4 -4
- package/roles/technical-planner.md +1 -1
- package/schemas/corrective-decision-event.schema.json +3 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +46 -2
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1123 -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 +39 -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 +2438 -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 +411 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +355 -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 +107 -0
- package/src/cli.ts +1457 -0
- package/src/contracts.ts +1494 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +190 -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/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 +408 -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 +843 -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 +572 -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 +660 -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 +818 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +908 -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 +10172 -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
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type {
|
|
4
|
+
AuthorizationEvent,
|
|
5
|
+
CorrectiveDecisionEvent,
|
|
6
|
+
CorrectiveDecisionRecoveryEvent,
|
|
7
|
+
DelegationGrant,
|
|
8
|
+
DependencyProvenanceRecoveryRecord,
|
|
9
|
+
PlanRiskAuditEvent,
|
|
10
|
+
ProjectKnowledgeMap,
|
|
11
|
+
StopEscalateOverrideEvent,
|
|
12
|
+
TaskKnowledgeRebindRecord,
|
|
13
|
+
TaskState,
|
|
14
|
+
WriterLease,
|
|
15
|
+
} from '../contracts.js';
|
|
16
|
+
import { hashDelegationPolicy } from '../delegation.js';
|
|
17
|
+
import { WorkflowError } from '../errors.js';
|
|
18
|
+
import { hashKnowledgeMap } from '../memory.js';
|
|
19
|
+
import type { FileStateStore } from '../state/store.js';
|
|
20
|
+
import { createUlid } from '../ulid.js';
|
|
21
|
+
import { readTaskC1Posture, readTaskHandoffEvents } from '../alpha6/handoff.js';
|
|
22
|
+
import { readCurrentPlanRiskAudit, readPlanRiskAuditEvents } from '../alpha6/plan-risk.js';
|
|
23
|
+
import { readMilestoneAutonomyEvents, type MilestoneAutonomyEvent } from './autonomy.js';
|
|
24
|
+
import {
|
|
25
|
+
assertCorrectiveAuditorIndependence,
|
|
26
|
+
readCorrectiveDecisionEvents,
|
|
27
|
+
readRemediationEvents,
|
|
28
|
+
readStopEscalateOverrideEvents,
|
|
29
|
+
} from '../alpha6/remediation.js';
|
|
30
|
+
import { assertHash64, canonicalJsonStringify, sha256Hex } from '../alpha6/store-sidecars.js';
|
|
31
|
+
import { gitIsAncestor, runGit } from '../repository.js';
|
|
32
|
+
import { PACKAGE_NAME } from '../version.js';
|
|
33
|
+
|
|
34
|
+
export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
|
|
35
|
+
const RECOVERY_LOCK = '.corrective-decision-recovery.lock';
|
|
36
|
+
const KNOWLEDGE_LINE = /^- Knowledge map: revision ([1-9]\d*), hash ([a-f0-9]{64})$/gm;
|
|
37
|
+
const KNOWLEDGE_SENTINEL = '- Knowledge map: revision <context>, hash <context>';
|
|
38
|
+
const REFRESH_REASONS = new Set([
|
|
39
|
+
'Delegated content-only context refresh.',
|
|
40
|
+
'Delegated Plan-bounded supporting-source context refresh.',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export interface CorrectiveRecoveryAssessment {
|
|
44
|
+
task: TaskState;
|
|
45
|
+
decision: CorrectiveDecisionEvent;
|
|
46
|
+
directRebind: TaskKnowledgeRebindRecord;
|
|
47
|
+
semanticPlanHash: string;
|
|
48
|
+
sourceAudit: PlanRiskAuditEvent;
|
|
49
|
+
reboundAudit: PlanRiskAuditEvent;
|
|
50
|
+
knowledgeMapApprovalFingerprint: string;
|
|
51
|
+
taskAuthorizationFingerprint: string;
|
|
52
|
+
delegationGrantFingerprint: string;
|
|
53
|
+
c1EvidenceFingerprint: string;
|
|
54
|
+
evidenceFingerprint: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StopOverrideContextRebindAssessment {
|
|
58
|
+
task: TaskState;
|
|
59
|
+
override: StopEscalateOverrideEvent;
|
|
60
|
+
stopDecision: CorrectiveDecisionEvent;
|
|
61
|
+
semanticPlanHash: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ContextOnlyPlanRebindEvidence {
|
|
65
|
+
directRebind: TaskKnowledgeRebindRecord;
|
|
66
|
+
semanticPlanHash: string;
|
|
67
|
+
sourceAudit: PlanRiskAuditEvent;
|
|
68
|
+
reboundAudit: PlanRiskAuditEvent;
|
|
69
|
+
planRiskAudits: PlanRiskAuditEvent[];
|
|
70
|
+
knowledgeMap: ProjectKnowledgeMap;
|
|
71
|
+
authorization: AuthorizationEvent;
|
|
72
|
+
grant: DelegationGrant;
|
|
73
|
+
autonomyContract: MilestoneAutonomyEvent | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface AppendOnlyContextPlanRebindEvidence {
|
|
77
|
+
rebindChain: TaskKnowledgeRebindRecord[];
|
|
78
|
+
semanticPlanHash: string;
|
|
79
|
+
sourceAudit: PlanRiskAuditEvent;
|
|
80
|
+
reboundAudit: PlanRiskAuditEvent;
|
|
81
|
+
planRiskAudits: PlanRiskAuditEvent[];
|
|
82
|
+
knowledgeMap: ProjectKnowledgeMap;
|
|
83
|
+
authorization: AuthorizationEvent;
|
|
84
|
+
grant: DelegationGrant;
|
|
85
|
+
autonomyContract: MilestoneAutonomyEvent | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface RecoveryChainSnapshot {
|
|
89
|
+
fingerprint: string;
|
|
90
|
+
matchingEvent: CorrectiveDecisionRecoveryEvent | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readCorrectiveDecisionRecoveryEvents(
|
|
94
|
+
store: FileStateStore,
|
|
95
|
+
task: TaskState,
|
|
96
|
+
): CorrectiveDecisionRecoveryEvent[] {
|
|
97
|
+
return store.readSidecarEvents<CorrectiveDecisionRecoveryEvent>(
|
|
98
|
+
store.taskRoot(task.projectId, task.id),
|
|
99
|
+
CORRECTIVE_DECISION_RECOVERIES_SIDECAR,
|
|
100
|
+
{ validateEvent: (event, context) => validateRecoveryEvent(event, task, context.file, context.line) },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function assessCorrectiveDecisionRecovery(
|
|
105
|
+
store: FileStateStore,
|
|
106
|
+
task: TaskState,
|
|
107
|
+
stepId: string,
|
|
108
|
+
actor: string,
|
|
109
|
+
writerToken: string | null,
|
|
110
|
+
): CorrectiveRecoveryAssessment {
|
|
111
|
+
const normalizedActor = actor.trim();
|
|
112
|
+
if (!normalizedActor) throw new WorkflowError('INVALID_ARGUMENT', 'Corrective recovery actor is required.');
|
|
113
|
+
if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
114
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
|
|
115
|
+
}
|
|
116
|
+
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
117
|
+
if (!step) throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
118
|
+
if (!['planned', 'failed'].includes(step.status)) {
|
|
119
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective recovery is unavailable while ${stepId} is ${step.status}.`);
|
|
120
|
+
}
|
|
121
|
+
const remediationEvents = readRemediationEvents(store, task);
|
|
122
|
+
const stepRemediations = remediationEvents.filter((event) => event.stepId === stepId);
|
|
123
|
+
if (stepRemediations.length !== 2) {
|
|
124
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery requires exactly two failed remediation attempts.', {
|
|
125
|
+
taskId: task.id,
|
|
126
|
+
stepId,
|
|
127
|
+
attemptCount: stepRemediations.length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const correctiveDecisions = readCorrectiveDecisionEvents(store, task);
|
|
131
|
+
const matchingDecisions = correctiveDecisions.filter((event) =>
|
|
132
|
+
event.stepId === stepId && event.triggeringAttemptCount === 3,
|
|
133
|
+
);
|
|
134
|
+
if (matchingDecisions.length !== 1) {
|
|
135
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery requires one decision for the exact Step/ordinal.', {
|
|
136
|
+
taskId: task.id,
|
|
137
|
+
stepId,
|
|
138
|
+
matchingDecisionIds: matchingDecisions.map((event) => event.eventId),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const decision = matchingDecisions[0]!;
|
|
142
|
+
if (decision.decision !== 'continue-fix') {
|
|
143
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Only continue-fix may be recovered after context refresh.', {
|
|
144
|
+
taskId: task.id,
|
|
145
|
+
stepId,
|
|
146
|
+
decision: decision.decision,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (decision.planHash === task.planHash) {
|
|
150
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective decision already binds the current Plan; recovery is unnecessary.');
|
|
151
|
+
}
|
|
152
|
+
assertCorrectiveAuditorIndependence(store, task, stepId, decision.auditor);
|
|
153
|
+
|
|
154
|
+
const rebindEvidence = assessContextOnlyPlanRebindEvidence(store, task, stepId, decision);
|
|
155
|
+
const {
|
|
156
|
+
directRebind,
|
|
157
|
+
semanticPlanHash,
|
|
158
|
+
sourceAudit,
|
|
159
|
+
reboundAudit,
|
|
160
|
+
planRiskAudits,
|
|
161
|
+
knowledgeMap,
|
|
162
|
+
authorization,
|
|
163
|
+
grant,
|
|
164
|
+
autonomyContract,
|
|
165
|
+
} = rebindEvidence;
|
|
166
|
+
const c1 = requireC1Evidence(store, task, normalizedActor, writerToken);
|
|
167
|
+
|
|
168
|
+
const knowledgeMapApprovalFingerprint = hashEvidence(knowledgeMap.approval);
|
|
169
|
+
const taskAuthorizationFingerprint = hashEvidence(authorization);
|
|
170
|
+
const delegationGrantFingerprint = hashEvidence(grant);
|
|
171
|
+
const c1EvidenceFingerprint = hashEvidence(c1);
|
|
172
|
+
const evidenceFingerprint = hashEvidence({
|
|
173
|
+
task,
|
|
174
|
+
decisionPlanHash: decision.planHash,
|
|
175
|
+
reboundPlanHash: task.planHash,
|
|
176
|
+
semanticPlanHash,
|
|
177
|
+
remediationEvents,
|
|
178
|
+
correctiveDecisions,
|
|
179
|
+
planRiskAudits,
|
|
180
|
+
knowledgeMap,
|
|
181
|
+
grant,
|
|
182
|
+
autonomyContract,
|
|
183
|
+
handoffs: readTaskHandoffEvents(store, task),
|
|
184
|
+
c1,
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
task,
|
|
188
|
+
decision,
|
|
189
|
+
directRebind,
|
|
190
|
+
semanticPlanHash,
|
|
191
|
+
sourceAudit,
|
|
192
|
+
reboundAudit,
|
|
193
|
+
knowledgeMapApprovalFingerprint,
|
|
194
|
+
taskAuthorizationFingerprint,
|
|
195
|
+
delegationGrantFingerprint,
|
|
196
|
+
c1EvidenceFingerprint,
|
|
197
|
+
evidenceFingerprint,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function appendCorrectiveDecisionRecovery(
|
|
202
|
+
store: FileStateStore,
|
|
203
|
+
task: TaskState,
|
|
204
|
+
stepId: string,
|
|
205
|
+
expectedRevision: number,
|
|
206
|
+
actor: string,
|
|
207
|
+
writerToken: string | null,
|
|
208
|
+
now: Date,
|
|
209
|
+
): CorrectiveDecisionRecoveryEvent {
|
|
210
|
+
if (task.revision !== expectedRevision) {
|
|
211
|
+
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
212
|
+
expectedRevision,
|
|
213
|
+
actualRevision: task.revision,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const preflight = assessCorrectiveDecisionRecovery(store, task, stepId, actor, writerToken);
|
|
217
|
+
const preflightRecoveryChain = snapshotRecoveryChain(store, task, preflight, actor.trim());
|
|
218
|
+
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
219
|
+
const lockFile = path.join(taskRoot, RECOVERY_LOCK);
|
|
220
|
+
let descriptor: number;
|
|
221
|
+
try {
|
|
222
|
+
descriptor = openSync(lockFile, 'wx', 0o600);
|
|
223
|
+
closeSync(descriptor);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (existsSync(lockFile)) {
|
|
226
|
+
throw new WorkflowError('LOCKED', `Corrective recovery for ${task.id} is already being serialized.`, {
|
|
227
|
+
taskId: task.id,
|
|
228
|
+
stepId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const currentTask = store.readTask(task.projectId, task.id);
|
|
235
|
+
if (currentTask.revision !== expectedRevision) {
|
|
236
|
+
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
237
|
+
expectedRevision,
|
|
238
|
+
actualRevision: currentTask.revision,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
let underLock: CorrectiveRecoveryAssessment;
|
|
242
|
+
try {
|
|
243
|
+
underLock = assessCorrectiveDecisionRecovery(store, currentTask, stepId, actor, writerToken);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new WorkflowError('STATE_CONFLICT', 'Corrective recovery evidence changed after preflight.', {
|
|
246
|
+
taskId: task.id,
|
|
247
|
+
stepId,
|
|
248
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (underLock.evidenceFingerprint !== preflight.evidenceFingerprint) {
|
|
252
|
+
throw new WorkflowError('STATE_CONFLICT', 'Corrective recovery evidence changed after preflight.', {
|
|
253
|
+
taskId: task.id,
|
|
254
|
+
stepId,
|
|
255
|
+
preflightEvidenceFingerprint: preflight.evidenceFingerprint,
|
|
256
|
+
currentEvidenceFingerprint: underLock.evidenceFingerprint,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const underLockRecoveryChain = snapshotRecoveryChain(store, currentTask, underLock, actor.trim());
|
|
260
|
+
if (underLockRecoveryChain.matchingEvent) {
|
|
261
|
+
return underLockRecoveryChain.matchingEvent;
|
|
262
|
+
}
|
|
263
|
+
if (underLockRecoveryChain.fingerprint !== preflightRecoveryChain.fingerprint) {
|
|
264
|
+
throw new WorkflowError('STATE_CONFLICT', 'Corrective recovery chain changed after preflight.', {
|
|
265
|
+
taskId: task.id,
|
|
266
|
+
stepId,
|
|
267
|
+
preflightRecoveryChainFingerprint: preflightRecoveryChain.fingerprint,
|
|
268
|
+
currentRecoveryChainFingerprint: underLockRecoveryChain.fingerprint,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return store.appendSidecarEvent<CorrectiveDecisionRecoveryEvent>(
|
|
272
|
+
taskRoot,
|
|
273
|
+
CORRECTIVE_DECISION_RECOVERIES_SIDECAR,
|
|
274
|
+
recoveryPayload(underLock, actor.trim(), now),
|
|
275
|
+
{ validateEvent: (event, context) => validateRecoveryEvent(event, currentTask, context.file, context.line) },
|
|
276
|
+
);
|
|
277
|
+
} finally {
|
|
278
|
+
if (existsSync(lockFile)) unlinkSync(lockFile);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function snapshotRecoveryChain(
|
|
283
|
+
store: FileStateStore,
|
|
284
|
+
task: TaskState,
|
|
285
|
+
assessment: CorrectiveRecoveryAssessment,
|
|
286
|
+
actor: string,
|
|
287
|
+
): RecoveryChainSnapshot {
|
|
288
|
+
const events = readCorrectiveDecisionRecoveryEvents(store, task);
|
|
289
|
+
const sameBinding = events.filter((event) =>
|
|
290
|
+
event.correctiveDecisionEventId === assessment.decision.eventId
|
|
291
|
+
&& event.stepId === assessment.decision.stepId
|
|
292
|
+
&& event.triggeringAttemptCount === assessment.decision.triggeringAttemptCount,
|
|
293
|
+
);
|
|
294
|
+
if (sameBinding.length > 1) {
|
|
295
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple recoveries exist for one corrective decision binding.', {
|
|
296
|
+
taskId: task.id,
|
|
297
|
+
stepId: assessment.decision.stepId,
|
|
298
|
+
eventIds: sameBinding.map((event) => event.eventId),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const matchingEvent = sameBinding[0] ?? null;
|
|
302
|
+
if (matchingEvent) assertRecoveryMatchesAssessment(matchingEvent, assessment, actor);
|
|
303
|
+
return { fingerprint: hashEvidence(events), matchingEvent };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function requireCorrectiveDecisionRecoveryForRun(
|
|
307
|
+
store: FileStateStore,
|
|
308
|
+
task: TaskState,
|
|
309
|
+
stepId: string,
|
|
310
|
+
actor: string,
|
|
311
|
+
writerToken: string | null,
|
|
312
|
+
): CorrectiveDecisionRecoveryEvent {
|
|
313
|
+
const assessment = assessCorrectiveDecisionRecovery(store, task, stepId, actor, writerToken);
|
|
314
|
+
const events = readCorrectiveDecisionRecoveryEvents(store, task);
|
|
315
|
+
const matches = events.filter((event) =>
|
|
316
|
+
event.correctiveDecisionEventId === assessment.decision.eventId
|
|
317
|
+
&& event.stepId === stepId
|
|
318
|
+
&& event.triggeringAttemptCount === assessment.decision.triggeringAttemptCount,
|
|
319
|
+
);
|
|
320
|
+
if (matches.length !== 1) {
|
|
321
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A validated corrective recovery is required before this run.', {
|
|
322
|
+
taskId: task.id,
|
|
323
|
+
stepId,
|
|
324
|
+
correctiveDecisionEventId: assessment.decision.eventId,
|
|
325
|
+
recoveryCount: matches.length,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const recovery = matches[0]!;
|
|
329
|
+
assertRecoveryMatchesAssessment(recovery, assessment, actor.trim());
|
|
330
|
+
return recovery;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function assessCorrectiveDecisionRecoveryForNavigation(
|
|
334
|
+
store: FileStateStore,
|
|
335
|
+
task: TaskState,
|
|
336
|
+
stepId: string,
|
|
337
|
+
): CorrectiveRecoveryAssessment {
|
|
338
|
+
const posture = readTaskC1Posture(store, task);
|
|
339
|
+
const lease = readWriterLease(store, task);
|
|
340
|
+
const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
|
|
341
|
+
return assessCorrectiveDecisionRecovery(store, task, stepId, actor, lease?.token ?? null);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function readValidatedStopOverrideContextRebindIfApplicable(
|
|
345
|
+
store: FileStateStore,
|
|
346
|
+
task: TaskState,
|
|
347
|
+
stepId: string,
|
|
348
|
+
repositoryRoot: string,
|
|
349
|
+
): StopOverrideContextRebindAssessment | null {
|
|
350
|
+
if (!task.planHash) return null;
|
|
351
|
+
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
352
|
+
const triggeringAttemptCount = remediationEvents.length + 1;
|
|
353
|
+
const staleStops = readCorrectiveDecisionEvents(store, task).filter((event) =>
|
|
354
|
+
event.stepId === stepId
|
|
355
|
+
&& event.triggeringAttemptCount === triggeringAttemptCount
|
|
356
|
+
&& event.decision === 'stop-escalate'
|
|
357
|
+
&& event.planHash !== task.planHash,
|
|
358
|
+
);
|
|
359
|
+
if (staleStops.length === 0) return null;
|
|
360
|
+
if (staleStops.length !== 1) {
|
|
361
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple stale stop decisions match one guarded retry binding.', {
|
|
362
|
+
taskId: task.id,
|
|
363
|
+
stepId,
|
|
364
|
+
triggeringAttemptCount,
|
|
365
|
+
decisionEventIds: staleStops.map((event) => event.eventId),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return assessStopOverrideContextRebind(store, task, stepId, staleStops[0]!, remediationEvents, repositoryRoot);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function assessStopOverrideContextRebind(
|
|
372
|
+
store: FileStateStore,
|
|
373
|
+
task: TaskState,
|
|
374
|
+
stepId: string,
|
|
375
|
+
stopDecision: CorrectiveDecisionEvent,
|
|
376
|
+
remediationEvents: ReturnType<typeof readRemediationEvents>,
|
|
377
|
+
repositoryRoot: string,
|
|
378
|
+
): StopOverrideContextRebindAssessment {
|
|
379
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
380
|
+
if (!step) throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
381
|
+
if (!['planned', 'failed', 'in_progress'].includes(step.status)) {
|
|
382
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Attempt-four continuation is unavailable while ${stepId} is ${step.status}.`);
|
|
383
|
+
}
|
|
384
|
+
if (
|
|
385
|
+
remediationEvents.length !== 3
|
|
386
|
+
|| remediationEvents[0]?.attemptOrdinal !== 1
|
|
387
|
+
|| remediationEvents[0]?.mode !== 'ordinary'
|
|
388
|
+
|| remediationEvents[1]?.attemptOrdinal !== 2
|
|
389
|
+
|| remediationEvents[1]?.mode !== 'ordinary'
|
|
390
|
+
|| remediationEvents[2]?.attemptOrdinal !== 3
|
|
391
|
+
|| remediationEvents[2]?.mode !== 'corrective'
|
|
392
|
+
) {
|
|
393
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound stop override requires the exact three-attempt beta.11 history.', {
|
|
394
|
+
taskId: task.id,
|
|
395
|
+
stepId,
|
|
396
|
+
attempts: remediationEvents.map((event) => ({ ordinal: event.attemptOrdinal, mode: event.mode })),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const overrides = readStopEscalateOverrideEvents(store, task).filter((event) =>
|
|
400
|
+
event.stepId === stepId && event.stopDecisionEventId === stopDecision.eventId,
|
|
401
|
+
);
|
|
402
|
+
if (overrides.length !== 1) {
|
|
403
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound attempt four requires one exact applied stop override.', {
|
|
404
|
+
taskId: task.id,
|
|
405
|
+
stepId,
|
|
406
|
+
stopDecisionEventId: stopDecision.eventId,
|
|
407
|
+
overrideEventIds: overrides.map((event) => event.eventId),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const override = overrides[0]!;
|
|
411
|
+
if (
|
|
412
|
+
stopDecision.triggeringAttemptCount !== 4
|
|
413
|
+
|| stopDecision.planHash !== override.planHash
|
|
414
|
+
|| task.planHash === override.planHash
|
|
415
|
+
|| override.coveredEventIds.length !== remediationEvents.length
|
|
416
|
+
|| override.coveredEventIds.some((eventId, index) => eventId !== remediationEvents[index]!.eventId)
|
|
417
|
+
) {
|
|
418
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context-rebound stop override no longer binds the exact source Plan/history.', {
|
|
419
|
+
taskId: task.id,
|
|
420
|
+
stepId,
|
|
421
|
+
overrideEventId: override.eventId,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
assertCorrectiveAuditorIndependence(store, task, stepId, stopDecision.auditor);
|
|
425
|
+
const rebindEvidence = assessAppendOnlyContextPlanRebindEvidence(
|
|
426
|
+
store,
|
|
427
|
+
task,
|
|
428
|
+
stepId,
|
|
429
|
+
stopDecision,
|
|
430
|
+
);
|
|
431
|
+
const verifiedHeadRecoveries = requireStopOverrideHeadContinuity(
|
|
432
|
+
repositoryRoot,
|
|
433
|
+
task,
|
|
434
|
+
override,
|
|
435
|
+
rebindEvidence.authorization,
|
|
436
|
+
);
|
|
437
|
+
requireEvidenceBoundAuditTaskRevision(
|
|
438
|
+
task,
|
|
439
|
+
rebindEvidence.reboundAudit,
|
|
440
|
+
verifiedHeadRecoveries,
|
|
441
|
+
step.status === 'in_progress',
|
|
442
|
+
);
|
|
443
|
+
const stopAt = Date.parse(stopDecision.recordedAt);
|
|
444
|
+
const overrideAt = Date.parse(override.recordedAt);
|
|
445
|
+
const firstRebindAt = Date.parse(rebindEvidence.rebindChain[0]!.recordedAt);
|
|
446
|
+
const lastRebindAt = Date.parse(rebindEvidence.rebindChain.at(-1)!.recordedAt);
|
|
447
|
+
if (
|
|
448
|
+
![stopAt, overrideAt, firstRebindAt, lastRebindAt].every(Number.isFinite)
|
|
449
|
+
|| overrideAt < stopAt
|
|
450
|
+
|| firstRebindAt < overrideAt
|
|
451
|
+
|| lastRebindAt < firstRebindAt
|
|
452
|
+
) {
|
|
453
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop override and context-rebind chronology is invalid.', {
|
|
454
|
+
taskId: task.id,
|
|
455
|
+
stepId,
|
|
456
|
+
stopDecisionRecordedAt: stopDecision.recordedAt,
|
|
457
|
+
overrideRecordedAt: override.recordedAt,
|
|
458
|
+
firstRebindRecordedAt: rebindEvidence.rebindChain[0]!.recordedAt,
|
|
459
|
+
lastRebindRecordedAt: rebindEvidence.rebindChain.at(-1)!.recordedAt,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
task,
|
|
464
|
+
override,
|
|
465
|
+
stopDecision,
|
|
466
|
+
semanticPlanHash: rebindEvidence.semanticPlanHash,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function recoveryPayload(
|
|
471
|
+
assessment: CorrectiveRecoveryAssessment,
|
|
472
|
+
actor: string,
|
|
473
|
+
now: Date,
|
|
474
|
+
): Omit<CorrectiveDecisionRecoveryEvent, 'previousEventHash' | 'eventHash'> {
|
|
475
|
+
const { task, decision, directRebind, sourceAudit, reboundAudit } = assessment;
|
|
476
|
+
return {
|
|
477
|
+
eventId: `CDR-${createUlid(now.getTime())}`,
|
|
478
|
+
recordedAt: now.toISOString(),
|
|
479
|
+
taskId: task.id,
|
|
480
|
+
stepId: decision.stepId,
|
|
481
|
+
triggeringAttemptCount: decision.triggeringAttemptCount,
|
|
482
|
+
correctiveDecisionEventId: decision.eventId,
|
|
483
|
+
correctiveDecisionEventHash: decision.eventHash,
|
|
484
|
+
decisionPlanHash: decision.planHash,
|
|
485
|
+
reboundPlanHash: task.planHash!,
|
|
486
|
+
semanticPlanHash: assessment.semanticPlanHash,
|
|
487
|
+
fromKnowledgeMapRevision: directRebind.fromKnowledgeMapRevision,
|
|
488
|
+
fromKnowledgeMapHash: directRebind.fromKnowledgeMapHash,
|
|
489
|
+
toKnowledgeMapRevision: directRebind.toKnowledgeMapRevision,
|
|
490
|
+
toKnowledgeMapHash: directRebind.toKnowledgeMapHash,
|
|
491
|
+
rebindRecordedAt: directRebind.recordedAt,
|
|
492
|
+
sourcePlanRiskAuditEventId: sourceAudit.eventId,
|
|
493
|
+
sourcePlanRiskAuditEventHash: sourceAudit.eventHash,
|
|
494
|
+
reboundPlanRiskAuditEventId: reboundAudit.eventId,
|
|
495
|
+
reboundPlanRiskAuditEventHash: reboundAudit.eventHash,
|
|
496
|
+
knowledgeMapApprovalFingerprint: assessment.knowledgeMapApprovalFingerprint,
|
|
497
|
+
taskAuthorizationFingerprint: assessment.taskAuthorizationFingerprint,
|
|
498
|
+
delegationGrantFingerprint: assessment.delegationGrantFingerprint,
|
|
499
|
+
c1EvidenceFingerprint: assessment.c1EvidenceFingerprint,
|
|
500
|
+
evidenceFingerprint: assessment.evidenceFingerprint,
|
|
501
|
+
actor,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function hashEvidence(value: unknown): string {
|
|
506
|
+
return sha256Hex(canonicalJsonStringify(value));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function assessContextOnlyPlanRebindEvidence(
|
|
510
|
+
store: FileStateStore,
|
|
511
|
+
task: TaskState,
|
|
512
|
+
stepId: string,
|
|
513
|
+
sourceDecision: CorrectiveDecisionEvent,
|
|
514
|
+
options: { allowTaskRevisionAdvance?: boolean } = {},
|
|
515
|
+
): ContextOnlyPlanRebindEvidence {
|
|
516
|
+
if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
517
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
|
|
518
|
+
}
|
|
519
|
+
if (sourceDecision.planHash === task.planHash) {
|
|
520
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Context-only Plan rebind validation requires distinct source and current Plan hashes.');
|
|
521
|
+
}
|
|
522
|
+
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
523
|
+
const planEvidence = comparePlanArtifacts(taskRoot, sourceDecision.planHash, task.planHash);
|
|
524
|
+
const currentAudit = readCurrentPlanRiskAudit(store, task);
|
|
525
|
+
if (!currentAudit || !currentAudit.reviewRequiredStepIds.includes(stepId)) {
|
|
526
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
|
|
527
|
+
}
|
|
528
|
+
const planRiskAudits = readPlanRiskAuditEvents(store, task);
|
|
529
|
+
const { sourceAudit, reboundAudit } = requireMechanicalAuditRebound(
|
|
530
|
+
task,
|
|
531
|
+
sourceDecision,
|
|
532
|
+
planRiskAudits,
|
|
533
|
+
options.allowTaskRevisionAdvance ?? false,
|
|
534
|
+
);
|
|
535
|
+
const directRebind = requireDirectRebind(task, sourceDecision, sourceAudit);
|
|
536
|
+
assertRebindMatchesPlans(directRebind, planEvidence);
|
|
537
|
+
if (
|
|
538
|
+
sourceAudit.knowledgeMapRevision !== directRebind.fromKnowledgeMapRevision
|
|
539
|
+
|| sourceAudit.knowledgeMapHash !== directRebind.fromKnowledgeMapHash
|
|
540
|
+
|| reboundAudit.knowledgeMapRevision !== directRebind.toKnowledgeMapRevision
|
|
541
|
+
|| reboundAudit.knowledgeMapHash !== directRebind.toKnowledgeMapHash
|
|
542
|
+
) {
|
|
543
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit pair does not bind the direct Knowledge rebind endpoints.');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const knowledgeMap = store.readKnowledgeMap(task.projectId);
|
|
547
|
+
const authorization = requireRefreshAuthorization(task);
|
|
548
|
+
const { grant, autonomyContract } = requireRefreshAuthority(
|
|
549
|
+
store,
|
|
550
|
+
task,
|
|
551
|
+
knowledgeMap,
|
|
552
|
+
authorization,
|
|
553
|
+
directRebind,
|
|
554
|
+
);
|
|
555
|
+
assertRefreshChronology(knowledgeMap, authorization, directRebind, reboundAudit);
|
|
556
|
+
return {
|
|
557
|
+
directRebind,
|
|
558
|
+
semanticPlanHash: planEvidence.semanticPlanHash,
|
|
559
|
+
sourceAudit,
|
|
560
|
+
reboundAudit,
|
|
561
|
+
planRiskAudits,
|
|
562
|
+
knowledgeMap,
|
|
563
|
+
authorization,
|
|
564
|
+
grant,
|
|
565
|
+
autonomyContract,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function assessAppendOnlyContextPlanRebindEvidence(
|
|
570
|
+
store: FileStateStore,
|
|
571
|
+
task: TaskState,
|
|
572
|
+
stepId: string,
|
|
573
|
+
sourceDecision: CorrectiveDecisionEvent,
|
|
574
|
+
): AppendOnlyContextPlanRebindEvidence {
|
|
575
|
+
if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
576
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
|
|
577
|
+
}
|
|
578
|
+
if (sourceDecision.planHash === task.planHash) {
|
|
579
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Append-only Plan rebind validation requires distinct source and current Plan hashes.');
|
|
580
|
+
}
|
|
581
|
+
const planRiskAudits = readPlanRiskAuditEvents(store, task);
|
|
582
|
+
const rebindChain = requireAppendOnlyRebindChain(task, sourceDecision, planRiskAudits);
|
|
583
|
+
const planEvidence = comparePlanRebindChainArtifacts(
|
|
584
|
+
store.taskRoot(task.projectId, task.id),
|
|
585
|
+
sourceDecision.planHash,
|
|
586
|
+
task.planHash,
|
|
587
|
+
rebindChain,
|
|
588
|
+
);
|
|
589
|
+
const { sourceAudit, reboundAudit, auditChain } = requireMechanicalAuditReboundChain(
|
|
590
|
+
task,
|
|
591
|
+
sourceDecision,
|
|
592
|
+
planRiskAudits,
|
|
593
|
+
rebindChain,
|
|
594
|
+
);
|
|
595
|
+
if (!reboundAudit.stepClassifications.some((classification) =>
|
|
596
|
+
classification.stepId === stepId && classification.reviewRequired
|
|
597
|
+
)) {
|
|
598
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
|
|
599
|
+
}
|
|
600
|
+
for (let index = 0; index < rebindChain.length; index += 1) {
|
|
601
|
+
const rebind = rebindChain[index]!;
|
|
602
|
+
const fromBinding = planEvidence.bindings.get(rebind.previousPlanHash);
|
|
603
|
+
const toBinding = planEvidence.bindings.get(rebind.reboundPlanHash);
|
|
604
|
+
if (!fromBinding || !toBinding) {
|
|
605
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind references an unverified Plan artifact.', {
|
|
606
|
+
previousPlanHash: rebind.previousPlanHash,
|
|
607
|
+
reboundPlanHash: rebind.reboundPlanHash,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
assertRebindMatchesPlans(rebind, { decisionBinding: fromBinding, reboundBinding: toBinding });
|
|
611
|
+
const fromAudit = auditChain[index]!;
|
|
612
|
+
const toAudit = auditChain[index + 1]!;
|
|
613
|
+
if (
|
|
614
|
+
fromAudit.knowledgeMapRevision !== rebind.fromKnowledgeMapRevision
|
|
615
|
+
|| fromAudit.knowledgeMapHash !== rebind.fromKnowledgeMapHash
|
|
616
|
+
|| toAudit.knowledgeMapRevision !== rebind.toKnowledgeMapRevision
|
|
617
|
+
|| toAudit.knowledgeMapHash !== rebind.toKnowledgeMapHash
|
|
618
|
+
) {
|
|
619
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit chain does not bind every Knowledge rebind endpoint.', {
|
|
620
|
+
rebindIndex: index,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const knowledgeMap = store.readKnowledgeMap(task.projectId);
|
|
626
|
+
const authorization = requireRefreshAuthorization(task);
|
|
627
|
+
const finalRebind = rebindChain.at(-1)!;
|
|
628
|
+
const { grant, autonomyContract } = requireRefreshAuthority(
|
|
629
|
+
store,
|
|
630
|
+
task,
|
|
631
|
+
knowledgeMap,
|
|
632
|
+
authorization,
|
|
633
|
+
finalRebind,
|
|
634
|
+
);
|
|
635
|
+
assertRefreshChronology(knowledgeMap, authorization, finalRebind, reboundAudit);
|
|
636
|
+
return {
|
|
637
|
+
rebindChain,
|
|
638
|
+
semanticPlanHash: planEvidence.semanticPlanHash,
|
|
639
|
+
sourceAudit,
|
|
640
|
+
reboundAudit,
|
|
641
|
+
planRiskAudits,
|
|
642
|
+
knowledgeMap,
|
|
643
|
+
authorization,
|
|
644
|
+
grant,
|
|
645
|
+
autonomyContract,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function comparePlanRebindChainArtifacts(
|
|
650
|
+
taskRoot: string,
|
|
651
|
+
sourcePlanHash: string,
|
|
652
|
+
currentPlanHash: string,
|
|
653
|
+
rebindChain: TaskKnowledgeRebindRecord[],
|
|
654
|
+
): {
|
|
655
|
+
semanticPlanHash: string;
|
|
656
|
+
bindings: Map<string, { revision: number; hash: string }>;
|
|
657
|
+
} {
|
|
658
|
+
const planHashes = [sourcePlanHash, ...rebindChain.map((rebind) => rebind.reboundPlanHash)];
|
|
659
|
+
if (planHashes.at(-1) !== currentPlanHash || new Set(planHashes).size !== planHashes.length) {
|
|
660
|
+
throw new WorkflowError('STATE_CORRUPT', 'Knowledge rebind chain does not form one acyclic source-to-current Plan sequence.');
|
|
661
|
+
}
|
|
662
|
+
const contents = planHashes.map((planHash) => readVerifiedPlanArtifact(taskRoot, planHash, currentPlanHash));
|
|
663
|
+
const bindings = new Map<string, { revision: number; hash: string }>();
|
|
664
|
+
let semanticPlanHash: string | null = null;
|
|
665
|
+
for (let index = 0; index < contents.length; index += 1) {
|
|
666
|
+
const planHash = planHashes[index]!;
|
|
667
|
+
bindings.set(planHash, normalizePlanKnowledgeBinding(contents[index]!, `Plan ${planHash}`).binding);
|
|
668
|
+
if (index === 0) continue;
|
|
669
|
+
const adjacentSemanticHash = assertKnowledgeBindingOnlyPlanChange(contents[index - 1]!, contents[index]!);
|
|
670
|
+
if (semanticPlanHash !== null && semanticPlanHash !== adjacentSemanticHash) {
|
|
671
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain does not preserve one semantic Plan identity.');
|
|
672
|
+
}
|
|
673
|
+
semanticPlanHash = adjacentSemanticHash;
|
|
674
|
+
}
|
|
675
|
+
if (!semanticPlanHash) {
|
|
676
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain has no comparable Plan hop.');
|
|
677
|
+
}
|
|
678
|
+
return { semanticPlanHash, bindings };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function readVerifiedPlanArtifact(taskRoot: string, planHash: string, currentPlanHash: string): string {
|
|
682
|
+
assertHash64(planHash, 'Plan hash');
|
|
683
|
+
const file = planHash === currentPlanHash
|
|
684
|
+
? path.join(taskRoot, 'plan.md')
|
|
685
|
+
: path.join(taskRoot, '.versions', `plan.md.${planHash}`);
|
|
686
|
+
if (!existsSync(file)) {
|
|
687
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact required by the append-only rebind chain is missing.', {
|
|
688
|
+
planHash,
|
|
689
|
+
file,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
const content = readFileSync(file, 'utf8');
|
|
693
|
+
if (sha256Hex(content) !== planHash) {
|
|
694
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its append-only rebind hash.', {
|
|
695
|
+
planHash,
|
|
696
|
+
file,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return content;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function comparePlanArtifacts(taskRoot: string, decisionPlanHash: string, reboundPlanHash: string): {
|
|
703
|
+
semanticPlanHash: string;
|
|
704
|
+
decisionBinding: { revision: number; hash: string };
|
|
705
|
+
reboundBinding: { revision: number; hash: string };
|
|
706
|
+
} {
|
|
707
|
+
assertHash64(decisionPlanHash, 'Corrective decision Plan hash');
|
|
708
|
+
assertHash64(reboundPlanHash, 'Current Plan hash');
|
|
709
|
+
const historicalFile = path.join(taskRoot, '.versions', `plan.md.${decisionPlanHash}`);
|
|
710
|
+
if (!existsSync(historicalFile)) {
|
|
711
|
+
throw new WorkflowError('STATE_CORRUPT', 'Historical corrective-decision Plan artifact is missing.', {
|
|
712
|
+
decisionPlanHash,
|
|
713
|
+
historicalFile,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
const historical = readFileSync(historicalFile, 'utf8');
|
|
717
|
+
const current = readFileSync(path.join(taskRoot, 'plan.md'), 'utf8');
|
|
718
|
+
if (sha256Hex(historical) !== decisionPlanHash || sha256Hex(current) !== reboundPlanHash) {
|
|
719
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its recorded hash.');
|
|
720
|
+
}
|
|
721
|
+
const semanticPlanHash = assertKnowledgeBindingOnlyPlanChange(historical, current);
|
|
722
|
+
const decision = normalizePlanKnowledgeBinding(historical, 'historical');
|
|
723
|
+
const rebound = normalizePlanKnowledgeBinding(current, 'current');
|
|
724
|
+
return {
|
|
725
|
+
semanticPlanHash,
|
|
726
|
+
decisionBinding: decision.binding,
|
|
727
|
+
reboundBinding: rebound.binding,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function normalizePlanKnowledgeBinding(content: string, label: string): {
|
|
732
|
+
normalized: string;
|
|
733
|
+
binding: { revision: number; hash: string };
|
|
734
|
+
} {
|
|
735
|
+
const matches = [...content.matchAll(KNOWLEDGE_LINE)];
|
|
736
|
+
if (matches.length !== 1) {
|
|
737
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} Plan must contain exactly one canonical Knowledge Map binding line.`, {
|
|
738
|
+
matchCount: matches.length,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
const match = matches[0]!;
|
|
742
|
+
return {
|
|
743
|
+
normalized: content.slice(0, match.index) + KNOWLEDGE_SENTINEL + content.slice(match.index! + match[0].length),
|
|
744
|
+
binding: { revision: Number(match[1]), hash: match[2]! },
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function requireMechanicalAuditRebound(
|
|
749
|
+
task: TaskState,
|
|
750
|
+
decision: CorrectiveDecisionEvent,
|
|
751
|
+
events: PlanRiskAuditEvent[],
|
|
752
|
+
allowTaskRevisionAdvance = false,
|
|
753
|
+
): { sourceAudit: PlanRiskAuditEvent; reboundAudit: PlanRiskAuditEvent } {
|
|
754
|
+
const sources = events.filter((event) => event.planHash === decision.planHash);
|
|
755
|
+
const rebounds = events.filter((event) => event.planHash === task.planHash);
|
|
756
|
+
if (sources.length !== 1 || rebounds.length !== 1) {
|
|
757
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery requires one source and one rebound Plan Risk Audit.', {
|
|
758
|
+
sourceAuditIds: sources.map((event) => event.eventId),
|
|
759
|
+
reboundAuditIds: rebounds.map((event) => event.eventId),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const sourceAudit = sources[0]!;
|
|
763
|
+
const reboundAudit = rebounds[0]!;
|
|
764
|
+
const sourceIndex = events.indexOf(sourceAudit);
|
|
765
|
+
const reboundIndex = events.indexOf(reboundAudit);
|
|
766
|
+
if (reboundIndex !== sourceIndex + 1 || reboundAudit.previousEventHash !== sourceAudit.eventHash || reboundIndex !== events.length - 1) {
|
|
767
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebound is not the immediate append-only successor of the source audit.');
|
|
768
|
+
}
|
|
769
|
+
const expectedSummary = `${sourceAudit.summary} Rebound automatically after content-only Task context refresh.`;
|
|
770
|
+
if (
|
|
771
|
+
reboundAudit.taskId !== sourceAudit.taskId
|
|
772
|
+
|| reboundAudit.briefHash !== sourceAudit.briefHash
|
|
773
|
+
|| reboundAudit.planner !== sourceAudit.planner
|
|
774
|
+
|| reboundAudit.auditor !== sourceAudit.auditor
|
|
775
|
+
|| reboundAudit.decision !== sourceAudit.decision
|
|
776
|
+
|| reboundAudit.splitRationale !== sourceAudit.splitRationale
|
|
777
|
+
|| reboundAudit.summary !== expectedSummary
|
|
778
|
+
|| canonicalJsonStringify(reboundAudit.stepClassifications) !== canonicalJsonStringify(sourceAudit.stepClassifications)
|
|
779
|
+
) {
|
|
780
|
+
throw new WorkflowError('STATE_CORRUPT', 'Current Plan Risk Audit is not a mechanical content-refresh rebound.');
|
|
781
|
+
}
|
|
782
|
+
if (sourceAudit.briefHash !== task.briefHash || reboundAudit.briefHash !== task.briefHash) {
|
|
783
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebound does not preserve the current Brief binding.');
|
|
784
|
+
}
|
|
785
|
+
const authorizedTaskRevision = reboundAudit.taskRevision + 1;
|
|
786
|
+
if (
|
|
787
|
+
(!allowTaskRevisionAdvance && authorizedTaskRevision !== task.revision)
|
|
788
|
+
|| (allowTaskRevisionAdvance && authorizedTaskRevision > task.revision)
|
|
789
|
+
) {
|
|
790
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit does not bind the required Task authorization boundary.', {
|
|
791
|
+
reboundAuditTaskRevision: reboundAudit.taskRevision,
|
|
792
|
+
currentTaskRevision: task.revision,
|
|
793
|
+
allowTaskRevisionAdvance,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return { sourceAudit, reboundAudit };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function requireDirectRebind(
|
|
800
|
+
task: TaskState,
|
|
801
|
+
decision: CorrectiveDecisionEvent,
|
|
802
|
+
sourceAudit: PlanRiskAuditEvent,
|
|
803
|
+
): TaskKnowledgeRebindRecord {
|
|
804
|
+
for (let index = 1; index < task.knowledgeRebinds.length; index += 1) {
|
|
805
|
+
if (Date.parse(task.knowledgeRebinds[index]!.recordedAt) <= Date.parse(task.knowledgeRebinds[index - 1]!.recordedAt)) {
|
|
806
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Knowledge rebind timestamps must be strictly increasing.');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const boundary = Math.max(Date.parse(decision.recordedAt), Date.parse(sourceAudit.recordedAt));
|
|
810
|
+
const afterBoundary = task.knowledgeRebinds.filter((record) => Date.parse(record.recordedAt) > boundary);
|
|
811
|
+
if (afterBoundary.length !== 1) {
|
|
812
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Legacy recovery requires exactly one Knowledge rebind after the decision/source-audit binding.', {
|
|
813
|
+
rebindCount: afterBoundary.length,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
const selected = afterBoundary[0]!;
|
|
817
|
+
if (task.knowledgeRebinds.at(-1) !== selected || selected.previousPlanHash !== decision.planHash || selected.reboundPlanHash !== task.planHash) {
|
|
818
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance is not one direct decision-Plan to current-Plan hop.');
|
|
819
|
+
}
|
|
820
|
+
return selected;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function requireAppendOnlyRebindChain(
|
|
824
|
+
task: TaskState,
|
|
825
|
+
decision: CorrectiveDecisionEvent,
|
|
826
|
+
planRiskAudits: PlanRiskAuditEvent[],
|
|
827
|
+
): TaskKnowledgeRebindRecord[] {
|
|
828
|
+
for (let index = 0; index < task.knowledgeRebinds.length; index += 1) {
|
|
829
|
+
const recordedAt = Date.parse(task.knowledgeRebinds[index]!.recordedAt);
|
|
830
|
+
if (
|
|
831
|
+
!Number.isFinite(recordedAt)
|
|
832
|
+
|| (index > 0 && recordedAt <= Date.parse(task.knowledgeRebinds[index - 1]!.recordedAt))
|
|
833
|
+
) {
|
|
834
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Knowledge rebind timestamps must be finite and strictly increasing.');
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const sourceAudits = planRiskAudits.filter((event) => event.planHash === decision.planHash);
|
|
838
|
+
if (sourceAudits.length !== 1) {
|
|
839
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one exact source Plan Risk Audit.', {
|
|
840
|
+
sourceAuditIds: sourceAudits.map((event) => event.eventId),
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const boundary = Math.max(Date.parse(decision.recordedAt), Date.parse(sourceAudits[0]!.recordedAt));
|
|
844
|
+
if (!Number.isFinite(boundary)) {
|
|
845
|
+
throw new WorkflowError('STATE_CORRUPT', 'Stop decision or source Plan Risk Audit timestamp is invalid.');
|
|
846
|
+
}
|
|
847
|
+
const chain = task.knowledgeRebinds.filter((record) => Date.parse(record.recordedAt) > boundary);
|
|
848
|
+
if (chain.length < 1) {
|
|
849
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop-override continuation requires at least one Knowledge rebind after its source authority.');
|
|
850
|
+
}
|
|
851
|
+
if (task.knowledgeRebinds.at(-1) !== chain.at(-1)) {
|
|
852
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind chain does not terminate at the current Task boundary.');
|
|
853
|
+
}
|
|
854
|
+
let expectedPlanHash = decision.planHash;
|
|
855
|
+
for (const record of chain) {
|
|
856
|
+
if (record.previousPlanHash !== expectedPlanHash || record.reboundPlanHash === record.previousPlanHash) {
|
|
857
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance is not one contiguous source-to-current Plan chain.', {
|
|
858
|
+
expectedPreviousPlanHash: expectedPlanHash,
|
|
859
|
+
actualPreviousPlanHash: record.previousPlanHash,
|
|
860
|
+
reboundPlanHash: record.reboundPlanHash,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
expectedPlanHash = record.reboundPlanHash;
|
|
864
|
+
}
|
|
865
|
+
if (expectedPlanHash !== task.planHash) {
|
|
866
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance does not terminate at the current Task Plan.', {
|
|
867
|
+
expectedCurrentPlanHash: task.planHash,
|
|
868
|
+
actualTerminalPlanHash: expectedPlanHash,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
return chain;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function requireMechanicalAuditReboundChain(
|
|
875
|
+
task: TaskState,
|
|
876
|
+
decision: CorrectiveDecisionEvent,
|
|
877
|
+
events: PlanRiskAuditEvent[],
|
|
878
|
+
rebindChain: TaskKnowledgeRebindRecord[],
|
|
879
|
+
): { sourceAudit: PlanRiskAuditEvent; reboundAudit: PlanRiskAuditEvent; auditChain: PlanRiskAuditEvent[] } {
|
|
880
|
+
const planHashes = [decision.planHash, ...rebindChain.map((record) => record.reboundPlanHash)];
|
|
881
|
+
const auditChain = planHashes.map((planHash) => {
|
|
882
|
+
const matches = events.filter((event) => event.planHash === planHash);
|
|
883
|
+
if (matches.length !== 1) {
|
|
884
|
+
throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one Plan Risk Audit for every Plan in the rebind chain.', {
|
|
885
|
+
planHash,
|
|
886
|
+
auditEventIds: matches.map((event) => event.eventId),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
return matches[0]!;
|
|
890
|
+
});
|
|
891
|
+
const sourceIndex = events.indexOf(auditChain[0]!);
|
|
892
|
+
for (let index = 0; index < auditChain.length; index += 1) {
|
|
893
|
+
if (events[sourceIndex + index] !== auditChain[index]) {
|
|
894
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind evidence is not one contiguous append-only chain.');
|
|
895
|
+
}
|
|
896
|
+
if (index > 0) assertMechanicalAuditSuccessor(task, auditChain[index - 1]!, auditChain[index]!);
|
|
897
|
+
}
|
|
898
|
+
if (sourceIndex < 0 || sourceIndex + auditChain.length !== events.length) {
|
|
899
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind chain does not end at the current append-only boundary.');
|
|
900
|
+
}
|
|
901
|
+
const reboundAudit = auditChain.at(-1)!;
|
|
902
|
+
return { sourceAudit: auditChain[0]!, reboundAudit, auditChain };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function assertMechanicalAuditSuccessor(
|
|
906
|
+
task: TaskState,
|
|
907
|
+
sourceAudit: PlanRiskAuditEvent,
|
|
908
|
+
reboundAudit: PlanRiskAuditEvent,
|
|
909
|
+
): void {
|
|
910
|
+
const expectedSummary = `${sourceAudit.summary} Rebound automatically after content-only Task context refresh.`;
|
|
911
|
+
if (
|
|
912
|
+
reboundAudit.previousEventHash !== sourceAudit.eventHash
|
|
913
|
+
|| reboundAudit.taskId !== sourceAudit.taskId
|
|
914
|
+
|| reboundAudit.briefHash !== sourceAudit.briefHash
|
|
915
|
+
|| reboundAudit.briefHash !== task.briefHash
|
|
916
|
+
|| reboundAudit.planner !== sourceAudit.planner
|
|
917
|
+
|| reboundAudit.auditor !== sourceAudit.auditor
|
|
918
|
+
|| reboundAudit.decision !== sourceAudit.decision
|
|
919
|
+
|| reboundAudit.splitRationale !== sourceAudit.splitRationale
|
|
920
|
+
|| reboundAudit.summary !== expectedSummary
|
|
921
|
+
|| canonicalJsonStringify(reboundAudit.stepClassifications) !== canonicalJsonStringify(sourceAudit.stepClassifications)
|
|
922
|
+
) {
|
|
923
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit successor is not a mechanical content-refresh rebound.');
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function requireStopOverrideHeadContinuity(
|
|
928
|
+
repositoryRoot: string,
|
|
929
|
+
task: TaskState,
|
|
930
|
+
override: StopEscalateOverrideEvent,
|
|
931
|
+
authorization: AuthorizationEvent,
|
|
932
|
+
): DependencyProvenanceRecoveryRecord[] {
|
|
933
|
+
const authorizedHead = authorization.headCommit;
|
|
934
|
+
if (!authorizedHead) {
|
|
935
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution authorization does not bind a Git HEAD.');
|
|
936
|
+
}
|
|
937
|
+
if (authorizedHead === override.headCommit) return [];
|
|
938
|
+
if (!gitIsAncestor(repositoryRoot, override.headCommit, authorizedHead)) {
|
|
939
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution HEAD is not an append-only descendant of the stop-override HEAD.', {
|
|
940
|
+
overrideHeadCommit: override.headCommit,
|
|
941
|
+
authorizedHeadCommit: authorizedHead,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const output = runGit(repositoryRoot, ['rev-list', '--reverse', `${override.headCommit}..${authorizedHead}`]);
|
|
945
|
+
const commits = output ? output.split('\n') : [];
|
|
946
|
+
if (commits.length < 1) {
|
|
947
|
+
throw new WorkflowError('STATE_CORRUPT', 'Git reports distinct stop-override and authorization HEADs without an intervening commit.');
|
|
948
|
+
}
|
|
949
|
+
let expectedParent = override.headCommit;
|
|
950
|
+
let previousRecoveryIndex = -1;
|
|
951
|
+
let previousRecoveryAt = Number.NEGATIVE_INFINITY;
|
|
952
|
+
const verifiedRecoveries: DependencyProvenanceRecoveryRecord[] = [];
|
|
953
|
+
for (const commit of commits) {
|
|
954
|
+
const matches = (task.dependencyProvenanceRecoveries ?? []).filter((record) => record.commitSha === commit);
|
|
955
|
+
if (matches.length !== 1) {
|
|
956
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Git HEAD advanced through a commit without one exact dependency-provenance recovery.', {
|
|
957
|
+
commit,
|
|
958
|
+
recoveryCount: matches.length,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
const recovery = matches[0]!;
|
|
962
|
+
const recoveryIndex = (task.dependencyProvenanceRecoveries ?? []).indexOf(recovery);
|
|
963
|
+
const recoveryAt = Date.parse(recovery.recordedAt);
|
|
964
|
+
const parents = runGit(repositoryRoot, ['show', '-s', '--format=%P', commit]).split(/\s+/).filter(Boolean);
|
|
965
|
+
const filesOutput = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commit]);
|
|
966
|
+
const files = filesOutput ? filesOutput.split('\n').sort() : [];
|
|
967
|
+
if (
|
|
968
|
+
parents.length !== 1
|
|
969
|
+
|| parents[0] !== expectedParent
|
|
970
|
+
|| recovery.parentCommitSha !== expectedParent
|
|
971
|
+
|| recoveryIndex <= previousRecoveryIndex
|
|
972
|
+
|| !Number.isFinite(recoveryAt)
|
|
973
|
+
|| recoveryAt <= previousRecoveryAt
|
|
974
|
+
|| recovery.packageName !== PACKAGE_NAME
|
|
975
|
+
|| canonicalJsonStringify(recovery.files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
|
|
976
|
+
|| canonicalJsonStringify(files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
|
|
977
|
+
|| !task.systemCommits.includes(commit)
|
|
978
|
+
) {
|
|
979
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency-only Git continuity from stop override to current authorization is invalid.', {
|
|
980
|
+
commit,
|
|
981
|
+
expectedParent,
|
|
982
|
+
recordedParent: recovery.parentCommitSha,
|
|
983
|
+
parents,
|
|
984
|
+
files,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
verifiedRecoveries.push(recovery);
|
|
988
|
+
previousRecoveryIndex = recoveryIndex;
|
|
989
|
+
previousRecoveryAt = recoveryAt;
|
|
990
|
+
expectedParent = commit;
|
|
991
|
+
}
|
|
992
|
+
if (expectedParent !== authorizedHead) {
|
|
993
|
+
throw new WorkflowError('STATE_CORRUPT', 'Dependency-provenance Git chain does not terminate at the authorized HEAD.');
|
|
994
|
+
}
|
|
995
|
+
return verifiedRecoveries;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function requireEvidenceBoundAuditTaskRevision(
|
|
999
|
+
task: TaskState,
|
|
1000
|
+
reboundAudit: PlanRiskAuditEvent,
|
|
1001
|
+
verifiedHeadRecoveries: DependencyProvenanceRecoveryRecord[],
|
|
1002
|
+
allowTaskRevisionAdvance: boolean,
|
|
1003
|
+
): void {
|
|
1004
|
+
const auditAt = Date.parse(reboundAudit.recordedAt);
|
|
1005
|
+
if (!Number.isFinite(auditAt)) {
|
|
1006
|
+
throw new WorkflowError('STATE_CORRUPT', 'Current rebound Plan Risk Audit timestamp is invalid.');
|
|
1007
|
+
}
|
|
1008
|
+
const postAuditRecoveries = verifiedHeadRecoveries.filter(
|
|
1009
|
+
(recovery) => Date.parse(recovery.recordedAt) > auditAt,
|
|
1010
|
+
);
|
|
1011
|
+
const evidenceBoundTaskRevision = reboundAudit.taskRevision + 1 + postAuditRecoveries.length;
|
|
1012
|
+
if (
|
|
1013
|
+
(!allowTaskRevisionAdvance && evidenceBoundTaskRevision !== task.revision)
|
|
1014
|
+
|| (allowTaskRevisionAdvance && evidenceBoundTaskRevision > task.revision)
|
|
1015
|
+
) {
|
|
1016
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit chain does not bind the evidence-accounted Task authorization boundary.', {
|
|
1017
|
+
reboundAuditTaskRevision: reboundAudit.taskRevision,
|
|
1018
|
+
currentTaskRevision: task.revision,
|
|
1019
|
+
postAuditDependencyRecoveryCommits: postAuditRecoveries.map((recovery) => recovery.commitSha),
|
|
1020
|
+
evidenceBoundTaskRevision,
|
|
1021
|
+
allowTaskRevisionAdvance,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function assertRebindMatchesPlans(
|
|
1027
|
+
rebind: TaskKnowledgeRebindRecord,
|
|
1028
|
+
plans: {
|
|
1029
|
+
decisionBinding: { revision: number; hash: string };
|
|
1030
|
+
reboundBinding: { revision: number; hash: string };
|
|
1031
|
+
},
|
|
1032
|
+
): void {
|
|
1033
|
+
if (
|
|
1034
|
+
rebind.fromKnowledgeMapRevision !== plans.decisionBinding.revision
|
|
1035
|
+
|| rebind.fromKnowledgeMapHash !== plans.decisionBinding.hash
|
|
1036
|
+
|| rebind.toKnowledgeMapRevision !== plans.reboundBinding.revision
|
|
1037
|
+
|| rebind.toKnowledgeMapHash !== plans.reboundBinding.hash
|
|
1038
|
+
) {
|
|
1039
|
+
throw new WorkflowError('STATE_CORRUPT', 'Direct Knowledge rebind does not match the two verified Plan artifacts.');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function requireRefreshAuthorization(task: TaskState): AuthorizationEvent {
|
|
1044
|
+
const matches = task.authorizations.filter((authorization) =>
|
|
1045
|
+
authorization.kind === 'execution'
|
|
1046
|
+
&& authorization.decision === 'approved'
|
|
1047
|
+
&& authorization.planHash === task.planHash
|
|
1048
|
+
&& authorization.briefHash === task.briefHash,
|
|
1049
|
+
);
|
|
1050
|
+
if (matches.length !== 1) {
|
|
1051
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery requires one exact current execution authorization.', {
|
|
1052
|
+
taskId: task.id,
|
|
1053
|
+
authorizationCount: matches.length,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
const authorization = matches[0]!;
|
|
1057
|
+
if (
|
|
1058
|
+
authorization.authorizationMode !== 'delegated'
|
|
1059
|
+
|| !authorization.delegation
|
|
1060
|
+
|| authorization.delegation.transition !== 'task.execution_authorize'
|
|
1061
|
+
|| !REFRESH_REASONS.has(authorization.reason)
|
|
1062
|
+
) {
|
|
1063
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution authorization is not confirmed delegated context-refresh evidence.');
|
|
1064
|
+
}
|
|
1065
|
+
return authorization;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function requireRefreshAuthority(
|
|
1069
|
+
store: FileStateStore,
|
|
1070
|
+
task: TaskState,
|
|
1071
|
+
map: ProjectKnowledgeMap,
|
|
1072
|
+
authorization: AuthorizationEvent,
|
|
1073
|
+
rebind: TaskKnowledgeRebindRecord,
|
|
1074
|
+
): { grant: DelegationGrant; autonomyContract: MilestoneAutonomyEvent | null } {
|
|
1075
|
+
const approval = map.approval;
|
|
1076
|
+
const taskDelegation = authorization.delegation!;
|
|
1077
|
+
if (
|
|
1078
|
+
map.status !== 'active'
|
|
1079
|
+
|| !approval
|
|
1080
|
+
|| map.revision !== task.knowledgeMapRevision
|
|
1081
|
+
|| hashKnowledgeMap(map) !== task.knowledgeMapHash
|
|
1082
|
+
|| approval.mapHash !== task.knowledgeMapHash
|
|
1083
|
+
|| approval.authorizationMode !== 'delegated'
|
|
1084
|
+
|| !approval.delegation
|
|
1085
|
+
|| approval.delegation.transition !== 'project_memory.approve'
|
|
1086
|
+
) {
|
|
1087
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Current Knowledge Map is not the delegated approved direct-rebind target.');
|
|
1088
|
+
}
|
|
1089
|
+
const mapDelegation = approval.delegation;
|
|
1090
|
+
if (
|
|
1091
|
+
approval.actor !== authorization.actor
|
|
1092
|
+
|| mapDelegation.grantId !== taskDelegation.grantId
|
|
1093
|
+
|| mapDelegation.policyHash !== taskDelegation.policyHash
|
|
1094
|
+
|| mapDelegation.principal !== taskDelegation.principal
|
|
1095
|
+
|| mapDelegation.delegate !== taskDelegation.delegate
|
|
1096
|
+
) {
|
|
1097
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge Map and Task authorization do not share one delegated identity.');
|
|
1098
|
+
}
|
|
1099
|
+
const grant = store.readDelegation(task.projectId, taskDelegation.grantId);
|
|
1100
|
+
const expectedPolicyHash = hashDelegationPolicy(grant.projectId, {
|
|
1101
|
+
principal: grant.principal,
|
|
1102
|
+
delegate: grant.delegate,
|
|
1103
|
+
scope: grant.scope,
|
|
1104
|
+
transitions: grant.transitions,
|
|
1105
|
+
expiresAt: grant.expiresAt,
|
|
1106
|
+
});
|
|
1107
|
+
const autonomyContract = requireRefreshGrantScope(store, task, grant);
|
|
1108
|
+
if (
|
|
1109
|
+
grant.policyHash !== expectedPolicyHash
|
|
1110
|
+
|| grant.policyHash !== taskDelegation.policyHash
|
|
1111
|
+
|| grant.principal !== taskDelegation.principal
|
|
1112
|
+
|| grant.delegate !== taskDelegation.delegate
|
|
1113
|
+
|| approval.actor !== grant.delegate
|
|
1114
|
+
|| authorization.actor !== grant.delegate
|
|
1115
|
+
|| grant.status !== 'active'
|
|
1116
|
+
|| grant.revokedAt !== null
|
|
1117
|
+
|| !grant.transitions.includes('project_memory.approve')
|
|
1118
|
+
|| !grant.transitions.includes('task.execution_authorize')
|
|
1119
|
+
) {
|
|
1120
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation evidence is revoked, corrupt, mismatched, or insufficient for recovery.');
|
|
1121
|
+
}
|
|
1122
|
+
const createdAt = Date.parse(grant.createdAt);
|
|
1123
|
+
const expiresAt = Date.parse(grant.expiresAt);
|
|
1124
|
+
const approvalAt = Date.parse(approval.approvedAt);
|
|
1125
|
+
const authorizationAt = Date.parse(authorization.recordedAt);
|
|
1126
|
+
if (
|
|
1127
|
+
![createdAt, expiresAt, approvalAt, authorizationAt].every(Number.isFinite)
|
|
1128
|
+
|| approvalAt < createdAt
|
|
1129
|
+
|| authorizationAt < createdAt
|
|
1130
|
+
|| approvalAt >= expiresAt
|
|
1131
|
+
|| authorizationAt >= expiresAt
|
|
1132
|
+
) {
|
|
1133
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegated context-refresh approvals were not recorded inside the historical grant validity window.');
|
|
1134
|
+
}
|
|
1135
|
+
if (
|
|
1136
|
+
rebind.toKnowledgeMapRevision !== map.revision
|
|
1137
|
+
|| rebind.toKnowledgeMapHash !== approval.mapHash
|
|
1138
|
+
) {
|
|
1139
|
+
throw new WorkflowError('STATE_CORRUPT', 'Knowledge Map approval does not bind the direct rebind target.');
|
|
1140
|
+
}
|
|
1141
|
+
return { grant, autonomyContract };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function requireRefreshGrantScope(
|
|
1145
|
+
store: FileStateStore,
|
|
1146
|
+
task: TaskState,
|
|
1147
|
+
grant: DelegationGrant,
|
|
1148
|
+
): MilestoneAutonomyEvent | null {
|
|
1149
|
+
if (grant.scope.kind === 'project') return null;
|
|
1150
|
+
if (grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
|
|
1151
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation scope cannot authorize the Knowledge Map refresh target.');
|
|
1152
|
+
}
|
|
1153
|
+
const contracts = readMilestoneAutonomyEvents(store, task.projectId, grant.scope.id);
|
|
1154
|
+
const matches = contracts.filter((contract) =>
|
|
1155
|
+
contract.delegationGrantId === grant.id
|
|
1156
|
+
&& contract.delegationPolicyHash === grant.policyHash
|
|
1157
|
+
&& contract.principal === grant.principal
|
|
1158
|
+
&& contract.delegate === grant.delegate
|
|
1159
|
+
&& contract.expiresAt === grant.expiresAt,
|
|
1160
|
+
);
|
|
1161
|
+
if (matches.length !== 1) {
|
|
1162
|
+
throw new WorkflowError(
|
|
1163
|
+
'TRANSITION_BLOCKED',
|
|
1164
|
+
'Milestone-scoped context-refresh authority requires one exact alpha7 autonomy contract.',
|
|
1165
|
+
{ grantId: grant.id, contractCount: matches.length },
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
return matches[0]!;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function requireC1Evidence(
|
|
1172
|
+
store: FileStateStore,
|
|
1173
|
+
task: TaskState,
|
|
1174
|
+
actor: string,
|
|
1175
|
+
writerToken: string | null,
|
|
1176
|
+
): { posture: string; claimEventHash: string | null; owner: string | null; tokenHash: string | null } {
|
|
1177
|
+
const posture = readTaskC1Posture(store, task);
|
|
1178
|
+
const lease = readWriterLease(store, task);
|
|
1179
|
+
if (posture.state === 'pending') {
|
|
1180
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending C1 handoff must be claimed before corrective recovery.');
|
|
1181
|
+
}
|
|
1182
|
+
if (posture.state === 'claimed') {
|
|
1183
|
+
const tokenHash = writerToken ? sha256Hex(writerToken) : null;
|
|
1184
|
+
if (
|
|
1185
|
+
!lease
|
|
1186
|
+
|| actor !== posture.claimant
|
|
1187
|
+
|| lease.owner !== posture.claimant
|
|
1188
|
+
|| !writerToken
|
|
1189
|
+
|| lease.token !== writerToken
|
|
1190
|
+
|| tokenHash !== posture.writerLeaseTokenHash
|
|
1191
|
+
) {
|
|
1192
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery requires the claimed C1 actor and exact bound writer lease token.', {
|
|
1193
|
+
taskId: task.id,
|
|
1194
|
+
claimant: posture.claimant,
|
|
1195
|
+
leaseOwner: lease?.owner ?? null,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
return { posture: 'claimed', claimEventHash: posture.latestEvent.eventHash, owner: lease.owner, tokenHash };
|
|
1199
|
+
}
|
|
1200
|
+
if (lease && (lease.owner !== actor || !writerToken || lease.token !== writerToken)) {
|
|
1201
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Existing writer lease is not owned by the recovering actor/token.');
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
posture: 'none',
|
|
1205
|
+
claimEventHash: posture.latestEvent?.eventHash ?? null,
|
|
1206
|
+
owner: lease?.owner ?? null,
|
|
1207
|
+
tokenHash: lease ? sha256Hex(lease.token) : null,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function readWriterLease(store: FileStateStore, task: TaskState): WriterLease | null {
|
|
1212
|
+
const file = path.join(store.lockRoot(task.projectId), `${task.id}.json`);
|
|
1213
|
+
if (!existsSync(file)) return null;
|
|
1214
|
+
let lease: WriterLease;
|
|
1215
|
+
try {
|
|
1216
|
+
lease = JSON.parse(readFileSync(file, 'utf8')) as WriterLease;
|
|
1217
|
+
} catch {
|
|
1218
|
+
throw new WorkflowError('STATE_CORRUPT', `Writer lease is unreadable: ${file}`);
|
|
1219
|
+
}
|
|
1220
|
+
if (lease.entityId !== task.id || typeof lease.owner !== 'string' || typeof lease.token !== 'string') {
|
|
1221
|
+
throw new WorkflowError('STATE_CORRUPT', `Writer lease does not match Task ${task.id}.`);
|
|
1222
|
+
}
|
|
1223
|
+
return lease;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function assertRefreshChronology(
|
|
1227
|
+
map: ProjectKnowledgeMap,
|
|
1228
|
+
authorization: AuthorizationEvent,
|
|
1229
|
+
rebind: TaskKnowledgeRebindRecord,
|
|
1230
|
+
reboundAudit: PlanRiskAuditEvent,
|
|
1231
|
+
): void {
|
|
1232
|
+
const values = [
|
|
1233
|
+
Date.parse(map.approval!.approvedAt),
|
|
1234
|
+
Date.parse(rebind.recordedAt),
|
|
1235
|
+
Date.parse(reboundAudit.recordedAt),
|
|
1236
|
+
Date.parse(authorization.recordedAt),
|
|
1237
|
+
];
|
|
1238
|
+
if (values.some((value) => !Number.isFinite(value)) || values.some((value, index) => index > 0 && value < values[index - 1]!)) {
|
|
1239
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context-refresh evidence chronology is invalid.');
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function assertRecoveryMatchesAssessment(
|
|
1244
|
+
event: CorrectiveDecisionRecoveryEvent,
|
|
1245
|
+
assessment: CorrectiveRecoveryAssessment,
|
|
1246
|
+
actor: string,
|
|
1247
|
+
): void {
|
|
1248
|
+
const expected = recoveryPayload(assessment, actor, new Date(event.recordedAt));
|
|
1249
|
+
const ignored = new Set(['eventId', 'recordedAt']);
|
|
1250
|
+
const mismatches = Object.entries(expected)
|
|
1251
|
+
.filter(([key, value]) => !ignored.has(key) && event[key as keyof CorrectiveDecisionRecoveryEvent] !== value)
|
|
1252
|
+
.map(([key]) => key);
|
|
1253
|
+
if (mismatches.length > 0) {
|
|
1254
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery event is stale or conflicts with current evidence.', {
|
|
1255
|
+
taskId: event.taskId,
|
|
1256
|
+
stepId: event.stepId,
|
|
1257
|
+
eventId: event.eventId,
|
|
1258
|
+
mismatches,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function validateRecoveryEvent(
|
|
1264
|
+
event: CorrectiveDecisionRecoveryEvent,
|
|
1265
|
+
task: TaskState,
|
|
1266
|
+
file: string,
|
|
1267
|
+
line: number,
|
|
1268
|
+
): void {
|
|
1269
|
+
if (!/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId) || event.taskId !== task.id) {
|
|
1270
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery event identity is invalid.', { file, line });
|
|
1271
|
+
}
|
|
1272
|
+
if (typeof event.stepId !== 'string' || !event.stepId || event.triggeringAttemptCount !== 3) {
|
|
1273
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery Step/ordinal binding is invalid.', { file, line });
|
|
1274
|
+
}
|
|
1275
|
+
const hashes = [
|
|
1276
|
+
event.correctiveDecisionEventHash,
|
|
1277
|
+
event.decisionPlanHash,
|
|
1278
|
+
event.reboundPlanHash,
|
|
1279
|
+
event.semanticPlanHash,
|
|
1280
|
+
event.fromKnowledgeMapHash,
|
|
1281
|
+
event.toKnowledgeMapHash,
|
|
1282
|
+
event.sourcePlanRiskAuditEventHash,
|
|
1283
|
+
event.reboundPlanRiskAuditEventHash,
|
|
1284
|
+
event.knowledgeMapApprovalFingerprint,
|
|
1285
|
+
event.taskAuthorizationFingerprint,
|
|
1286
|
+
event.delegationGrantFingerprint,
|
|
1287
|
+
event.c1EvidenceFingerprint,
|
|
1288
|
+
event.evidenceFingerprint,
|
|
1289
|
+
];
|
|
1290
|
+
for (const hash of hashes) assertHash64(hash, `Corrective recovery hash at ${file}:${line}`);
|
|
1291
|
+
if (
|
|
1292
|
+
!Number.isInteger(event.fromKnowledgeMapRevision)
|
|
1293
|
+
|| event.fromKnowledgeMapRevision < 1
|
|
1294
|
+
|| !Number.isInteger(event.toKnowledgeMapRevision)
|
|
1295
|
+
|| event.toKnowledgeMapRevision < 1
|
|
1296
|
+
|| typeof event.actor !== 'string'
|
|
1297
|
+
|| !event.actor.trim()
|
|
1298
|
+
) {
|
|
1299
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery payload is malformed.', { file, line });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
export function readValidatedCorrectiveDecisionRecoveryForNavigation(
|
|
1304
|
+
store: FileStateStore,
|
|
1305
|
+
task: TaskState,
|
|
1306
|
+
stepId: string,
|
|
1307
|
+
): { assessment: CorrectiveRecoveryAssessment; recovery: CorrectiveDecisionRecoveryEvent | null } {
|
|
1308
|
+
const posture = readTaskC1Posture(store, task);
|
|
1309
|
+
const lease = readWriterLease(store, task);
|
|
1310
|
+
const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
|
|
1311
|
+
const assessment = assessCorrectiveDecisionRecovery(store, task, stepId, actor, lease?.token ?? null);
|
|
1312
|
+
const matches = readCorrectiveDecisionRecoveryEvents(store, task).filter((event) =>
|
|
1313
|
+
event.correctiveDecisionEventId === assessment.decision.eventId
|
|
1314
|
+
&& event.stepId === stepId
|
|
1315
|
+
&& event.triggeringAttemptCount === assessment.decision.triggeringAttemptCount,
|
|
1316
|
+
);
|
|
1317
|
+
if (matches.length > 1) {
|
|
1318
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple recoveries exist for one corrective decision binding.');
|
|
1319
|
+
}
|
|
1320
|
+
const recovery = matches[0] ?? null;
|
|
1321
|
+
if (recovery) assertRecoveryMatchesAssessment(recovery, assessment, actor);
|
|
1322
|
+
return { assessment, recovery };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
export function assertKnowledgeBindingOnlyPlanChange(historical: string, current: string): string {
|
|
1326
|
+
const decision = normalizePlanKnowledgeBinding(historical, 'historical');
|
|
1327
|
+
const rebound = normalizePlanKnowledgeBinding(current, 'current');
|
|
1328
|
+
if (decision.normalized !== rebound.normalized) {
|
|
1329
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery permits only the Knowledge Map binding line to change.');
|
|
1330
|
+
}
|
|
1331
|
+
return sha256Hex(decision.normalized);
|
|
1332
|
+
}
|