Files
fusion/packages/core/src/workflow-cutover.ts
gsxdsm 61d687440b FN-5770: promote workflow interpreter to guarded lifecycle driver
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
2026-06-07 18:51:46 -07:00

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,
};
}