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) <noreply@anthropic.com>
This commit is contained in:
parent
7003da3f6a
commit
a2c55d5fde
4 changed files with 141 additions and 0 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
78
src/resources/extensions/sf/uok/auto-uok-ctx.js
Normal file
78
src/resources/extensions/sf/uok/auto-uok-ctx.js
Normal file
|
|
@ -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:<flowId>`, `guard:<flowId>`, `finalize:<flowId>`) 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,
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue