Route default coding tasks through the workflow interpreter only when parity readiness checks pass. - add a core cutover-readiness evaluator and exports for the workflowInterpreterAuthoritative flag - wire a WorkflowAuthoritativeDriver into runtime dispatch and reuse authoritative lifecycle seams with legacy fallback behavior - cover the cutover with reliability tests, docs updates, and a published CLI changeset Files changed: .../FN-5770-workflow-interpreter-authoritative.md | 5 + docs/architecture.md | 1 + docs/settings-reference.md | 5 +- docs/workflow-steps.md | 22 +- .../core/src/__tests__/workflow-cutover.test.ts | 68 +++++ packages/core/src/index.ts | 8 + packages/core/src/workflow-cutover.ts | 81 ++++++ .../src/__tests__/openclaw-runtime-e2e.test.ts | 1 + .../engine/src/__tests__/pi-layers-wiring.test.ts | 2 +- .../branch-recovery-live-zero-commits.test.ts | 2 +- .../branch-recovery-stale-cached-base.test.ts | 2 +- .../workflow-interpreter-cutover.test.ts | 306 +++++++++++++++++++++ .../self-healing-completion-fanout.test.ts | 2 +- .../self-healing-ghost-branch-recovery.test.ts | 2 +- .../self-healing-orphan-only-scope.test.ts | 2 +- .../self-healing-reclaim-live-zero-commits.test.ts | 2 +- .../self-healing-stale-merger-status.test.ts | 2 +- .../src/__tests__/step-session-executor.test.ts | 2 +- .../__tests__/worktree-admin-entry-prune.test.ts | 8 +- packages/engine/src/executor.ts | 13 +- packages/engine/src/index.ts | 6 + packages/engine/src/runtimes/in-process-runtime.ts | 8 + .../engine/src/workflow-authoritative-driver.ts | 154 +++++++++++ 23 files changed, 686 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-5770 Fusion-Task-Lineage: dee668b5-10e0-4a3c-b025-9cc43a26286a
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import type { WorkflowParityDriftReport, WorkflowParitySummary } from "./workflow-parity.js";
|
|
|
|
/**
|
|
* Opt-in authoritative cutover flag for routing the coding lifecycle through the
|
|
* workflow interpreter. The cutover is guarded by rollout-readiness checks and
|
|
* remains reversible by disabling the flag.
|
|
*/
|
|
export const WORKFLOW_INTERPRETER_AUTHORITATIVE_FLAG = "workflowInterpreterAuthoritative" as const;
|
|
|
|
export interface InterpreterCutoverReadinessInput {
|
|
/** Explicit operator opt-in; default runtime remains legacy when false. */
|
|
authoritativeFlagEnabled: boolean;
|
|
/** The dual-observe rollout must stay enabled/proven before cutover. */
|
|
dualObserveEnabled: boolean;
|
|
/** Aggregated parity signal from the audit trail (for example `store.getWorkflowParitySummary()`). */
|
|
paritySummary?: Pick<WorkflowParitySummary, "observed" | "drift" | "recentDrift"> | null;
|
|
/** Optional unresolved drift reports surfaced directly by the caller. */
|
|
unresolvedDriftReports?: readonly Pick<WorkflowParityDriftReport, "agree" | "diffs">[] | null;
|
|
/** Minimum observed parity runs required before cutover may proceed. Default: 1. */
|
|
minimumObservedRuns?: number;
|
|
}
|
|
|
|
export interface InterpreterCutoverReadinessResult {
|
|
ready: boolean;
|
|
reasons: string[];
|
|
}
|
|
|
|
function normalizeMinimumObservedRuns(value: number | undefined): number {
|
|
if (!Number.isFinite(value)) return 1;
|
|
return Math.max(1, Math.floor(value!));
|
|
}
|
|
|
|
function countUnresolvedDriftReports(
|
|
reports: readonly Pick<WorkflowParityDriftReport, "agree" | "diffs">[] | null | undefined,
|
|
): number {
|
|
if (!reports || reports.length === 0) return 0;
|
|
return reports.filter((report) => report.agree === false || report.diffs.length > 0).length;
|
|
}
|
|
|
|
/**
|
|
* Pure rollout-readiness guard for the interpreter-authoritative cutover.
|
|
* Callers supply explicit parity evidence; this function performs no I/O.
|
|
*/
|
|
export function evaluateInterpreterCutoverReadiness(
|
|
input: InterpreterCutoverReadinessInput,
|
|
): InterpreterCutoverReadinessResult {
|
|
const reasons: string[] = [];
|
|
const minimumObservedRuns = normalizeMinimumObservedRuns(input.minimumObservedRuns);
|
|
|
|
if (!input.authoritativeFlagEnabled) {
|
|
reasons.push("experimentalFeatures.workflowInterpreterAuthoritative is disabled");
|
|
}
|
|
|
|
if (!input.dualObserveEnabled) {
|
|
reasons.push("experimentalFeatures.workflowInterpreterDualObserve is disabled");
|
|
}
|
|
|
|
const paritySummary = input.paritySummary;
|
|
if (!paritySummary) {
|
|
reasons.push("workflow parity summary unavailable");
|
|
} else {
|
|
if (paritySummary.observed < minimumObservedRuns) {
|
|
reasons.push(
|
|
`workflow parity observation window too small (${paritySummary.observed}/${minimumObservedRuns} observed)`,
|
|
);
|
|
}
|
|
if (paritySummary.drift > 0) {
|
|
reasons.push(`workflow parity drift above zero (${paritySummary.drift} drift events)`);
|
|
}
|
|
}
|
|
|
|
const unresolvedDriftCount = countUnresolvedDriftReports(input.unresolvedDriftReports);
|
|
if (unresolvedDriftCount > 0) {
|
|
reasons.push(`workflow parity has unresolved drift reports (${unresolvedDriftCount})`);
|
|
}
|
|
|
|
return {
|
|
ready: reasons.length === 0,
|
|
reasons,
|
|
};
|
|
}
|