codex-workflow-v2 2.0.0-beta.13.14 → 2.0.0-beta.13.16
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 +17 -0
- package/dist/reviewer-runtime-build.json +20 -8
- package/dist/src/alpha6/captured-check-evidence.d.ts +49 -0
- package/dist/src/alpha6/captured-check-evidence.js +146 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- 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/root-cause-replan-carryover.d.ts +3 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +4 -0
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/contracts.d.ts +5 -1
- package/dist/src/dependency-provenance.js +10 -0
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +1 -0
- package/dist/src/workflow.js +139 -19
- package/dist/src/workflow.js.map +1 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
- package/docs/release.md +49 -0
- package/package.json +2 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +40 -11
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +55 -0
- 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 +78 -12
- package/src/alpha6/adoption.ts +1105 -0
- package/src/alpha6/captured-check-evidence.ts +126 -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 +732 -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 +2182 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity-update.ts +20 -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 +2965 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +471 -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 +351 -0
- package/src/change-explanation.ts +153 -0
- package/src/cli-actions.ts +119 -0
- package/src/cli.ts +1583 -0
- package/src/contracts.ts +1510 -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 +538 -0
- package/src/diagnostics.ts +93 -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/validation.ts +177 -0
- package/src/errors.ts +23 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +103 -0
- package/src/git.ts +158 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +21 -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/observation.ts +198 -0
- package/src/observed-routes.ts +661 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +191 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1876 -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 +947 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +728 -0
- package/src/state/lock.ts +880 -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 +11436 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { PlanStepInput, TaskPlanInput, TaskState } from '../contracts.js';
|
|
2
|
+
|
|
3
|
+
type TaskStep = TaskState['steps'][number];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive the completed Steps whose authority remains valid under a replacement Plan.
|
|
7
|
+
*
|
|
8
|
+
* A completed Step is retained only when its executable definition is unchanged and
|
|
9
|
+
* every dependency is itself retained. The recursive dependency check makes the
|
|
10
|
+
* result independent from candidate Plan ordering and fails closed on malformed
|
|
11
|
+
* dependency cycles even though ordinary Plan validation rejects cycles earlier.
|
|
12
|
+
*/
|
|
13
|
+
export function deriveCompletedStepCarryover(
|
|
14
|
+
task: TaskState,
|
|
15
|
+
candidateSteps: readonly TaskPlanInput['steps'][number][],
|
|
16
|
+
): ReadonlyMap<string, TaskStep> {
|
|
17
|
+
const duplicateCandidateIds = new Set<string>();
|
|
18
|
+
const candidateById = new Map<string, PlanStepInput>();
|
|
19
|
+
for (const candidate of candidateSteps) {
|
|
20
|
+
if (candidateById.has(candidate.id)) duplicateCandidateIds.add(candidate.id);
|
|
21
|
+
else candidateById.set(candidate.id, normalizePlanStepDefinition(candidate));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const exactCompletedById = new Map<string, TaskStep>();
|
|
25
|
+
for (const previous of task.steps) {
|
|
26
|
+
if (previous.status !== 'completed' || duplicateCandidateIds.has(previous.id)) continue;
|
|
27
|
+
const candidate = candidateById.get(previous.id);
|
|
28
|
+
if (!candidate) continue;
|
|
29
|
+
if (samePlanStepDefinition(previous, candidate)) exactCompletedById.set(previous.id, previous);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const retained = new Map<string, TaskStep>();
|
|
33
|
+
const rejected = new Set<string>();
|
|
34
|
+
const visiting = new Set<string>();
|
|
35
|
+
const retain = (stepId: string): boolean => {
|
|
36
|
+
if (retained.has(stepId)) return true;
|
|
37
|
+
if (rejected.has(stepId) || visiting.has(stepId)) return false;
|
|
38
|
+
const previous = exactCompletedById.get(stepId);
|
|
39
|
+
if (!previous) {
|
|
40
|
+
rejected.add(stepId);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
visiting.add(stepId);
|
|
44
|
+
const dependenciesRetained = previous.dependencies.every((dependencyId) => retain(dependencyId));
|
|
45
|
+
visiting.delete(stepId);
|
|
46
|
+
if (!dependenciesRetained) {
|
|
47
|
+
rejected.add(stepId);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
retained.set(stepId, previous);
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (const stepId of exactCompletedById.keys()) retain(stepId);
|
|
55
|
+
return retained;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function normalizePlanStepDefinition(
|
|
59
|
+
step: PlanStepInput | TaskStep,
|
|
60
|
+
): PlanStepInput {
|
|
61
|
+
return {
|
|
62
|
+
id: step.id,
|
|
63
|
+
title: step.title,
|
|
64
|
+
objective: step.objective,
|
|
65
|
+
requirements: [...step.requirements],
|
|
66
|
+
dependencies: [...step.dependencies],
|
|
67
|
+
expectedOutputs: [...step.expectedOutputs],
|
|
68
|
+
allowedWrites: [...step.allowedWrites],
|
|
69
|
+
forbiddenScope: [...step.forbiddenScope],
|
|
70
|
+
checks: [...step.checks],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function samePlanStepDefinition(previous: TaskStep, candidate: PlanStepInput): boolean {
|
|
75
|
+
return JSON.stringify(normalizePlanStepDefinition(previous)) === JSON.stringify(candidate);
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DiscoveryState } from '../contracts.js';
|
|
2
|
+
|
|
3
|
+
export interface DiscoveryReadiness {
|
|
4
|
+
ready: boolean;
|
|
5
|
+
status: 'needs_discovery' | 'needs_clarification' | 'ready_to_materialize';
|
|
6
|
+
missing: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function assessDiscovery(discovery: DiscoveryState): DiscoveryReadiness {
|
|
10
|
+
const missing: string[] = [];
|
|
11
|
+
if (!discovery.goal.trim()) missing.push('goal');
|
|
12
|
+
if (!discovery.outcome?.trim()) missing.push('observable outcome');
|
|
13
|
+
if (discovery.scope.length === 0) missing.push('scope');
|
|
14
|
+
if (discovery.acceptance.length === 0) missing.push('acceptance scenario');
|
|
15
|
+
if (discovery.blockingUnknowns.length > 0) missing.push('resolved blocking unknowns');
|
|
16
|
+
if (missing.length === 0) return { ready: true, status: 'ready_to_materialize', missing };
|
|
17
|
+
const hasInitialUnderstanding = Boolean(discovery.goal.trim() && (discovery.outcome || discovery.scope.length > 0));
|
|
18
|
+
return {
|
|
19
|
+
ready: false,
|
|
20
|
+
status: hasInitialUnderstanding ? 'needs_clarification' : 'needs_discovery',
|
|
21
|
+
missing,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function mergeUnique(current: string[], additions: string[]): string[] {
|
|
26
|
+
return [...new Set([...current, ...additions].map((value) => value.trim()).filter(Boolean))];
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { WorkflowError } from '../errors.js';
|
|
5
|
+
|
|
6
|
+
const HASH64 = /^[a-f0-9]{64}$/;
|
|
7
|
+
const KNOWLEDGE_LINE = /^- Knowledge map: revision ([1-9]\d*), hash ([a-f0-9]{64})$/gm;
|
|
8
|
+
const KNOWLEDGE_SENTINEL = '- Knowledge map: revision <context>, hash <context>';
|
|
9
|
+
|
|
10
|
+
export interface NormalizedPlanSemantics {
|
|
11
|
+
normalized: string;
|
|
12
|
+
semanticHash: string;
|
|
13
|
+
binding: { revision: number; hash: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizePlanKnowledgeBinding(content: string, label: string): NormalizedPlanSemantics {
|
|
17
|
+
const matches = [...content.matchAll(KNOWLEDGE_LINE)];
|
|
18
|
+
if (matches.length !== 1) {
|
|
19
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} Plan must contain exactly one canonical Knowledge Map binding line.`, {
|
|
20
|
+
matchCount: matches.length,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const match = matches[0]!;
|
|
24
|
+
const normalized = content.slice(0, match.index) + KNOWLEDGE_SENTINEL + content.slice(match.index! + match[0].length);
|
|
25
|
+
return {
|
|
26
|
+
normalized,
|
|
27
|
+
semanticHash: sha256Hex(normalized),
|
|
28
|
+
binding: { revision: Number(match[1]), hash: match[2]! },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function assertKnowledgeBindingOnlyPlanChange(historical: string, current: string): string {
|
|
33
|
+
const source = normalizePlanKnowledgeBinding(historical, 'historical');
|
|
34
|
+
const target = normalizePlanKnowledgeBinding(current, 'current');
|
|
35
|
+
if (source.normalized !== target.normalized) {
|
|
36
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective recovery permits only the Knowledge Map binding line to change.');
|
|
37
|
+
}
|
|
38
|
+
return source.semanticHash;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readVerifiedPlanArtifact(taskRoot: string, planHash: string, currentPlanHash: string): string {
|
|
42
|
+
if (!HASH64.test(planHash)) throw new WorkflowError('STATE_CORRUPT', 'Plan hash is malformed.', { planHash });
|
|
43
|
+
const file = planHash === currentPlanHash
|
|
44
|
+
? path.join(taskRoot, 'plan.md')
|
|
45
|
+
: path.join(taskRoot, '.versions', `plan.md.${planHash}`);
|
|
46
|
+
if (!existsSync(file)) {
|
|
47
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact required by lifecycle evidence is missing.', { planHash, file });
|
|
48
|
+
}
|
|
49
|
+
const content = readFileSync(file, 'utf8');
|
|
50
|
+
if (sha256Hex(content) !== planHash) {
|
|
51
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its recorded hash.', { planHash, file });
|
|
52
|
+
}
|
|
53
|
+
return content;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sha256Hex(value: string): string {
|
|
57
|
+
return createHash('sha256').update(value).digest('hex');
|
|
58
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlanRiskAuditStepClassification,
|
|
3
|
+
TaskPlanInput,
|
|
4
|
+
TaskState,
|
|
5
|
+
} from '../contracts.js';
|
|
6
|
+
import { WorkflowError } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
export function validatePlan(plan: TaskPlanInput): void {
|
|
9
|
+
const issues: string[] = [];
|
|
10
|
+
if (!plan.objective.trim()) issues.push('Plan objective is required.');
|
|
11
|
+
if (plan.requirements.length === 0) issues.push('At least one requirement is required.');
|
|
12
|
+
if (plan.acceptance.length === 0) issues.push('At least one acceptance scenario is required.');
|
|
13
|
+
if (plan.steps.length === 0) issues.push('At least one STEP-* is required.');
|
|
14
|
+
if (!plan.knowledgeImpactReason.trim()) issues.push('knowledgeImpactReason is required.');
|
|
15
|
+
if (!Number.isInteger(plan.knowledgeMapRevision) || plan.knowledgeMapRevision < 1) {
|
|
16
|
+
issues.push('knowledgeMapRevision must be a positive integer.');
|
|
17
|
+
}
|
|
18
|
+
if (!/^[a-f0-9]{64}$/.test(plan.knowledgeMapHash)) issues.push('knowledgeMapHash must be a SHA-256 hash.');
|
|
19
|
+
if (plan.knowledgeImpact === 'none' && plan.knowledgeTargets.length > 0) {
|
|
20
|
+
issues.push('knowledgeImpact none cannot declare knowledgeTargets.');
|
|
21
|
+
}
|
|
22
|
+
if (plan.knowledgeImpact !== 'none' && plan.knowledgeTargets.length === 0) {
|
|
23
|
+
issues.push(`knowledgeImpact ${plan.knowledgeImpact} requires at least one knowledgeTarget.`);
|
|
24
|
+
}
|
|
25
|
+
for (const target of plan.knowledgeTargets) {
|
|
26
|
+
if (!target.reason.trim()) issues.push(`Knowledge target ${target.path} requires a reason.`);
|
|
27
|
+
if (target.requirementIds.length === 0) issues.push(`Knowledge target ${target.path} requires requirementIds.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ids = new Set<string>();
|
|
31
|
+
for (const step of plan.steps) {
|
|
32
|
+
if (!/^STEP-[0-9]{3}(?:-[A-Z0-9-]+)?$/.test(step.id)) issues.push(`Invalid Step id: ${step.id}`);
|
|
33
|
+
if (ids.has(step.id)) issues.push(`Duplicate Step id: ${step.id}`);
|
|
34
|
+
ids.add(step.id);
|
|
35
|
+
if (!step.title.trim() || !step.objective.trim()) issues.push(`${step.id} requires title and objective.`);
|
|
36
|
+
if (step.requirements.length === 0) issues.push(`${step.id} must cover at least one requirement.`);
|
|
37
|
+
if (step.allowedWrites.length === 0) issues.push(`${step.id} must declare allowedWrites.`);
|
|
38
|
+
if (step.expectedOutputs.length === 0) issues.push(`${step.id} must declare expectedOutputs.`);
|
|
39
|
+
if (step.checks.length === 0) issues.push(`${step.id} must declare at least one executable check.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const step of plan.steps) {
|
|
43
|
+
for (const dependency of step.dependencies) {
|
|
44
|
+
if (!ids.has(dependency)) issues.push(`${step.id} depends on missing ${dependency}.`);
|
|
45
|
+
if (dependency === step.id) issues.push(`${step.id} cannot depend on itself.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const covered = new Set(plan.steps.flatMap((step) => step.requirements));
|
|
50
|
+
for (const requirement of plan.requirements) {
|
|
51
|
+
if (!covered.has(requirement)) issues.push(`Requirement is not covered by any Step: ${requirement}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const visiting = new Set<string>();
|
|
55
|
+
const visited = new Set<string>();
|
|
56
|
+
const byId = new Map(plan.steps.map((step) => [step.id, step]));
|
|
57
|
+
const visit = (stepId: string): void => {
|
|
58
|
+
if (visited.has(stepId)) return;
|
|
59
|
+
if (visiting.has(stepId)) {
|
|
60
|
+
issues.push(`Step dependency cycle includes ${stepId}.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
visiting.add(stepId);
|
|
64
|
+
for (const dependency of byId.get(stepId)?.dependencies ?? []) visit(dependency);
|
|
65
|
+
visiting.delete(stepId);
|
|
66
|
+
visited.add(stepId);
|
|
67
|
+
};
|
|
68
|
+
for (const step of plan.steps) visit(step.id);
|
|
69
|
+
|
|
70
|
+
const productionEdges = plan.productionEdges ?? [];
|
|
71
|
+
if (plan.requiresProductionEdges && productionEdges.length === 0) {
|
|
72
|
+
issues.push('Plan requiresProductionEdges but declares no explicit production edge.');
|
|
73
|
+
}
|
|
74
|
+
if (!plan.requiresProductionEdges && productionEdges.length > 0) {
|
|
75
|
+
issues.push('Plan declares productionEdges without the explicit requiresProductionEdges boundary.');
|
|
76
|
+
}
|
|
77
|
+
const edgeKeys = new Set<string>();
|
|
78
|
+
for (const edge of productionEdges) {
|
|
79
|
+
const key = `${edge.ownerStepId}\0${edge.consumerStepId}\0${edge.checkRef}`;
|
|
80
|
+
if (edgeKeys.has(key)) issues.push(`Duplicate production edge: ${edge.ownerStepId} -> ${edge.consumerStepId}.`);
|
|
81
|
+
edgeKeys.add(key);
|
|
82
|
+
const owner = byId.get(edge.ownerStepId);
|
|
83
|
+
const consumer = byId.get(edge.consumerStepId);
|
|
84
|
+
if (!owner) issues.push(`Production edge owner is missing: ${edge.ownerStepId}.`);
|
|
85
|
+
if (!consumer) issues.push(`Production edge consumer is missing: ${edge.consumerStepId}.`);
|
|
86
|
+
if (owner && consumer && owner.id === consumer.id) issues.push(`Production edge cannot self-reference ${owner.id}.`);
|
|
87
|
+
if (owner && consumer && !transitiveDependencies(consumer.id, byId).has(owner.id)) {
|
|
88
|
+
issues.push(`Production edge owner ${owner.id} is not a transitive dependency of consumer ${consumer.id}.`);
|
|
89
|
+
}
|
|
90
|
+
if (consumer && !consumer.checks.includes(edge.checkRef)) {
|
|
91
|
+
issues.push(`Production edge ${edge.ownerStepId} -> ${edge.consumerStepId} names a check not executed by its consumer.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (issues.length > 0) throw new WorkflowError('TRANSITION_BLOCKED', 'Plan is not execution-ready.', { issues });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Derive the narrow beta.13 structural integration obligation from the independent
|
|
100
|
+
* Plan Risk Audit rather than trusting the planner-owned opt-in flag. This is not
|
|
101
|
+
* semantic output-to-evidence authority (P02): it only proves that a multi-Step
|
|
102
|
+
* runtime-composition Plan names an inbound production hand-off and executes its
|
|
103
|
+
* exact check in the consumer Step.
|
|
104
|
+
*/
|
|
105
|
+
export function validateStructuralProductionEdgeAuthority(
|
|
106
|
+
task: Pick<TaskState, 'id' | 'steps' | 'productionEdges' | 'requiresProductionEdges'>,
|
|
107
|
+
classifications: readonly PlanRiskAuditStepClassification[],
|
|
108
|
+
): void {
|
|
109
|
+
if (task.steps.length < 2) return;
|
|
110
|
+
const integrationConsumerIds = classifications
|
|
111
|
+
.filter((classification) => classification.categories.includes('runtime-composition'))
|
|
112
|
+
.map((classification) => classification.stepId)
|
|
113
|
+
.sort();
|
|
114
|
+
if (integrationConsumerIds.length === 0) return;
|
|
115
|
+
|
|
116
|
+
const edges = task.productionEdges ?? [];
|
|
117
|
+
const byId = new Map(task.steps.map((step) => [step.id, step]));
|
|
118
|
+
const missingConsumerEdgeIds = integrationConsumerIds.filter((consumerStepId) =>
|
|
119
|
+
!edges.some((edge) => edge.consumerStepId === consumerStepId));
|
|
120
|
+
const blockingGuardIds = [
|
|
121
|
+
...(task.requiresProductionEdges === true ? [] : ['task.production-edge.explicit-boundary']),
|
|
122
|
+
...(edges.length > 0 ? [] : ['task.production-edge.non-empty']),
|
|
123
|
+
...(missingConsumerEdgeIds.length > 0 ? ['task.production-edge.runtime-consumer-covered'] : []),
|
|
124
|
+
];
|
|
125
|
+
for (const edge of edges) {
|
|
126
|
+
const consumer = byId.get(edge.consumerStepId);
|
|
127
|
+
const owner = byId.get(edge.ownerStepId);
|
|
128
|
+
if (!consumer || !owner || !transitiveDependencies(consumer.id, byId).has(owner.id)
|
|
129
|
+
|| !consumer.checks.includes(edge.checkRef)) {
|
|
130
|
+
blockingGuardIds.push('task.production-edge.executable-predecessor-binding');
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (blockingGuardIds.length === 0) return;
|
|
135
|
+
throw new WorkflowError(
|
|
136
|
+
'TRANSITION_BLOCKED',
|
|
137
|
+
`Runtime-composition authority for Task ${task.id} requires explicit production-edge coverage.`,
|
|
138
|
+
{
|
|
139
|
+
diagnosticCode: 'STRUCTURAL_PRODUCTION_EDGE_REQUIRED',
|
|
140
|
+
triggerAuthority: 'plan-risk-audit.runtime-composition',
|
|
141
|
+
integrationConsumerStepIds: integrationConsumerIds,
|
|
142
|
+
missingConsumerEdgeIds,
|
|
143
|
+
blockingGuardIds: [...new Set(blockingGuardIds)],
|
|
144
|
+
semanticAuthorityClaimed: false,
|
|
145
|
+
requiredAction: 'Declare requiresProductionEdges=true and bind each runtime-composition consumer to a transitive owner and one exact consumer check.',
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function transitiveDependencies(
|
|
151
|
+
stepId: string,
|
|
152
|
+
byId: ReadonlyMap<string, TaskPlanInput['steps'][number]>,
|
|
153
|
+
): Set<string> {
|
|
154
|
+
const result = new Set<string>();
|
|
155
|
+
const visit = (candidateId: string): void => {
|
|
156
|
+
for (const dependency of byId.get(candidateId)?.dependencies ?? []) {
|
|
157
|
+
if (result.has(dependency)) continue;
|
|
158
|
+
result.add(dependency);
|
|
159
|
+
visit(dependency);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
visit(stepId);
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function pathAllowed(file: string, patterns: string[]): boolean {
|
|
167
|
+
const normalized = file.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
168
|
+
return patterns.some((pattern) => {
|
|
169
|
+
const candidate = pattern.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
170
|
+
if (candidate.endsWith('/**')) return normalized === candidate.slice(0, -3) || normalized.startsWith(candidate.slice(0, -2));
|
|
171
|
+
if (candidate.endsWith('/*')) {
|
|
172
|
+
const prefix = candidate.slice(0, -1);
|
|
173
|
+
return normalized.startsWith(prefix) && !normalized.slice(prefix.length).includes('/');
|
|
174
|
+
}
|
|
175
|
+
return normalized === candidate;
|
|
176
|
+
});
|
|
177
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type WorkflowErrorCode =
|
|
2
|
+
| 'INVALID_ARGUMENT'
|
|
3
|
+
| 'NOT_FOUND'
|
|
4
|
+
| 'STATE_CONFLICT'
|
|
5
|
+
| 'STATE_CORRUPT'
|
|
6
|
+
| 'LOCKED'
|
|
7
|
+
| 'LOCK_TOKEN_INVALID'
|
|
8
|
+
| 'TASK_RECOVERY_REQUIRED'
|
|
9
|
+
| 'TRANSITION_BLOCKED'
|
|
10
|
+
| 'GIT_PRECONDITION_FAILED'
|
|
11
|
+
| 'COMMAND_FAILED';
|
|
12
|
+
|
|
13
|
+
export class WorkflowError extends Error {
|
|
14
|
+
readonly code: WorkflowErrorCode;
|
|
15
|
+
readonly details: Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
constructor(code: WorkflowErrorCode, message: string, details: Record<string, unknown> = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'WorkflowError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/fs-utils.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { WorkflowError } from './errors.js';
|
|
14
|
+
|
|
15
|
+
export function ensureDirectory(directory: string): void {
|
|
16
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
17
|
+
if (lstatSync(directory).isSymbolicLink()) {
|
|
18
|
+
throw new WorkflowError('STATE_CORRUPT', `State directory must not be a symlink: ${directory}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readJson<T>(file: string): T {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(file, 'utf8')) as T;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new WorkflowError('STATE_CORRUPT', `Cannot read JSON state: ${file}`, {
|
|
27
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeJsonAtomic(file: string, value: unknown): void {
|
|
33
|
+
const directory = path.dirname(file);
|
|
34
|
+
ensureDirectory(directory);
|
|
35
|
+
if (existsSync(file) && lstatSync(file).isSymbolicLink()) {
|
|
36
|
+
throw new WorkflowError('STATE_CORRUPT', `Refusing to replace state symlink: ${file}`);
|
|
37
|
+
}
|
|
38
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
39
|
+
try {
|
|
40
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
41
|
+
chmodSync(temporary, 0o600);
|
|
42
|
+
renameSync(temporary, file);
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(temporary, { force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeTextAtomic(file: string, content: string): void {
|
|
49
|
+
const directory = path.dirname(file);
|
|
50
|
+
ensureDirectory(directory);
|
|
51
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(temporary, content.endsWith('\n') ? content : `${content}\n`, {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
});
|
|
57
|
+
renameSync(temporary, file);
|
|
58
|
+
} finally {
|
|
59
|
+
rmSync(temporary, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION } from './contracts.js';
|
|
2
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
3
|
+
|
|
4
|
+
export interface GatewayHandshake {
|
|
5
|
+
packageName: typeof PACKAGE_NAME;
|
|
6
|
+
packageVersion: typeof PACKAGE_VERSION;
|
|
7
|
+
protocolVersion: typeof PROTOCOL_VERSION;
|
|
8
|
+
stateSchemaVersion: typeof STATE_SCHEMA_VERSION;
|
|
9
|
+
capabilities: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function gatewayHandshake(): GatewayHandshake {
|
|
13
|
+
return {
|
|
14
|
+
packageName: PACKAGE_NAME,
|
|
15
|
+
packageVersion: PACKAGE_VERSION,
|
|
16
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
17
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
18
|
+
capabilities: workflowCapabilities(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function workflowCapabilities(): string[] {
|
|
23
|
+
return [
|
|
24
|
+
'project-memory-v1',
|
|
25
|
+
'graph-binding-v1',
|
|
26
|
+
'local-state-v1',
|
|
27
|
+
'milestone-lifecycle-v1',
|
|
28
|
+
'milestone-human-final-gate-v1',
|
|
29
|
+
'delegated-approval-v1',
|
|
30
|
+
'task-knowledge-rebind-v1',
|
|
31
|
+
'task-context-refresh-v1',
|
|
32
|
+
'corrective-plan-audit-v1',
|
|
33
|
+
'safe-update-preflight-v1',
|
|
34
|
+
'pending-strict-review-dependency-bridge-v1',
|
|
35
|
+
'bounded-dependency-provenance-recovery-v1',
|
|
36
|
+
'task-local-dependency-provenance-recovery-v1',
|
|
37
|
+
'root-cause-corrective-replan-dirty-carryover-v1',
|
|
38
|
+
'legacy-stop-escalate-override-v1',
|
|
39
|
+
'stop-override-context-rebind-v1',
|
|
40
|
+
'stop-override-context-rebind-chain-v1',
|
|
41
|
+
'stop-override-post-audit-dependency-revision-v1',
|
|
42
|
+
'retained-reviewed-history-recovery-v1',
|
|
43
|
+
'historical-step-provenance-recovery-v1',
|
|
44
|
+
'milestone-progress-projection-v1',
|
|
45
|
+
'bounded-plan-integrity-recovery-v1',
|
|
46
|
+
'check-support-scope-augmentation-v1',
|
|
47
|
+
'coherent-plan-integrity-update-source-v1',
|
|
48
|
+
'historical-check-support-scope-adoption-v1',
|
|
49
|
+
'historical-check-support-dirty-carryover-v1',
|
|
50
|
+
'downstream-proof-predecessor-recovery-v1',
|
|
51
|
+
'downstream-proof-replan-recovery-v1',
|
|
52
|
+
'active-downstream-proof-dependency-recovery-v1',
|
|
53
|
+
'downstream-proof-replan-update-recovery-v1',
|
|
54
|
+
'plan-integrity-update-recovery-v1',
|
|
55
|
+
'task-mechanical-feasibility-v1',
|
|
56
|
+
'task-human-gate-lease-release-v1',
|
|
57
|
+
'strict-reviewer-v1',
|
|
58
|
+
'legacy-snapshot-v1',
|
|
59
|
+
'plan-risk-audit-sidecar-v1',
|
|
60
|
+
'step-strict-review-sidecar-v1',
|
|
61
|
+
'step-remediation-circuit-breaker-sidecar-v1',
|
|
62
|
+
'milestone-scope-change-sidecar-v1',
|
|
63
|
+
'milestone-membership-integrity-v1',
|
|
64
|
+
'milestone-task-dependency-dag-v1',
|
|
65
|
+
'milestone-initial-plan-transaction-v1',
|
|
66
|
+
'task-c1-handoff-sidecar-v1',
|
|
67
|
+
'semantic-plan-risk-audit-v2',
|
|
68
|
+
'guarded-remediation-continuation-v1',
|
|
69
|
+
'cause-bound-rolling-refinement-v1',
|
|
70
|
+
'failed-step-continuation-transaction-v1',
|
|
71
|
+
'host-safe-credential-reference-v1',
|
|
72
|
+
'structural-production-edge-contract-v1',
|
|
73
|
+
'structural-production-edge-authority-v1',
|
|
74
|
+
'observed-route-matrix-v2',
|
|
75
|
+
'compact-status-v1',
|
|
76
|
+
'reviewer-runtime-preflight-v1',
|
|
77
|
+
'explicit-review-mode-v1',
|
|
78
|
+
'app-owned-operational-preflight-v1',
|
|
79
|
+
'milestone-reverse-membership-integrity-v1',
|
|
80
|
+
'task-c1-handoff-bundle-v2',
|
|
81
|
+
'alpha6-adoption-posture-v1',
|
|
82
|
+
'alpha6-strict-review-rescue-v1',
|
|
83
|
+
'milestone-initial-assembly-v1',
|
|
84
|
+
'milestone-initial-assembly-navigation-v2',
|
|
85
|
+
'milestone-autonomy-contract-v1',
|
|
86
|
+
'milestone-autonomy-content-refresh-v1',
|
|
87
|
+
'milestone-autonomous-membership-evolution-v1',
|
|
88
|
+
'audited-remediation-topology-v1',
|
|
89
|
+
'audited-remediation-dependency-knowledge-refresh-v1',
|
|
90
|
+
'corrective-derived-task-authority-v1',
|
|
91
|
+
'task-c1-derived-worker-actor-v1',
|
|
92
|
+
'guarded-corrective-auditor-routing-v1',
|
|
93
|
+
'corrective-decision-context-recovery-v1',
|
|
94
|
+
'remediation-mode-recovery-v1',
|
|
95
|
+
'plan-proof-obligations-v1',
|
|
96
|
+
'structural-task-replacement-disabled-v1',
|
|
97
|
+
'project-composite-transaction-journal-v1',
|
|
98
|
+
'corrective-yield-journal-v1',
|
|
99
|
+
'corrective-replan-journal-v1',
|
|
100
|
+
'corrective-replan-human-gate-v1',
|
|
101
|
+
'corrective-replan-protocol-v2',
|
|
102
|
+
];
|
|
103
|
+
}
|