From f9f712098ddfe99e7972bb8d9a4d30143a46fed9 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Tue, 14 Apr 2026 20:41:57 -0500 Subject: [PATCH] feat(gsd-uok): flip default to UOK with emergency legacy fallback --- .../gsd/docs/preferences-reference.md | 6 ++- .../extensions/gsd/preferences-types.ts | 3 ++ .../extensions/gsd/preferences-validation.ts | 4 +- src/resources/extensions/gsd/preferences.ts | 3 ++ .../extensions/gsd/templates/PREFERENCES.md | 4 +- .../extensions/gsd/tests/uok-flags.test.ts | 39 +++++++++++++++++++ .../gsd/tests/uok-preferences.test.ts | 2 + src/resources/extensions/gsd/uok/flags.ts | 13 ++++++- src/resources/extensions/gsd/uok/kernel.ts | 11 ++++-- 9 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 src/resources/extensions/gsd/tests/uok-flags.test.ts diff --git a/src/resources/extensions/gsd/docs/preferences-reference.md b/src/resources/extensions/gsd/docs/preferences-reference.md index 956819e4c..23830100f 100644 --- a/src/resources/extensions/gsd/docs/preferences-reference.md +++ b/src/resources/extensions/gsd/docs/preferences-reference.md @@ -191,8 +191,10 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea - `hooks`: boolean — enable routing hooks. Default: `true`. - `capability_routing`: boolean — enable capability-profile scoring for model selection within a tier. Requires `enabled: true`. Default: `false`. -- `uok`: Unified Orchestration Kernel controls (all flags default to `false` during migration). Keys: - - `enabled`: boolean — enable kernel wrappers and contract observers. +- `uok`: Unified Orchestration Kernel controls. Keys: + - `enabled`: boolean — enable kernel wrappers and contract observers. Default: `true`. + - `legacy_fallback.enabled`: boolean — emergency release fallback that forces legacy orchestration behavior even when `uok.enabled` is `true`. Default: `false`. + - Runtime override: set `GSD_UOK_FORCE_LEGACY=1` (or `GSD_UOK_LEGACY_FALLBACK=1`) to force legacy behavior for the current process. - `gates.enabled`: boolean — route checks through the unified gate runner and persist `gate_runs`. - `model_policy.enabled`: boolean — enforce policy filtering before model capability scoring. - `execution_graph.enabled`: boolean — enable DAG scheduler facade/adapters for execution. diff --git a/src/resources/extensions/gsd/preferences-types.ts b/src/resources/extensions/gsd/preferences-types.ts index 3809f3d20..430c7fe85 100644 --- a/src/resources/extensions/gsd/preferences-types.ts +++ b/src/resources/extensions/gsd/preferences-types.ts @@ -213,6 +213,9 @@ export type UokTurnActionMode = "commit" | "snapshot" | "status-only"; export interface UokPreferences { enabled?: boolean; + legacy_fallback?: { + enabled?: boolean; + }; gates?: { enabled?: boolean; }; diff --git a/src/resources/extensions/gsd/preferences-validation.ts b/src/resources/extensions/gsd/preferences-validation.ts index 198bc97af..78faebf96 100644 --- a/src/resources/extensions/gsd/preferences-validation.ts +++ b/src/resources/extensions/gsd/preferences-validation.ts @@ -178,7 +178,7 @@ export function validatePreferences(preferences: GSDPreferences): { } const parseEnabledBlock = ( - key: "gates" | "model_policy" | "execution_graph" | "audit_unified" | "plan_v2", + key: "legacy_fallback" | "gates" | "model_policy" | "execution_graph" | "audit_unified" | "plan_v2", ): void => { const value = raw[key]; if (value === undefined) return; @@ -201,6 +201,7 @@ export function validatePreferences(preferences: GSDPreferences): { } }; + parseEnabledBlock("legacy_fallback"); parseEnabledBlock("gates"); parseEnabledBlock("model_policy"); parseEnabledBlock("execution_graph"); @@ -243,6 +244,7 @@ export function validatePreferences(preferences: GSDPreferences): { const knownUokKeys = new Set([ "enabled", + "legacy_fallback", "gates", "model_policy", "execution_graph", diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 845676390..414a5f0c8 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -383,6 +383,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr uok: (base.uok || override.uok) ? { enabled: override.uok?.enabled ?? base.uok?.enabled, + legacy_fallback: (base.uok?.legacy_fallback || override.uok?.legacy_fallback) + ? { ...(base.uok?.legacy_fallback ?? {}), ...(override.uok?.legacy_fallback ?? {}) } + : undefined, gates: (base.uok?.gates || override.uok?.gates) ? { ...(base.uok?.gates ?? {}), ...(override.uok?.gates ?? {}) } : undefined, diff --git a/src/resources/extensions/gsd/templates/PREFERENCES.md b/src/resources/extensions/gsd/templates/PREFERENCES.md index 5cbdf757f..9117bf24a 100644 --- a/src/resources/extensions/gsd/templates/PREFERENCES.md +++ b/src/resources/extensions/gsd/templates/PREFERENCES.md @@ -40,7 +40,9 @@ dynamic_routing: cross_provider: hooks: uok: - enabled: false + enabled: true + legacy_fallback: + enabled: false gates: enabled: false model_policy: diff --git a/src/resources/extensions/gsd/tests/uok-flags.test.ts b/src/resources/extensions/gsd/tests/uok-flags.test.ts new file mode 100644 index 000000000..d8ccb9f23 --- /dev/null +++ b/src/resources/extensions/gsd/tests/uok-flags.test.ts @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { resolveUokFlags } from "../uok/flags.ts"; + +test("uok flags default to enabled when preference is unset", () => { + const flags = resolveUokFlags(undefined); + assert.equal(flags.enabled, true); + assert.equal(flags.legacyFallback, false); +}); + +test("uok legacy fallback preference forces legacy path", () => { + const flags = resolveUokFlags({ + uok: { + enabled: true, + legacy_fallback: { enabled: true }, + }, + }); + assert.equal(flags.enabled, false); + assert.equal(flags.legacyFallback, true); +}); + +test("uok legacy fallback env var forces legacy path", () => { + const previous = process.env.GSD_UOK_FORCE_LEGACY; + process.env.GSD_UOK_FORCE_LEGACY = "1"; + try { + const flags = resolveUokFlags({ + uok: { + enabled: true, + }, + }); + assert.equal(flags.enabled, false); + assert.equal(flags.legacyFallback, true); + } finally { + if (previous === undefined) delete process.env.GSD_UOK_FORCE_LEGACY; + else process.env.GSD_UOK_FORCE_LEGACY = previous; + } +}); + diff --git a/src/resources/extensions/gsd/tests/uok-preferences.test.ts b/src/resources/extensions/gsd/tests/uok-preferences.test.ts index b13deeaec..31b141d46 100644 --- a/src/resources/extensions/gsd/tests/uok-preferences.test.ts +++ b/src/resources/extensions/gsd/tests/uok-preferences.test.ts @@ -7,6 +7,7 @@ test("uok preferences validate nested flags and turn_action", () => { const input = { uok: { enabled: true, + legacy_fallback: { enabled: false }, gates: { enabled: true }, model_policy: { enabled: true }, execution_graph: { enabled: false }, @@ -23,6 +24,7 @@ test("uok preferences validate nested flags and turn_action", () => { const result = validatePreferences(input as never); assert.equal(result.errors.length, 0); assert.equal(result.preferences.uok?.enabled, true); + assert.equal(result.preferences.uok?.legacy_fallback?.enabled, false); assert.equal(result.preferences.uok?.gitops?.turn_action, "status-only"); assert.equal(result.preferences.uok?.plan_v2?.enabled, true); }); diff --git a/src/resources/extensions/gsd/uok/flags.ts b/src/resources/extensions/gsd/uok/flags.ts index 24e1cd0c9..8eacf1dd7 100644 --- a/src/resources/extensions/gsd/uok/flags.ts +++ b/src/resources/extensions/gsd/uok/flags.ts @@ -3,6 +3,7 @@ import { loadEffectiveGSDPreferences } from "../preferences.js"; export interface UokFlags { enabled: boolean; + legacyFallback: boolean; gates: boolean; modelPolicy: boolean; executionGraph: boolean; @@ -13,10 +14,20 @@ export interface UokFlags { planV2: boolean; } +function envForcesLegacyFallback(): boolean { + const raw = process.env.GSD_UOK_FORCE_LEGACY ?? process.env.GSD_UOK_LEGACY_FALLBACK; + if (!raw) return false; + const normalized = raw.trim().toLowerCase(); + return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; +} + export function resolveUokFlags(prefs: GSDPreferences | undefined): UokFlags { const uok = prefs?.uok; + const legacyFallback = uok?.legacy_fallback?.enabled === true || envForcesLegacyFallback(); + const enabledByPreference = uok?.enabled ?? true; return { - enabled: uok?.enabled === true, + enabled: enabledByPreference && !legacyFallback, + legacyFallback, gates: uok?.gates?.enabled === true, modelPolicy: uok?.model_policy?.enabled === true, executionGraph: uok?.execution_graph?.enabled === true, diff --git a/src/resources/extensions/gsd/uok/kernel.ts b/src/resources/extensions/gsd/uok/kernel.ts index 656c6db92..69138d4bc 100644 --- a/src/resources/extensions/gsd/uok/kernel.ts +++ b/src/resources/extensions/gsd/uok/kernel.ts @@ -36,6 +36,11 @@ function writeParityEvent(basePath: string, event: Record): voi } } +function resolveKernelPathLabel(flags: ReturnType): "uok-wrapper" | "legacy-wrapper" | "legacy-fallback" { + if (flags.legacyFallback) return "legacy-fallback"; + return flags.enabled ? "uok-wrapper" : "legacy-wrapper"; +} + export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise { const { ctx, pi, s, deps, runLegacyLoop } = args; const prefs = deps.loadEffectiveGSDPreferences()?.preferences; @@ -44,7 +49,7 @@ export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise< writeParityEvent(s.basePath, { ts: new Date().toISOString(), - path: flags.enabled ? "uok-wrapper" : "legacy-wrapper", + path: resolveKernelPathLabel(flags), flags, phase: "enter", }); @@ -81,7 +86,7 @@ export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise< await runLegacyLoop(ctx, pi, s, decoratedDeps); writeParityEvent(s.basePath, { ts: new Date().toISOString(), - path: flags.enabled ? "uok-wrapper" : "legacy-wrapper", + path: resolveKernelPathLabel(flags), flags, phase: "exit", status: "ok", @@ -89,7 +94,7 @@ export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise< } catch (err) { writeParityEvent(s.basePath, { ts: new Date().toISOString(), - path: flags.enabled ? "uok-wrapper" : "legacy-wrapper", + path: resolveKernelPathLabel(flags), flags, phase: "exit", status: "error",