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,520 @@
|
|
|
1
|
+
import type { DownstreamProofInvalidationEvent, DownstreamProofReplanRecoveryEvent, TaskState } from '../contracts.js';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { WorkflowError } from '../errors.js';
|
|
6
|
+
import { pathAllowed } from '../domain/validation.js';
|
|
7
|
+
import {
|
|
8
|
+
changedFiles,
|
|
9
|
+
currentBranch,
|
|
10
|
+
headCommit,
|
|
11
|
+
} from '../repository.js';
|
|
12
|
+
import { validateTaskHistory } from '../git.js';
|
|
13
|
+
import type { FileStateStore } from '../state/store.js';
|
|
14
|
+
import { createUlid } from '../ulid.js';
|
|
15
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
16
|
+
import { prepareSidecarAppend } from './store-sidecars.js';
|
|
17
|
+
import { effectiveStepAllowedWrites } from './remediation.js';
|
|
18
|
+
|
|
19
|
+
const SIDECAR = 'downstream-proof-invalidations.jsonl';
|
|
20
|
+
const REPLAN_RECOVERY_SIDECAR = 'downstream-proof-replan-recoveries.jsonl';
|
|
21
|
+
const HASH_64 = /^[a-f0-9]{64}$/;
|
|
22
|
+
const GIT_HASH = /^[a-f0-9]{40}$/;
|
|
23
|
+
const EVENT_ID = /^DPI-[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
24
|
+
const REPLAN_EVENT_ID = /^DPR-[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
25
|
+
|
|
26
|
+
export interface DownstreamProofInvalidatedStep {
|
|
27
|
+
stepId: string;
|
|
28
|
+
evidenceCommit: string;
|
|
29
|
+
triggerFiles: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DownstreamProofRecoveryAssessment {
|
|
33
|
+
applicable: boolean;
|
|
34
|
+
eligible: boolean;
|
|
35
|
+
activeStepId: string | null;
|
|
36
|
+
dirtyFiles: string[];
|
|
37
|
+
outsideActiveStepFiles: string[];
|
|
38
|
+
invalidatedSteps: DownstreamProofInvalidatedStep[];
|
|
39
|
+
preservedHeadCommit: string | null;
|
|
40
|
+
dirtyWorktreeHash: string | null;
|
|
41
|
+
blockers: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PreparedDownstreamProofInvalidation {
|
|
45
|
+
event: DownstreamProofInvalidationEvent;
|
|
46
|
+
postimage: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DownstreamProofCarryoverAssessment {
|
|
50
|
+
applicable: boolean;
|
|
51
|
+
eligible: boolean;
|
|
52
|
+
dirtyFiles: string[];
|
|
53
|
+
dirtyWorktreeHash: string | null;
|
|
54
|
+
invalidationEventId: string | null;
|
|
55
|
+
blockers: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assessDownstreamProofRecovery(
|
|
59
|
+
store: FileStateStore,
|
|
60
|
+
repositoryRoot: string,
|
|
61
|
+
task: TaskState,
|
|
62
|
+
options: {
|
|
63
|
+
historyBoundary?: string;
|
|
64
|
+
preservedHeadCommit?: string;
|
|
65
|
+
} = {},
|
|
66
|
+
): DownstreamProofRecoveryAssessment {
|
|
67
|
+
readDownstreamProofInvalidations(store, task);
|
|
68
|
+
const activeStep = task.steps.find((step) => step.status === 'in_progress') ?? null;
|
|
69
|
+
if (task.status !== 'in_progress' || !activeStep) return emptyAssessment();
|
|
70
|
+
|
|
71
|
+
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
72
|
+
let dirtyWorktreeHash: string | null = null;
|
|
73
|
+
try {
|
|
74
|
+
dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
...emptyAssessment(),
|
|
78
|
+
applicable: true,
|
|
79
|
+
activeStepId: activeStep.id,
|
|
80
|
+
dirtyFiles,
|
|
81
|
+
blockers: [error instanceof Error ? error.message : String(error)],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const activeAllowedWrites = effectiveStepAllowedWrites(store, task, activeStep.id);
|
|
85
|
+
const outsideActiveStepFiles = dirtyFiles.filter((file) => !pathAllowed(file, activeAllowedWrites));
|
|
86
|
+
if (outsideActiveStepFiles.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
...emptyAssessment(),
|
|
89
|
+
activeStepId: activeStep.id,
|
|
90
|
+
dirtyFiles,
|
|
91
|
+
dirtyWorktreeHash,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const blockers: string[] = [];
|
|
96
|
+
if (task.workspaceOwner !== 'local') {
|
|
97
|
+
blockers.push('Recovery requires a local Task workspace because the dirty file set is repository-local evidence.');
|
|
98
|
+
}
|
|
99
|
+
if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
|
|
100
|
+
blockers.push(`Current branch must equal the recorded Task branch ${task.taskBranch ?? '<missing>'}.`);
|
|
101
|
+
}
|
|
102
|
+
if (!task.planHash) blockers.push('Task has no current Plan hash.');
|
|
103
|
+
if (activeStep.evidence !== null) {
|
|
104
|
+
blockers.push(`Active Step ${activeStep.id} already has completion evidence and must use strict-review recovery.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const predecessorIds = transitivePredecessors(task, activeStep.id, blockers);
|
|
108
|
+
const seedIds = new Set<string>();
|
|
109
|
+
for (const file of outsideActiveStepFiles) {
|
|
110
|
+
const owners = task.steps.filter((step) =>
|
|
111
|
+
step.status === 'completed'
|
|
112
|
+
&& predecessorIds.has(step.id)
|
|
113
|
+
&& pathAllowed(file, step.allowedWrites),
|
|
114
|
+
);
|
|
115
|
+
if (owners.length === 0) {
|
|
116
|
+
blockers.push(`${file} is outside ${activeStep.id} and is not owned by a completed transitive predecessor.`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const owner of owners) seedIds.add(owner.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const invalidatedIds = completedDependentClosure(task, seedIds, predecessorIds);
|
|
123
|
+
const invalidatedSteps = task.steps
|
|
124
|
+
.filter((step) => invalidatedIds.has(step.id))
|
|
125
|
+
.map((step) => {
|
|
126
|
+
if (!step.evidence) {
|
|
127
|
+
blockers.push(`Completed predecessor ${step.id} has no completion evidence.`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
stepId: step.id,
|
|
131
|
+
evidenceCommit: step.evidence?.commitSha ?? '',
|
|
132
|
+
triggerFiles: outsideActiveStepFiles.filter((file) => pathAllowed(file, step.allowedWrites)),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
if (invalidatedSteps.length === 0) blockers.push('No completed predecessor completion authority can be invalidated.');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
validateTaskHistory(repositoryRoot, task, options.historyBoundary);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
applicable: true,
|
|
145
|
+
eligible: blockers.length === 0,
|
|
146
|
+
activeStepId: activeStep.id,
|
|
147
|
+
dirtyFiles,
|
|
148
|
+
outsideActiveStepFiles,
|
|
149
|
+
invalidatedSteps,
|
|
150
|
+
preservedHeadCommit: options.preservedHeadCommit ?? headCommit(repositoryRoot),
|
|
151
|
+
dirtyWorktreeHash,
|
|
152
|
+
blockers,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function prepareDownstreamProofInvalidation(
|
|
157
|
+
store: FileStateStore,
|
|
158
|
+
resultTask: TaskState,
|
|
159
|
+
assessment: DownstreamProofRecoveryAssessment,
|
|
160
|
+
input: {
|
|
161
|
+
previousTaskRevision: number;
|
|
162
|
+
actor: string;
|
|
163
|
+
reason: string;
|
|
164
|
+
},
|
|
165
|
+
now: Date,
|
|
166
|
+
): PreparedDownstreamProofInvalidation {
|
|
167
|
+
if (!assessment.applicable || !assessment.eligible || !assessment.activeStepId || !assessment.preservedHeadCommit) {
|
|
168
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream proof recovery assessment is not eligible.', {
|
|
169
|
+
blockers: assessment.blockers,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const actor = normalizeRequired(input.actor, 'Recovery actor');
|
|
173
|
+
const reason = normalizeRequired(input.reason, 'Recovery reason');
|
|
174
|
+
if (!resultTask.planHash) throw new WorkflowError('STATE_CORRUPT', 'Recovered Task has no Plan hash.');
|
|
175
|
+
if (resultTask.revision !== input.previousTaskRevision + 1) {
|
|
176
|
+
throw new WorkflowError('STATE_CONFLICT', 'Recovered Task revision does not advance exactly once.');
|
|
177
|
+
}
|
|
178
|
+
const events = readDownstreamProofInvalidations(store, resultTask);
|
|
179
|
+
const prepared = prepareSidecarAppend<DownstreamProofInvalidationEvent>(events, {
|
|
180
|
+
eventId: `DPI-${createUlid(now.getTime())}`,
|
|
181
|
+
recordedAt: now.toISOString(),
|
|
182
|
+
taskId: resultTask.id,
|
|
183
|
+
previousTaskRevision: input.previousTaskRevision,
|
|
184
|
+
resultTaskRevision: resultTask.revision,
|
|
185
|
+
planHash: resultTask.planHash,
|
|
186
|
+
activeStepId: assessment.activeStepId,
|
|
187
|
+
invalidatedSteps: assessment.invalidatedSteps.map((step) => ({
|
|
188
|
+
stepId: step.stepId,
|
|
189
|
+
evidenceCommit: step.evidenceCommit,
|
|
190
|
+
triggerFiles: [...step.triggerFiles],
|
|
191
|
+
})),
|
|
192
|
+
dirtyFiles: [...assessment.dirtyFiles],
|
|
193
|
+
dirtyWorktreeHash: assessment.dirtyWorktreeHash!,
|
|
194
|
+
preservedHeadCommit: assessment.preservedHeadCommit,
|
|
195
|
+
actor,
|
|
196
|
+
reason,
|
|
197
|
+
packageVersion: PACKAGE_VERSION,
|
|
198
|
+
}, {
|
|
199
|
+
validateEvent: (event) => validateInvalidationEvent(event, resultTask),
|
|
200
|
+
});
|
|
201
|
+
return { event: prepared.event, postimage: prepared.content };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function readDownstreamProofInvalidations(
|
|
205
|
+
store: FileStateStore,
|
|
206
|
+
task: TaskState,
|
|
207
|
+
): DownstreamProofInvalidationEvent[] {
|
|
208
|
+
return store.readSidecarEvents<DownstreamProofInvalidationEvent>(
|
|
209
|
+
store.taskRoot(task.projectId, task.id),
|
|
210
|
+
SIDECAR,
|
|
211
|
+
{ validateEvent: (event) => validateInvalidationEvent(event, task) },
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function downstreamProofInvalidationSidecar(): string {
|
|
216
|
+
return SIDECAR;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function readDownstreamProofReplanRecoveries(
|
|
220
|
+
store: FileStateStore,
|
|
221
|
+
task: TaskState,
|
|
222
|
+
): DownstreamProofReplanRecoveryEvent[] {
|
|
223
|
+
return store.readSidecarEvents<DownstreamProofReplanRecoveryEvent>(
|
|
224
|
+
store.taskRoot(task.projectId, task.id),
|
|
225
|
+
REPLAN_RECOVERY_SIDECAR,
|
|
226
|
+
{ validateEvent: (event) => validateReplanRecoveryEvent(event, task) },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function assessDownstreamProofCarryover(
|
|
231
|
+
store: FileStateStore,
|
|
232
|
+
repositoryRoot: string,
|
|
233
|
+
task: TaskState,
|
|
234
|
+
stepId: string,
|
|
235
|
+
): DownstreamProofCarryoverAssessment {
|
|
236
|
+
const invalidation = readDownstreamProofInvalidations(store, task).at(-1) ?? null;
|
|
237
|
+
if (!invalidation || !task.planHash || task.planHash === invalidation.planHash) {
|
|
238
|
+
return {
|
|
239
|
+
applicable: false,
|
|
240
|
+
eligible: false,
|
|
241
|
+
dirtyFiles: [],
|
|
242
|
+
dirtyWorktreeHash: null,
|
|
243
|
+
invalidationEventId: null,
|
|
244
|
+
blockers: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const recovery = [...readDownstreamProofReplanRecoveries(store, task)]
|
|
248
|
+
.reverse()
|
|
249
|
+
.find((event) => event.invalidationEventId === invalidation.eventId) ?? null;
|
|
250
|
+
const sourceBinding = [...(task.dependencyProvenanceRecoveries ?? [])]
|
|
251
|
+
.reverse()
|
|
252
|
+
.map((record) => record.activeDownstreamProof)
|
|
253
|
+
.find((binding) => binding?.planHash === invalidation.planHash
|
|
254
|
+
&& JSON.stringify([...binding.dirtyFiles].sort()) === JSON.stringify(invalidation.dirtyFiles)) ?? null;
|
|
255
|
+
const expectedDirtyHash = recovery?.dirtyWorktreeHash
|
|
256
|
+
?? invalidation.dirtyWorktreeHash
|
|
257
|
+
?? sourceBinding?.dirtyWorktreeHash
|
|
258
|
+
?? null;
|
|
259
|
+
const expectedHead = recovery?.preservedHeadCommit ?? invalidation.preservedHeadCommit;
|
|
260
|
+
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
261
|
+
let dirtyWorktreeHash: string | null = null;
|
|
262
|
+
const blockers: string[] = [];
|
|
263
|
+
try {
|
|
264
|
+
dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
267
|
+
}
|
|
268
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
269
|
+
if (!step || step.status !== 'planned' || step.evidence !== null) {
|
|
270
|
+
blockers.push('The replacement carryover Step must be planned and have no completion evidence.');
|
|
271
|
+
}
|
|
272
|
+
if (task.steps.some((candidate) => candidate.status === 'in_progress' || candidate.evidence !== null)) {
|
|
273
|
+
blockers.push('No replacement Plan Step may already be running or completed.');
|
|
274
|
+
}
|
|
275
|
+
if (JSON.stringify(dirtyFiles) !== JSON.stringify(invalidation.dirtyFiles)) {
|
|
276
|
+
blockers.push('Dirty files changed after downstream-proof recovery.');
|
|
277
|
+
}
|
|
278
|
+
if (!expectedDirtyHash) {
|
|
279
|
+
blockers.push('No hash-bound downstream-proof dirty-worktree evidence is available.');
|
|
280
|
+
} else if (dirtyWorktreeHash !== expectedDirtyHash) {
|
|
281
|
+
blockers.push('Dirty file content changed after downstream-proof recovery.');
|
|
282
|
+
}
|
|
283
|
+
if (step && dirtyFiles.some((file) => !pathAllowed(file, effectiveStepAllowedWrites(store, task, step.id)))) {
|
|
284
|
+
blockers.push('The replacement Step does not own the complete preserved dirty set.');
|
|
285
|
+
}
|
|
286
|
+
if (headCommit(repositoryRoot) !== expectedHead) {
|
|
287
|
+
blockers.push('Git HEAD changed after downstream-proof recovery.');
|
|
288
|
+
}
|
|
289
|
+
if (task.workspaceOwner !== 'local' || !task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
|
|
290
|
+
blockers.push('Carryover execution requires the unchanged recorded local Task branch.');
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
validateTaskHistory(repositoryRoot, task);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
applicable: true,
|
|
299
|
+
eligible: blockers.length === 0,
|
|
300
|
+
dirtyFiles,
|
|
301
|
+
dirtyWorktreeHash,
|
|
302
|
+
invalidationEventId: invalidation.eventId,
|
|
303
|
+
blockers,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function emptyAssessment(): DownstreamProofRecoveryAssessment {
|
|
308
|
+
return {
|
|
309
|
+
applicable: false,
|
|
310
|
+
eligible: false,
|
|
311
|
+
activeStepId: null,
|
|
312
|
+
dirtyFiles: [],
|
|
313
|
+
outsideActiveStepFiles: [],
|
|
314
|
+
invalidatedSteps: [],
|
|
315
|
+
preservedHeadCommit: null,
|
|
316
|
+
dirtyWorktreeHash: null,
|
|
317
|
+
blockers: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function hashDirtyWorktree(repositoryRoot: string, dirtyFiles: readonly string[]): string {
|
|
322
|
+
const hash = createHash('sha256');
|
|
323
|
+
for (const file of [...dirtyFiles].sort()) {
|
|
324
|
+
const absolute = path.resolve(repositoryRoot, file);
|
|
325
|
+
const relative = path.relative(repositoryRoot, absolute);
|
|
326
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
327
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Dirty path escapes the repository boundary: ${file}`);
|
|
328
|
+
}
|
|
329
|
+
hash.update(`path\0${file}\0`);
|
|
330
|
+
if (!existsSync(absolute)) {
|
|
331
|
+
hash.update('deleted\0');
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const stat = lstatSync(absolute);
|
|
335
|
+
if (stat.isSymbolicLink()) {
|
|
336
|
+
hash.update(`symlink\0${readlinkSync(absolute)}\0`);
|
|
337
|
+
} else if (stat.isFile()) {
|
|
338
|
+
hash.update('file\0');
|
|
339
|
+
hash.update(readFileSync(absolute));
|
|
340
|
+
hash.update('\0');
|
|
341
|
+
} else {
|
|
342
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Dirty path is not a regular file or symlink: ${file}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return hash.digest('hex');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function transitivePredecessors(task: TaskState, stepId: string, blockers: string[]): Set<string> {
|
|
349
|
+
const byId = new Map(task.steps.map((step) => [step.id, step]));
|
|
350
|
+
const result = new Set<string>();
|
|
351
|
+
const visiting = new Set<string>();
|
|
352
|
+
const visit = (candidateId: string): void => {
|
|
353
|
+
if (result.has(candidateId)) return;
|
|
354
|
+
if (visiting.has(candidateId)) {
|
|
355
|
+
blockers.push(`Step dependency cycle reaches ${candidateId}.`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const candidate = byId.get(candidateId);
|
|
359
|
+
if (!candidate) {
|
|
360
|
+
blockers.push(`Step dependency references missing Step ${candidateId}.`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
visiting.add(candidateId);
|
|
364
|
+
for (const dependency of candidate.dependencies) {
|
|
365
|
+
visit(dependency);
|
|
366
|
+
result.add(dependency);
|
|
367
|
+
}
|
|
368
|
+
visiting.delete(candidateId);
|
|
369
|
+
};
|
|
370
|
+
visit(stepId);
|
|
371
|
+
result.delete(stepId);
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function completedDependentClosure(
|
|
376
|
+
task: TaskState,
|
|
377
|
+
seedIds: ReadonlySet<string>,
|
|
378
|
+
predecessorIds: ReadonlySet<string>,
|
|
379
|
+
): Set<string> {
|
|
380
|
+
const result = new Set(seedIds);
|
|
381
|
+
let changed = true;
|
|
382
|
+
while (changed) {
|
|
383
|
+
changed = false;
|
|
384
|
+
for (const step of task.steps) {
|
|
385
|
+
if (step.status !== 'completed' || !predecessorIds.has(step.id) || result.has(step.id)) continue;
|
|
386
|
+
if (step.dependencies.some((dependency) => result.has(dependency))) {
|
|
387
|
+
result.add(step.id);
|
|
388
|
+
changed = true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function validateInvalidationEvent(event: DownstreamProofInvalidationEvent, task: TaskState): void {
|
|
396
|
+
const requiredKeys = [
|
|
397
|
+
'activeStepId', 'actor', 'dirtyFiles', 'eventHash', 'eventId', 'invalidatedSteps', 'packageVersion',
|
|
398
|
+
'planHash', 'preservedHeadCommit', 'previousEventHash', 'previousTaskRevision', 'reason', 'recordedAt',
|
|
399
|
+
'resultTaskRevision', 'taskId',
|
|
400
|
+
];
|
|
401
|
+
const actualKeys = Object.keys(event).sort();
|
|
402
|
+
const expectedLegacyKeys = [...requiredKeys].sort();
|
|
403
|
+
const expectedCurrentKeys = [...requiredKeys, 'dirtyWorktreeHash'].sort();
|
|
404
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)
|
|
405
|
+
|| (JSON.stringify(actualKeys) !== JSON.stringify(expectedLegacyKeys)
|
|
406
|
+
&& JSON.stringify(actualKeys) !== JSON.stringify(expectedCurrentKeys))) {
|
|
407
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation event contains unexpected fields.');
|
|
408
|
+
}
|
|
409
|
+
if (typeof event.eventId !== 'string' || !EVENT_ID.test(event.eventId)) {
|
|
410
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation eventId is invalid.');
|
|
411
|
+
}
|
|
412
|
+
if (event.taskId !== task.id) throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation Task identity mismatch.');
|
|
413
|
+
if (!Number.isInteger(event.previousTaskRevision) || event.previousTaskRevision < 1
|
|
414
|
+
|| event.resultTaskRevision !== event.previousTaskRevision + 1
|
|
415
|
+
|| event.resultTaskRevision > task.revision) {
|
|
416
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation revision binding is invalid.');
|
|
417
|
+
}
|
|
418
|
+
if (typeof event.planHash !== 'string' || !HASH_64.test(event.planHash)) {
|
|
419
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation planHash is invalid.');
|
|
420
|
+
}
|
|
421
|
+
if (typeof event.preservedHeadCommit !== 'string' || !GIT_HASH.test(event.preservedHeadCommit)) {
|
|
422
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation preservedHeadCommit is invalid.');
|
|
423
|
+
}
|
|
424
|
+
for (const [value, label] of [
|
|
425
|
+
[event.activeStepId, 'activeStepId'],
|
|
426
|
+
[event.actor, 'actor'],
|
|
427
|
+
[event.reason, 'reason'],
|
|
428
|
+
[event.packageVersion, 'packageVersion'],
|
|
429
|
+
] as const) {
|
|
430
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
431
|
+
throw new WorkflowError('STATE_CORRUPT', `Downstream proof invalidation ${label} is required.`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!Array.isArray(event.dirtyFiles) || event.dirtyFiles.length === 0
|
|
435
|
+
|| event.dirtyFiles.some((file) => typeof file !== 'string' || !file.trim())
|
|
436
|
+
|| new Set(event.dirtyFiles).size !== event.dirtyFiles.length
|
|
437
|
+
|| JSON.stringify(event.dirtyFiles) !== JSON.stringify([...event.dirtyFiles].sort())) {
|
|
438
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation dirtyFiles are invalid.');
|
|
439
|
+
}
|
|
440
|
+
if (event.dirtyWorktreeHash !== undefined
|
|
441
|
+
&& (typeof event.dirtyWorktreeHash !== 'string' || !HASH_64.test(event.dirtyWorktreeHash))) {
|
|
442
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation dirtyWorktreeHash is invalid.');
|
|
443
|
+
}
|
|
444
|
+
if (!Array.isArray(event.invalidatedSteps) || event.invalidatedSteps.length === 0) {
|
|
445
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidation must identify invalidated Steps.');
|
|
446
|
+
}
|
|
447
|
+
const stepIds = new Set<string>();
|
|
448
|
+
for (const step of event.invalidatedSteps) {
|
|
449
|
+
if (!step || typeof step !== 'object' || Array.isArray(step)
|
|
450
|
+
|| JSON.stringify(Object.keys(step).sort()) !== JSON.stringify(['evidenceCommit', 'stepId', 'triggerFiles'])) {
|
|
451
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidated Step contains unexpected fields.');
|
|
452
|
+
}
|
|
453
|
+
if (typeof step.stepId !== 'string' || !step.stepId.trim() || stepIds.has(step.stepId)
|
|
454
|
+
|| typeof step.evidenceCommit !== 'string' || !GIT_HASH.test(step.evidenceCommit)
|
|
455
|
+
|| !(task.invalidatedStepCommits ?? []).includes(step.evidenceCommit)
|
|
456
|
+
|| !Array.isArray(step.triggerFiles)
|
|
457
|
+
|| new Set(step.triggerFiles).size !== step.triggerFiles.length
|
|
458
|
+
|| step.triggerFiles.some((file) => typeof file !== 'string' || !event.dirtyFiles.includes(file))) {
|
|
459
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof invalidated Step evidence is invalid.');
|
|
460
|
+
}
|
|
461
|
+
stepIds.add(step.stepId);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function validateReplanRecoveryEvent(event: DownstreamProofReplanRecoveryEvent, task: TaskState): void {
|
|
466
|
+
const expectedKeys = [
|
|
467
|
+
'actor', 'dependencyRecoveryCommit', 'dirtyFiles', 'dirtyWorktreeHash', 'eventHash', 'eventId',
|
|
468
|
+
'invalidationEventId', 'invalidationPlanHash', 'knowledgeRebindRecordedAt', 'packageVersion',
|
|
469
|
+
'preservedHeadCommit', 'previousEventHash', 'previousTaskRevision', 'reason', 'reboundPlanHash',
|
|
470
|
+
'recordedAt', 'resultTaskRevision', 'taskId',
|
|
471
|
+
];
|
|
472
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)
|
|
473
|
+
|| JSON.stringify(Object.keys(event).sort()) !== JSON.stringify(expectedKeys.sort())) {
|
|
474
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery event contains unexpected fields.');
|
|
475
|
+
}
|
|
476
|
+
if (typeof event.eventId !== 'string' || !REPLAN_EVENT_ID.test(event.eventId)) {
|
|
477
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery eventId is invalid.');
|
|
478
|
+
}
|
|
479
|
+
if (event.taskId !== task.id
|
|
480
|
+
|| !Number.isInteger(event.previousTaskRevision)
|
|
481
|
+
|| event.previousTaskRevision < 1
|
|
482
|
+
|| event.resultTaskRevision !== event.previousTaskRevision + 1
|
|
483
|
+
|| event.resultTaskRevision > task.revision) {
|
|
484
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery Task/revision binding is invalid.');
|
|
485
|
+
}
|
|
486
|
+
if (typeof event.invalidationEventId !== 'string' || !EVENT_ID.test(event.invalidationEventId)
|
|
487
|
+
|| typeof event.invalidationPlanHash !== 'string' || !HASH_64.test(event.invalidationPlanHash)
|
|
488
|
+
|| typeof event.reboundPlanHash !== 'string' || !HASH_64.test(event.reboundPlanHash)
|
|
489
|
+
|| typeof event.dirtyWorktreeHash !== 'string' || !HASH_64.test(event.dirtyWorktreeHash)
|
|
490
|
+
|| typeof event.preservedHeadCommit !== 'string' || !GIT_HASH.test(event.preservedHeadCommit)
|
|
491
|
+
|| (event.dependencyRecoveryCommit !== null
|
|
492
|
+
&& (typeof event.dependencyRecoveryCommit !== 'string' || !GIT_HASH.test(event.dependencyRecoveryCommit)))) {
|
|
493
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery hash binding is invalid.');
|
|
494
|
+
}
|
|
495
|
+
if (!Number.isFinite(Date.parse(event.knowledgeRebindRecordedAt))) {
|
|
496
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery knowledge rebind timestamp is invalid.');
|
|
497
|
+
}
|
|
498
|
+
if (!Array.isArray(event.dirtyFiles) || event.dirtyFiles.length === 0
|
|
499
|
+
|| event.dirtyFiles.some((file) => typeof file !== 'string' || !file.trim())
|
|
500
|
+
|| new Set(event.dirtyFiles).size !== event.dirtyFiles.length
|
|
501
|
+
|| JSON.stringify(event.dirtyFiles) !== JSON.stringify([...event.dirtyFiles].sort())) {
|
|
502
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery dirtyFiles are invalid.');
|
|
503
|
+
}
|
|
504
|
+
for (const [value, label] of [
|
|
505
|
+
[event.actor, 'actor'],
|
|
506
|
+
[event.reason, 'reason'],
|
|
507
|
+
[event.packageVersion, 'packageVersion'],
|
|
508
|
+
] as const) {
|
|
509
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
510
|
+
throw new WorkflowError('STATE_CORRUPT', `Downstream-proof replan recovery ${label} is required.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function normalizeRequired(value: string, label: string): string {
|
|
516
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
517
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} is required.`);
|
|
518
|
+
}
|
|
519
|
+
return value.trim();
|
|
520
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { TaskState } from '../contracts.js';
|
|
4
|
+
import type { FileStateStore } from '../state/store.js';
|
|
5
|
+
import { WorkflowError } from '../errors.js';
|
|
6
|
+
import { hashKnowledgeMap } from '../memory.js';
|
|
7
|
+
import { canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
|
|
8
|
+
import { readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents } from './plan-risk.js';
|
|
9
|
+
import { assertKnowledgeBindingOnlyPlanChange } from '../alpha7/corrective-recovery.js';
|
|
10
|
+
|
|
11
|
+
/** A stopped rebound Plan is evidence for repair, never execution authority. */
|
|
12
|
+
export function readFailedStepPlanningRecovery(store: FileStateStore, task: TaskState) {
|
|
13
|
+
if (task.status !== 'awaiting_execution_authorization' || !task.planHash
|
|
14
|
+
|| task.workspaceOwner === null || !task.steps.some(step => step.status === 'failed')
|
|
15
|
+
|| task.steps.some(step => step.status === 'in_progress')) return null;
|
|
16
|
+
const current = readCurrentPlanningObstructionAudit(store, task);
|
|
17
|
+
if (!current || current.audit.decision !== 'stop-escalate') return null;
|
|
18
|
+
const fail = (message: string): never => { throw new WorkflowError('TRANSITION_BLOCKED', message, { taskId: task.id }); };
|
|
19
|
+
const map = store.readKnowledgeMap(task.projectId);
|
|
20
|
+
if (map.status !== 'active' || map.revision !== task.knowledgeMapRevision || hashKnowledgeMap(map) !== task.knowledgeMapHash)
|
|
21
|
+
fail('Planning recovery requires the current active Knowledge binding.');
|
|
22
|
+
const audits = readPlanRiskAuditEvents(store, task);
|
|
23
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
24
|
+
let hash = task.planHash;
|
|
25
|
+
let knowledgeRevision = task.knowledgeMapRevision;
|
|
26
|
+
let knowledgeHash = task.knowledgeMapHash;
|
|
27
|
+
const visited = new Set<string>();
|
|
28
|
+
const plan = (h: string) => {
|
|
29
|
+
const text = readFileSync(h === task.planHash ? path.join(root, 'plan.md') : path.join(root, '.versions', `plan.md.${h}`), 'utf8');
|
|
30
|
+
if (sha256Hex(text) !== h) fail('Planning recovery Plan artifact hash mismatch.');
|
|
31
|
+
return text;
|
|
32
|
+
};
|
|
33
|
+
while (!visited.has(hash)) {
|
|
34
|
+
visited.add(hash);
|
|
35
|
+
const links = task.knowledgeRebinds.filter(record => record.reboundPlanHash === hash);
|
|
36
|
+
if (links.length !== 1) return fail('Planning recovery requires an unambiguous Knowledge rebind chain.');
|
|
37
|
+
const link = links[0]!;
|
|
38
|
+
if (link.toKnowledgeMapRevision !== knowledgeRevision || link.toKnowledgeMapHash !== knowledgeHash
|
|
39
|
+
|| !link.previousPlanHash) fail('Planning recovery Knowledge rebind endpoints are stale.');
|
|
40
|
+
const before = plan(link.previousPlanHash), after = plan(hash);
|
|
41
|
+
assertKnowledgeBindingOnlyPlanChange(before, after);
|
|
42
|
+
if (!before.includes(`- Knowledge map: revision ${link.fromKnowledgeMapRevision}, hash ${link.fromKnowledgeMapHash}\n`)
|
|
43
|
+
|| !after.includes(`- Knowledge map: revision ${link.toKnowledgeMapRevision}, hash ${link.toKnowledgeMapHash}\n`))
|
|
44
|
+
fail('Planning recovery artifact Knowledge endpoints differ from the recorded rebind.');
|
|
45
|
+
hash = link.previousPlanHash;
|
|
46
|
+
knowledgeRevision = link.fromKnowledgeMapRevision;
|
|
47
|
+
knowledgeHash = link.fromKnowledgeMapHash;
|
|
48
|
+
const baseline = audits.filter(a => a.planHash === hash && a.briefHash === task.briefHash
|
|
49
|
+
&& a.knowledgeMapRevision === knowledgeRevision && a.knowledgeMapHash === knowledgeHash
|
|
50
|
+
&& ['approved', 'approved-with-rationale'].includes(a.decision)
|
|
51
|
+
&& audits.indexOf(a) < audits.findIndex(event => event.eventHash === current.audit.eventHash)
|
|
52
|
+
&& task.authorizations.some(grant => grant.kind === 'execution' && grant.decision === 'superseded'
|
|
53
|
+
&& grant.planHash === a.planHash && grant.briefHash === a.briefHash
|
|
54
|
+
&& Date.parse(grant.recordedAt) >= Date.parse(a.recordedAt))).at(-1);
|
|
55
|
+
if (!baseline) continue;
|
|
56
|
+
// A negative audit may append an observed counterexample without erasing the
|
|
57
|
+
// historical proof. Prefix preservation is NOT proof of semantic strengthening:
|
|
58
|
+
// both audits remain planning evidence, and replacement requires its own gates.
|
|
59
|
+
const obstructionClassifications = structuredClone(current.audit.stepClassifications);
|
|
60
|
+
for (const [stepIndex, classification] of obstructionClassifications.entries()) {
|
|
61
|
+
const original = baseline.stepClassifications[stepIndex];
|
|
62
|
+
if (!original) return fail('Planning recovery must preserve the approved Step classification ordering.');
|
|
63
|
+
const proofs = classification.proofObligations ?? [], originalProofs = original.proofObligations ?? [];
|
|
64
|
+
if (proofs.length !== originalProofs.length)
|
|
65
|
+
fail('Planning recovery must preserve the approved proof tuple count and ordering.');
|
|
66
|
+
for (const [proofIndex, proof] of proofs.entries()) {
|
|
67
|
+
const originalProof = originalProofs[proofIndex]!;
|
|
68
|
+
for (const field of ['counterexample', 'expectedResult'] as const) {
|
|
69
|
+
if (!proof[field].startsWith(originalProof[field]))
|
|
70
|
+
fail('Planning recovery cannot truncate or replace historical proof narratives.');
|
|
71
|
+
proof[field] = originalProof[field];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (canonicalJsonStringify(baseline.stepClassifications) !== canonicalJsonStringify(obstructionClassifications))
|
|
76
|
+
fail('Planning recovery must preserve the approved risk classifications and proof tuples.');
|
|
77
|
+
const grants = task.authorizations.filter(a => a.kind === 'execution' && a.decision === 'superseded'
|
|
78
|
+
&& a.planHash === baseline.planHash && a.briefHash === baseline.briefHash
|
|
79
|
+
&& Date.parse(a.recordedAt) >= Date.parse(baseline.recordedAt));
|
|
80
|
+
if (grants.length !== 1) fail('Planning recovery requires one historical superseded Plan authorization.');
|
|
81
|
+
return { current, baseline, priorAuthorization: grants[0]!, authority: 'planning-only' as const };
|
|
82
|
+
}
|
|
83
|
+
return fail('Planning recovery Knowledge rebind chain contains a cycle.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readCorrectivePlanningAudit(store: FileStateStore, task: TaskState) {
|
|
87
|
+
return readFailedStepPlanningRecovery(store, task)?.current ?? readCurrentPlanRiskAudit(store, task);
|
|
88
|
+
}
|