From a2c55d5fded8b0f2c2ee3ad47cdd357622657d21 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 14 May 2026 18:18:17 +0200 Subject: [PATCH] feat(uok,autonomous-loop): wire pre-dispatch/guard/finalize gates to schema-v2 ctx Slice 3b of "Make UOK the SF Control Plane". The autonomous loop's three high-traffic gate sites (resource-version-guard, pre-dispatch-health-gate, planning-flow-gate in phases-pre-dispatch; plan-gate in phases-guards; unit-verification-gate in phases-finalize) now build a schema-v2 UOK run-context per iteration and pass surface/runControl/permissionProfile/parentTrace into the gate runner. The gate-runner emits these onto every gate_run trace event, so the classifier in `sf headless status uok --json` reads them as coverageStatus: "ok" instead of "legacy". New helper uok/auto-uok-ctx.js pins surface="autonomous" and runControl="autonomous" for these phases and derives permissionProfile from session/prefs: "low" under YOLO or a minimal/low permissionLevel, "medium" for medium, "high" otherwise (the default). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../extensions/sf/auto/phases-finalize.js | 23 ++++++ .../extensions/sf/auto/phases-guards.js | 20 +++++ .../extensions/sf/auto/phases-pre-dispatch.js | 20 +++++ .../extensions/sf/uok/auto-uok-ctx.js | 78 +++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 src/resources/extensions/sf/uok/auto-uok-ctx.js diff --git a/src/resources/extensions/sf/auto/phases-finalize.js b/src/resources/extensions/sf/auto/phases-finalize.js index d9d2a00ab..ada0a96b4 100644 --- a/src/resources/extensions/sf/auto/phases-finalize.js +++ b/src/resources/extensions/sf/auto/phases-finalize.js @@ -81,6 +81,7 @@ import { countChangedFiles, resetRunawayGuardState, } from "../uok/auto-runaway-guard.js"; +import { buildAutonomousUokContext } from "../uok/auto-uok-ctx.js"; import { resolveUokFlags } from "../uok/flags.js"; import { UokGateRunner } from "../uok/gate-runner.js"; import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js"; @@ -328,6 +329,24 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) { }; }, }); + // Schema-v2 run-context: finalize runs after unit dispatch + // in the autonomous loop, so surface/runControl are + // "autonomous"; permissionProfile follows session/prefs. + // sliceId/taskId are pulled from the unit id when the unit + // is execute-task (M*/S*/T*); other unit types just leave + // them undefined. + const parsed = parseUnitId(iterData.unitId); + const v2Ctx = buildAutonomousUokContext({ + s, + prefs: ic.prefs, + traceId: `finalize:${ic.flowId}`, + parentTrace: ic.flowId, + unitType: iterData.unitType, + unitId: iterData.unitId, + milestoneId: iterData.mid ?? parsed.milestone, + sliceId: parsed.slice, + taskId: parsed.task, + }); const gateResult = await vgRunner.run("unit-verification-gate", { basePath: s.basePath, traceId: `finalize:${ic.flowId}`, @@ -335,6 +354,10 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) { milestoneId: iterData.mid ?? undefined, unitType: iterData.unitType, unitId: iterData.unitId, + surface: v2Ctx?.surface, + runControl: v2Ctx?.runControl, + permissionProfile: v2Ctx?.permissionProfile, + parentTrace: v2Ctx?.parentTrace, }); if (gateResult.outcome !== "pass") { recordLearningOutcomeForUnit( diff --git a/src/resources/extensions/sf/auto/phases-guards.js b/src/resources/extensions/sf/auto/phases-guards.js index cd6dc4d94..652b410f8 100644 --- a/src/resources/extensions/sf/auto/phases-guards.js +++ b/src/resources/extensions/sf/auto/phases-guards.js @@ -82,6 +82,7 @@ import { resetRunawayGuardState, } from "../uok/auto-runaway-guard.js"; import { resolveUokFlags } from "../uok/flags.js"; +import { buildAutonomousUokContext } from "../uok/auto-uok-ctx.js"; import { UokGateRunner } from "../uok/gate-runner.js"; import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js"; import { @@ -414,6 +415,21 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) { rationale: planGateRationale || "Plan files verified", }), }); + // Schema-v2 run-context: this gate runs inside the autonomous + // loop's guard phase, so surface/runControl are "autonomous" and + // permissionProfile follows session/prefs. parentTrace points at + // the iteration's flow id so the gate is linkable back to the + // loop run that triggered it. + const v2Ctx = buildAutonomousUokContext({ + s, + prefs, + traceId: `guard:${ic.flowId}`, + parentTrace: ic.flowId, + unitType, + unitId, + milestoneId: mid, + sliceId, + }); const planGateResult = await planGateRunner.run("plan-gate", { basePath: s.basePath, traceId: `guard:${ic.flowId}`, @@ -422,6 +438,10 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) { sliceId, unitType, unitId, + surface: v2Ctx?.surface, + runControl: v2Ctx?.runControl, + permissionProfile: v2Ctx?.permissionProfile, + parentTrace: v2Ctx?.parentTrace, }); if (planGateResult.outcome !== "pass") { ctx.ui.notify( diff --git a/src/resources/extensions/sf/auto/phases-pre-dispatch.js b/src/resources/extensions/sf/auto/phases-pre-dispatch.js index b63923616..8f1b4c57c 100644 --- a/src/resources/extensions/sf/auto/phases-pre-dispatch.js +++ b/src/resources/extensions/sf/auto/phases-pre-dispatch.js @@ -82,6 +82,7 @@ import { countChangedFiles, resetRunawayGuardState, } from "../uok/auto-runaway-guard.js"; +import { buildAutonomousUokContext } from "../uok/auto-uok-ctx.js"; import { resolveUokFlags } from "../uok/flags.js"; import { UokGateRunner } from "../uok/gate-runner.js"; import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js"; @@ -162,6 +163,17 @@ export function surfaceSelfFeedbackQueueOnIdle(ctx, basePath, exitReason) { export async function runPreDispatch(ic, loopState) { const { ctx, pi, s, deps, prefs } = ic; const uokFlags = resolveUokFlags(prefs); + // Schema-v2 run-context for pre-dispatch gates. surface/runControl + // are pinned by buildAutonomousUokContext (this phase always runs + // inside the autonomous loop); permissionProfile is derived from + // session/prefs (YOLO + read-only sessions → "low", otherwise "high"). + const v2Ctx = buildAutonomousUokContext({ + s, + prefs, + traceId: `pre-dispatch:${ic.flowId}`, + unitType: "pre-dispatch", + unitId: `iter-${ic.iteration}`, + }); const runPreDispatchGate = async (input) => { if (!uokFlags.gates) return; const gateRunner = new UokGateRunner(); @@ -182,6 +194,14 @@ export async function runPreDispatch(ic, loopState) { milestoneId: input.milestoneId ?? s.currentMilestoneId ?? undefined, unitType: "pre-dispatch", unitId: `iter-${ic.iteration}`, + // Schema-v2 fields propagated from the iteration-level ctx. When + // buildUokRunContext returned null (impossible for the values we + // pass today, but defensive), these stay undefined and the gate + // classifies as "legacy" as it did before this slice. + surface: v2Ctx?.surface, + runControl: v2Ctx?.runControl, + permissionProfile: v2Ctx?.permissionProfile, + parentTrace: v2Ctx?.parentTrace, }); }; // Resource version guard diff --git a/src/resources/extensions/sf/uok/auto-uok-ctx.js b/src/resources/extensions/sf/uok/auto-uok-ctx.js new file mode 100644 index 000000000..3e89765f7 --- /dev/null +++ b/src/resources/extensions/sf/uok/auto-uok-ctx.js @@ -0,0 +1,78 @@ +/** + * uok/auto-uok-ctx.js — autonomous-loop helpers for building the + * schema-v2 UOK run-context. + * + * Lives in uok/ so it sits next to run-context.js (its only collaborator) + * and so the phase modules (auto/phases-*) can import it without setting + * up a circular dependency between each other. + * + * Slice 3b of "Make UOK the SF Control Plane". + */ + +import { buildUokRunContext } from "./run-context.js"; + +/** + * Derive the schema-v2 UOK permissionProfile from session/prefs. + * + * - "low" when YOLO mode is active or prefs.permissionLevel is "low" + * or "minimal" (sandboxed / read-only sessions). + * - "medium" when prefs.permissionLevel is "medium". + * - "high" otherwise (production / write-allowed — the default). + * + * Best-effort: if the session probe throws (test mocks, partial state), + * we fall back to the prefs path. The function never throws — gate + * emission must not break dispatch on a permission lookup. + */ +export function derivePermissionProfile(s, prefs) { + try { + if (typeof s?.isYolo === "function" && s.isYolo()) return "low"; + } catch { + // session probe must never break dispatch + } + const level = + typeof prefs?.permissionLevel === "string" + ? prefs.permissionLevel.toLowerCase() + : undefined; + if (level === "minimal" || level === "low") return "low"; + if (level === "medium") return "medium"; + return "high"; +} + +/** + * Build the schema-v2 UOK run-context for an autonomous-loop phase. + * + * Wraps buildUokRunContext with the autonomous-surface defaults so each + * phase module doesn't need to hardcode the same surface/runControl + * strings. Callers pass the phase-specific traceId (e.g. + * `pre-dispatch:`, `guard:`, `finalize:`) and + * any unit-level keys they already have. + * + * Returns null on invalid input (same contract as buildUokRunContext). + * Callers should treat null as "no v2 ctx, fall through to legacy". + * + * @param {object} opts + * @param {object} opts.s Autonomous session state. + * @param {object} [opts.prefs] Operator preferences. + * @param {string} opts.traceId Phase-specific trace id. + * @param {string} [opts.parentTrace] + * @param {string} [opts.unitType] + * @param {string} [opts.unitId] + * @param {string} [opts.milestoneId] + * @param {string} [opts.sliceId] + * @param {string} [opts.taskId] + */ +export function buildAutonomousUokContext(opts) { + if (!opts || typeof opts !== "object") return null; + return buildUokRunContext({ + surface: "autonomous", + runControl: "autonomous", + permissionProfile: derivePermissionProfile(opts.s, opts.prefs), + traceId: opts.traceId, + parentTrace: opts.parentTrace, + unitType: opts.unitType, + unitId: opts.unitId, + milestoneId: opts.milestoneId, + sliceId: opts.sliceId, + taskId: opts.taskId, + }); +}