FN-6971: retire dual-observe cutover gating

Remove stale dual-observe prerequisites from workflow-authoritative readiness while preserving parity-summary safeguards.

- Require the authoritative flag plus clean populated parity summaries for interpreter cutover readiness.
- Keep persisted workflowInterpreterDualObserve values inert in runtime tests and documentation.
- Update cutover, parity, and graph-executor tests to reflect retired shadow observation behavior.
- Add a formatted patch changeset for the operator-facing cutover readiness fix.

Files changed:
 .changeset/fn-6971-workflow-cutover-readiness.md   |  7 ++
 docs/architecture.md                               |  2 +-
 docs/settings-reference.md                         |  4 +-
 docs/workflow-steps.md                             | 19 +++--
 .../core/src/__tests__/workflow-cutover.test.ts    | 19 +++--
 packages/core/src/workflow-cutover.ts              |  9 +--
 .../workflow-interpreter-cutover.test.ts           | 87 +++++++++++++++++++---
 .../workflow-interpreter-dual-observe.test.ts      | 76 ++++++-------------
 .../src/__tests__/stepwise-workflow-parity.test.ts | 25 ++++---
 .../engine/src/workflow-authoritative-driver.ts    | 10 +--
 10 files changed, 152 insertions(+), 106 deletions(-)

Fusion-Task-Id: FN-6971
Fusion-Task-Lineage: 363a441d-62e5-403c-9389-75fcf788352a
This commit is contained in:
gsxdsm
2026-06-24 22:50:17 -07:00
parent e7021857a9
commit eb3833a542
10 changed files with 152 additions and 106 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Retire dual-observe as a workflow-authoritative cutover prerequisite.
category: fix
dev: Cutover readiness now uses the authoritative flag plus clean populated parity summaries; stale dual-observe settings remain inert.

View File

@@ -1866,7 +1866,7 @@ Reliability-layer changes are in scope. Interaction regression backstops live in
- FN-5741 backstop: `packages/engine/src/__tests__/reliability-interactions/merge-request-shadow-handoff.test.ts` guards Phase-1 merge-request contract shadow writes: flag OFF is a no-op, flag ON writes marker/record strictly after legacy handoff, and `autoMerge:false` remains `manual-required` without shadow running transitions.
- FN-5742 backstop: `packages/engine/src/__tests__/reliability-interactions/dual-observe-merge-seam.test.ts` guards Phase-2 dual-observe invariants: legacy dependency satisfaction remains authoritative while parity diffs emit, and shadow dequeue selection never advances `manual-required` rows.
- FN-5743 backstop: `packages/engine/src/__tests__/reliability-interactions/merge-request-cancel-on-hard-cancel.test.ts` and `packages/core/src/__tests__/merge-request-record.test.ts` guard Phase-3 cutover invariants: transient merge retries mutate merge-request state (no column rebound), user hard-cancel after accepted handoff cancels pending merge requests, and non-user rebounds preserve legacy fail-soft semantics.
- FN-5770 backstop: `packages/engine/src/__tests__/reliability-interactions/workflow-interpreter-cutover.test.ts` guards the interpreter-authoritative lifecycle seam. The cutover remains opt-in (`workflowInterpreterAuthoritative` default OFF), readiness-gated by dual-observe parity evidence, reversible by flipping one flag back OFF, and must preserve file-scope, squash-overlap, `autoMerge:false`, hard-cancel, and self-healing interaction invariants.
- FN-5770 backstop: `packages/engine/src/__tests__/reliability-interactions/workflow-interpreter-cutover.test.ts` guards the interpreter-authoritative lifecycle seam. The cutover remains opt-in (`workflowInterpreterAuthoritative` default OFF), readiness-gated by clean populated parity summary evidence while retired `workflowInterpreterDualObserve` settings stay inert, reversible by flipping one flag back OFF, and must preserve file-scope, squash-overlap, `autoMerge:false`, hard-cancel, and self-healing interaction invariants.
- FN-5337 backstop: `packages/engine/src/__tests__/reliability-interactions/orphan-detected-no-requeue.test.ts` locks observation-only orphan detection across FN-5279 repro metadata desync, worktree-present and worktree-missing candidates, FN-5219 ordering, FN-5147 in-review isolation, FN-5083 branch-cleared composition, lease-manager non-invocation, and per-sweep idempotent audit emission.
- FN-5256 backstop: `packages/engine/src/__tests__/reliability-interactions/dependency-cycle-reconcile.test.ts` covers persisted dependency-cycle detection via `reconcileDependencyCycles`, bounded umbrella-back-edge auto-repair, ambiguous-cycle observe-only behavior, composition ordering with `reconcileSelfDefeatingDependencies`, and the post-sweep write-time guard invariant. Core write-boundary regressions (FN-5240/5241/5242 signature, indirect cycle, umbrella back-edge rejection) live in `packages/core/src/__tests__/store-dependency-cycle.test.ts`.
- FN-5223 backstop: `packages/engine/src/__tests__/reliability-interactions/engine-active-since-floor.test.ts` covers engine-activation floor + grace composition across startup, pause/unpause, global-pause gating, and StuckTaskDetector lifecycle interactions.

View File

@@ -1306,8 +1306,8 @@ Common built-in dashboard/runtime flags include:
- `researchView`
- `evalsView` (gates Evals dashboard view, Settings → Scheduled Evals section, and scheduled-eval cron execution)
- `workflowGraphExecutor` (enables the workflow-IR interpreter path)
- `workflowInterpreterDualObserve` (observe-only parity instrumentation for interpreter rollout)
- `workflowInterpreterAuthoritative` (readiness-gated authoritative interpreter lifecycle cutover; legacy remains default/fallback when OFF)
- `workflowInterpreterDualObserve` (retired/inert; stale persisted `true` values are forced OFF and must not reactivate hidden shadow observation)
- `workflowInterpreterAuthoritative` (readiness-gated authoritative interpreter lifecycle cutover; requires clean populated parity summary evidence and legacy remains default/fallback when OFF)
- `remoteAccess`
- `agentOnboarding` (enables the **AI Interview** option inside the New Agent dialog)

View File

@@ -501,23 +501,22 @@ For pre-merge workflow hard failures, executor behavior is (gate-mode steps):
Tasks are not parked in `in-review` for this remediable path unless additional terminal failures occur.
## Workflow Interpreter Dual-Observe (parity instrumentation)
## Workflow Interpreter Dual-Observe (retired parity instrumentation)
Fusion now enables the workflow interpreter parity seam by default as part of the workflow rollout.
The workflow interpreter dual-observe seam is retired. `experimentalFeatures.workflowInterpreterDualObserve` is now inert: runtime feature helpers force it OFF even when stale persisted settings contain `true`, and Fusion must not invisibly re-enable shadow interpreter observation.
- **Flag:** `experimentalFeatures.workflowInterpreterDualObserve` (default ON)
- **Mode:** observe-only shadow run; legacy executor/reviewer/merger/scheduler path remains authoritative unless the authoritative cutover guard passes
- **Behavior when OFF:** strict no-op (no shadow run, no parity audit records)
- **Behavior when ON (default):** compare legacy and interpreter observations plus comparable run-audit slices
- **Flag:** `experimentalFeatures.workflowInterpreterDualObserve` (retired; forced OFF)
- **Mode:** strict no-op (no shadow run, no parity audit records)
- **Historical behavior:** earlier rollout builds compared legacy and interpreter observations plus comparable run-audit slices and emitted the parity audit records below
Run-audit events emitted in `database` domain:
Historical run-audit events in the `database` domain:
- `workflow:parity-observed` — always emitted for an enabled parity check with `metadata.agree`
- `workflow:parity-drift` — emitted when parity differs (or shadow execution fails), carrying `metadata.diffs`
- `workflow:parity-observed` — emitted for an enabled parity check with `metadata.agree`
- `workflow:parity-drift` — emitted when parity differed (or shadow execution failed), carrying `metadata.diffs`
The parity contract is exported from `@fusion/core` (`compareWorkflowRunObservations`, `compareWorkflowRunAudits`) and produces deterministic drift reports shaped as `{ agree, diffs[] }`, where each diff includes field name, legacy/interpreter values, category, and severity.
Dual-observe remains the rollout evidence path for the later authoritative cutover: the interpreter may only become authoritative when the separate `experimentalFeatures.workflowInterpreterAuthoritative` flag is ON **and** the cutover-readiness guard reports zero unresolved parity drift.
Authoritative cutover now depends on existing/current parity summary evidence, not on re-enabling dual-observe. The interpreter may become authoritative only when the separate `experimentalFeatures.workflowInterpreterAuthoritative` flag is ON and the cutover-readiness guard sees a populated parity summary with enough observed runs, zero summary drift, and zero unresolved parity reports.
#### Self-healing recovery for parked review tasks

View File

@@ -3,10 +3,9 @@ import { describe, expect, it } from "vitest";
import { evaluateInterpreterCutoverReadiness } from "../workflow-cutover.js";
describe("workflow interpreter authoritative cutover readiness", () => {
it("is ready when the cutover flag, dual-observe, and clean parity evidence are present", () => {
it("is ready when the cutover flag and clean parity evidence are present", () => {
const result = evaluateInterpreterCutoverReadiness({
authoritativeFlagEnabled: true,
dualObserveEnabled: true,
paritySummary: { observed: 5, drift: 0, recentDrift: [] },
minimumObservedRuns: 3,
});
@@ -14,25 +13,32 @@ describe("workflow interpreter authoritative cutover readiness", () => {
expect(result).toEqual({ ready: true, reasons: [] });
});
it("enumerates every failed criterion deterministically", () => {
it("enumerates every failed active criterion deterministically", () => {
const result = evaluateInterpreterCutoverReadiness({
authoritativeFlagEnabled: false,
dualObserveEnabled: false,
paritySummary: null,
});
expect(result.ready).toBe(false);
expect(result.reasons).toEqual([
"experimentalFeatures.workflowInterpreterAuthoritative is disabled",
"experimentalFeatures.workflowInterpreterDualObserve is disabled",
"workflow parity summary unavailable",
]);
});
it("does not require the retired dual-observe flag when clean parity evidence exists", () => {
const result = evaluateInterpreterCutoverReadiness({
authoritativeFlagEnabled: true,
paritySummary: { observed: 3, drift: 0, recentDrift: [] },
minimumObservedRuns: 3,
});
expect(result).toEqual({ ready: true, reasons: [] });
});
it("blocks when parity drift is present in the summary or unresolved drift reports", () => {
const result = evaluateInterpreterCutoverReadiness({
authoritativeFlagEnabled: true,
dualObserveEnabled: true,
paritySummary: {
observed: 8,
drift: 2,
@@ -55,7 +61,6 @@ describe("workflow interpreter authoritative cutover readiness", () => {
it("normalizes minimum observed runs to at least one", () => {
const result = evaluateInterpreterCutoverReadiness({
authoritativeFlagEnabled: true,
dualObserveEnabled: true,
paritySummary: { observed: 0, drift: 0, recentDrift: [] },
minimumObservedRuns: 0,
});

View File

@@ -10,8 +10,6 @@ export const WORKFLOW_INTERPRETER_AUTHORITATIVE_FLAG = "workflowInterpreterAutho
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. */
@@ -40,6 +38,9 @@ function countUnresolvedDriftReports(
/**
* Pure rollout-readiness guard for the interpreter-authoritative cutover.
* Callers supply explicit parity evidence; this function performs no I/O.
*
* FNXC:WorkflowInterpreterCutover 2026-06-23-21:58:
* workflowInterpreterDualObserve is retired and inert. Authoritative cutover must use the explicit authoritative flag plus clean populated parity summaries as evidence, without reactivating hidden shadow observation.
*/
export function evaluateInterpreterCutoverReadiness(
input: InterpreterCutoverReadinessInput,
@@ -51,10 +52,6 @@ export function evaluateInterpreterCutoverReadiness(
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");

View File

@@ -1,12 +1,18 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { DEFAULT_SETTINGS, type Settings, type TaskDetail } from "@fusion/core";
import {
DEFAULT_SETTINGS,
isExperimentalFeatureEnabled,
type Settings,
type TaskDetail,
} from "@fusion/core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { assertSquashOverlapsFileScope, FileScopeViolationError } from "../../merger.js";
import type { WorkflowLegacySeams } from "../../workflow-node-handlers.js";
import { WorkflowAuthoritativeDriver } from "../../workflow-authoritative-driver.js";
import { observeWorkflowParity } from "../../workflow-parity-observer.js";
import { git, hasGit, makeReliabilityFixture } from "./_helpers.js";
const readyParity = {
@@ -41,13 +47,15 @@ function createStore(options: {
settings?: Settings;
selection?: { workflowId: string; stepIds: string[] } | undefined;
task?: TaskDetail;
paritySummary?: typeof readyParity | undefined;
paritySummary?: typeof readyParity | null;
} = {}) {
return {
getSettings: vi.fn(async () => options.settings ?? settingsWith({ workflowInterpreterAuthoritative: true, workflowInterpreterDualObserve: true })),
getSettings: vi.fn(async () => options.settings ?? settingsWith({ workflowInterpreterAuthoritative: true })),
getTask: vi.fn(async () => options.task ?? baseTask),
getTaskWorkflowSelection: vi.fn(() => options.selection),
getWorkflowParitySummary: vi.fn(() => options.paritySummary ?? readyParity),
getWorkflowParitySummary: vi.fn(() => (
options.paritySummary === null ? undefined : options.paritySummary ?? readyParity
)),
};
}
@@ -60,7 +68,7 @@ function createExecutor(seams: WorkflowLegacySeams) {
describe("workflow interpreter authoritative cutover", () => {
it("is a strict no-op when the cutover flag is off", async () => {
const store = createStore({
settings: settingsWith({ workflowInterpreterAuthoritative: false, workflowInterpreterDualObserve: true }),
settings: settingsWith({ workflowInterpreterAuthoritative: false }),
});
const executor = createExecutor({
planning: vi.fn(async () => ({ outcome: "success" as const })),
@@ -77,6 +85,25 @@ describe("workflow interpreter authoritative cutover", () => {
expect(executor.createAuthoritativeWorkflowSeams).not.toHaveBeenCalled();
});
it("falls back when parity summary is missing even if the cutover flag is on", async () => {
const store = createStore({
paritySummary: null,
});
const executor = createExecutor({
planning: vi.fn(async () => ({ outcome: "success" as const })),
execute: vi.fn(async () => ({ outcome: "success" as const })),
review: vi.fn(async () => ({ outcome: "success" as const })),
merge: vi.fn(async () => ({ outcome: "success" as const })),
schedule: vi.fn(async () => ({ outcome: "success" as const })),
});
const result = await new WorkflowAuthoritativeDriver({ store, executor }).maybeRun(baseTask as any);
expect(result.handled).toBe(false);
expect(result.reason).toContain("workflow parity summary unavailable");
expect(executor.createAuthoritativeWorkflowSeams).not.toHaveBeenCalled();
});
it("falls back when readiness fails even if the cutover flag is on", async () => {
const store = createStore({
paritySummary: { ...readyParity, observed: 4, drift: 1 },
@@ -122,6 +149,46 @@ describe("workflow interpreter authoritative cutover", () => {
expect(calls).toEqual(["execute", "review", "merge"]);
});
it("keeps stale dual-observe settings inert while clean parity authorizes seams", async () => {
const runShadow = vi.fn(async () => ({ observation: {} as any, auditEvents: [] }));
await observeWorkflowParity({
settings: settingsWith({ workflowInterpreterDualObserve: true }),
store: { recordRunAuditEvent: vi.fn() },
agentId: "agent-test",
legacy: { taskId: baseTask.id, observation: {} as any, auditEvents: [] },
runShadow,
});
expect(
isExperimentalFeatureEnabled(
settingsWith({ workflowInterpreterDualObserve: true }),
"workflowInterpreterDualObserve",
),
).toBe(false);
expect(runShadow).not.toHaveBeenCalled();
const execute = vi.fn(async () => ({ outcome: "success" as const }));
const executor = createExecutor({
planning: async () => ({ outcome: "success" as const }),
execute,
review: async () => ({ outcome: "success" as const }),
merge: async () => ({ outcome: "success" as const }),
schedule: async () => ({ outcome: "success" as const }),
});
const result = await new WorkflowAuthoritativeDriver({
store: createStore({
settings: settingsWith({
workflowInterpreterAuthoritative: true,
workflowInterpreterDualObserve: true,
}),
}),
executor,
}).maybeRun(baseTask as any);
expect(result.handled).toBe(true);
expect(execute).toHaveBeenCalledTimes(1);
});
it("keeps autoMerge:false tasks terminal in review by stopping before merge", async () => {
const merge = vi.fn(async () => ({ outcome: "success" as const }));
const executor = createExecutor({
@@ -133,7 +200,9 @@ describe("workflow interpreter authoritative cutover", () => {
});
const result = await new WorkflowAuthoritativeDriver({
store: createStore({ settings: settingsWith({ workflowInterpreterAuthoritative: true, workflowInterpreterDualObserve: true }, { autoMerge: false }) }),
store: createStore({
settings: settingsWith({ workflowInterpreterAuthoritative: true }, { autoMerge: false }),
}),
executor,
}).maybeRun(baseTask as any);
@@ -186,7 +255,7 @@ describe("workflow interpreter authoritative cutover", () => {
});
it("immediately rolls back to legacy when the cutover flag is flipped back off", async () => {
let settings = settingsWith({ workflowInterpreterAuthoritative: true, workflowInterpreterDualObserve: true });
let settings = settingsWith({ workflowInterpreterAuthoritative: true });
const store = createStore();
store.getSettings.mockImplementation(async () => settings);
const executor = createExecutor({
@@ -199,7 +268,7 @@ describe("workflow interpreter authoritative cutover", () => {
const driver = new WorkflowAuthoritativeDriver({ store, executor });
const first = await driver.maybeRun(baseTask as any);
settings = settingsWith({ workflowInterpreterAuthoritative: false, workflowInterpreterDualObserve: true });
settings = settingsWith({ workflowInterpreterAuthoritative: false });
const second = await driver.maybeRun(baseTask as any);
expect(first.handled).toBe(true);
@@ -251,7 +320,7 @@ describeIfGit("workflow interpreter authoritative cutover + file-scope invariant
const driver = new WorkflowAuthoritativeDriver({
store: {
getSettings: async () => settingsWith({ workflowInterpreterAuthoritative: true, workflowInterpreterDualObserve: true }),
getSettings: async () => settingsWith({ workflowInterpreterAuthoritative: true }),
getTask: (taskId) => fx.store.getTask(taskId) as Promise<TaskDetail>,
getTaskWorkflowSelection: () => undefined,
getWorkflowParitySummary: () => readyParity,

View File

@@ -40,8 +40,9 @@ describe("FN-5768 workflow interpreter dual-observe", () => {
expect(recordRunAuditEvent).not.toHaveBeenCalled();
});
it("records parity-observed agree=true when observations match", async () => {
it("keeps retired persisted true values inert instead of shadow-observing", async () => {
const recordRunAuditEvent = vi.fn().mockResolvedValue(undefined);
const runShadow = vi.fn(async () => ({ observation: baseObservation, auditEvents: [] }));
await observeWorkflowParity({
settings: { experimentalFeatures: { [WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG]: true } },
@@ -52,21 +53,23 @@ describe("FN-5768 workflow interpreter dual-observe", () => {
observation: baseObservation,
auditEvents: [],
},
runShadow: async () => ({ observation: baseObservation, auditEvents: [] }),
runShadow,
});
expect(recordRunAuditEvent).toHaveBeenCalledTimes(1);
expect(recordRunAuditEvent).toHaveBeenCalledWith(
expect.objectContaining({
mutationType: "workflow:parity-observed",
metadata: expect.objectContaining({ agree: true }),
}),
);
expect(runShadow).not.toHaveBeenCalled();
expect(recordRunAuditEvent).not.toHaveBeenCalled();
});
it("records parity drift and keeps authoritative result unchanged", async () => {
it("does not run a drift shadow when the retired flag is stale true", async () => {
const recordRunAuditEvent = vi.fn().mockResolvedValue(undefined);
const legacyResult = { authoritative: true };
const runShadow = vi.fn(async () => ({
observation: {
...baseObservation,
terminalColumn: "in-review",
},
auditEvents: [],
}));
await observeWorkflowParity({
settings: { experimentalFeatures: { [WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG]: true } },
@@ -77,35 +80,19 @@ describe("FN-5768 workflow interpreter dual-observe", () => {
observation: baseObservation,
auditEvents: [],
},
runShadow: async () => ({
observation: {
...baseObservation,
terminalColumn: "in-review",
},
auditEvents: [],
}),
runShadow,
});
expect(legacyResult).toEqual({ authoritative: true });
expect(recordRunAuditEvent).toHaveBeenCalledTimes(2);
expect(recordRunAuditEvent).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ mutationType: "workflow:parity-observed" }),
);
expect(recordRunAuditEvent).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
mutationType: "workflow:parity-drift",
metadata: expect.objectContaining({
agree: false,
diffs: expect.arrayContaining([expect.objectContaining({ field: "terminalColumn" })]),
}),
}),
);
expect(runShadow).not.toHaveBeenCalled();
expect(recordRunAuditEvent).not.toHaveBeenCalled();
});
it("captures shadow errors fail-soft without rethrow", async () => {
it("does not execute stale shadow callbacks that would fail", async () => {
const recordRunAuditEvent = vi.fn().mockResolvedValue(undefined);
const runShadow = vi.fn(async () => {
throw new Error("shadow exploded");
});
await expect(
observeWorkflowParity({
@@ -117,28 +104,11 @@ describe("FN-5768 workflow interpreter dual-observe", () => {
observation: baseObservation,
auditEvents: [],
},
runShadow: async () => {
throw new Error("shadow exploded");
},
runShadow,
}),
).resolves.toBeUndefined();
expect(recordRunAuditEvent).toHaveBeenCalledTimes(2);
expect(recordRunAuditEvent).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
mutationType: "workflow:parity-observed",
metadata: expect.objectContaining({ agree: false }),
}),
);
expect(recordRunAuditEvent).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
mutationType: "workflow:parity-drift",
metadata: expect.objectContaining({
diffs: expect.arrayContaining([expect.objectContaining({ field: "shadow.error" })]),
}),
}),
);
expect(runShadow).not.toHaveBeenCalled();
expect(recordRunAuditEvent).not.toHaveBeenCalled();
});
});

View File

@@ -513,10 +513,12 @@ describe("stepwise workflow parity (U7 / KTD-9)", () => {
// ── Flag posture (R10) ─────────────────────────────────────────────────────
it("flag pinned-at-dispatch: flag OFF → graph executor is a strict no-op (legacy path owns the run)", async () => {
// With the flag OFF at dispatch, the graph executor does not run at all — the
// legacy step-session path owns the task. Toggling the flag mid-run cannot
// switch paths because the run never entered the graph.
it("graduated graph executor ignores stale false experimental flag", async () => {
/*
FNXC:WorkflowSettings 2026-06-23-22:28:
workflowGraphExecutor graduated from Experimental; stale persisted false values are ignored so the graph attempts authoritative execution rather than silently rolling back to the legacy path.
This fixture still lacks parse-step dependencies, so the graph fails before step execution.
*/
const task = taskWithSteps(2);
let stepExecuteCalls = 0;
const seams: WorkflowLegacySeams = {
@@ -533,24 +535,23 @@ describe("stepwise workflow parity (U7 / KTD-9)", () => {
const executor = new WorkflowGraphExecutor({ seams });
const result = await executor.run(task, settingsOff(), BUILTIN_STEPWISE_CODING_WORKFLOW_IR);
expect(result.executed).toBe(false);
expect(result.executed).toBe(true);
expect(result.outcome).toBe("failure");
expect(stepExecuteCalls).toBe(0);
});
it("OFF-rollback: a stepwise run with the flag OFF leaves steps[] (git-reconcilable) as surviving truth", async () => {
// KTD-8 OFF-rollback: instance rows are swept and steps[] — always
// git-reconcilable — is the surviving truth that legacy resume reconciles
// from. With the flag OFF the graph never writes, so the pre-existing steps[]
// projection (legacy's truth) is untouched; legacy resume then completes.
it("graduated graph executor preserves steps[] truth while running with stale false flag", async () => {
/*
FNXC:WorkflowSettings 2026-06-23-22:28:
Stale workflowGraphExecutor=false no longer disables the graph. If required graph dependencies are absent, rollback safety means the existing git-reconcilable steps[] projection remains the surviving truth.
*/
const task = taskWithSteps(2);
// Simulate a partially-progressed legacy projection (step 0 done by legacy).
(task.steps as TaskStep[])[0] = { name: "Step 1", status: "done" };
const executor = new WorkflowGraphExecutor({ seams: undefined });
const result = await executor.run(task, settingsOff(), BUILTIN_STEPWISE_CODING_WORKFLOW_IR);
expect(result.executed).toBe(false);
// steps[] is untouched by the (no-op) graph — legacy's projection survives.
expect(result.executed).toBe(true);
expect((task.steps as TaskStep[])[0].status).toBe("done");
expect((task.steps as TaskStep[])[1].status).toBe("pending");
});

View File

@@ -12,7 +12,6 @@ import {
import type { TaskExecutor } from "./executor.js";
import { executorLog } from "./logger.js";
import { WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG } from "./workflow-parity-observer.js";
import { WorkflowGraphTaskRunner, type WorkflowGraphTaskRunResult } from "./workflow-graph-task-runner.js";
import type { StepReviewSeamResult, WorkflowLegacySeams } from "./workflow-node-handlers.js";
import type { PreparedWorktree, WorkflowRuntimePrimitives } from "./runtime-primitives.js";
@@ -148,13 +147,12 @@ export class WorkflowAuthoritativeDriver {
settings,
WORKFLOW_INTERPRETER_AUTHORITATIVE_FLAG,
);
const dualObserveEnabled = isExperimentalFeatureEnabled(
settings,
WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG,
);
/*
FNXC:WorkflowInterpreterCutover 2026-06-23-21:59:
The retired workflowInterpreterDualObserve flag must remain inert in the authoritative driver. Historical/current parity summaries from the store are the readiness evidence source; do not probe or re-enable the hidden shadow observer here.
*/
const readiness = evaluateInterpreterCutoverReadiness({
authoritativeFlagEnabled,
dualObserveEnabled,
paritySummary,
minimumObservedRuns: this.deps.minimumObservedRuns,
});