diff --git a/src/resources/extensions/sf/tools/plan-slice.js b/src/resources/extensions/sf/tools/plan-slice.js index 0ea50ea2d..ddbd4dae6 100644 --- a/src/resources/extensions/sf/tools/plan-slice.js +++ b/src/resources/extensions/sf/tools/plan-slice.js @@ -237,7 +237,7 @@ function validateParams(params) { tasks: validateTasks(params.tasks), }; } -export async function handlePlanSlice(rawParams, basePath) { +export async function handlePlanSlice(rawParams, basePath, options = {}) { let params; try { params = validateParams(rawParams); @@ -250,6 +250,16 @@ export async function handlePlanSlice(rawParams, basePath) { : `validation failed: ${message}`, }; } + // Schema-v2 UOK run-context for the Q3/Q4/Q5/Q6/Q7/Q8 seed rows. When + // the caller supplied a context (autonomous-loop dispatch chains it + // through executePlanSlice), the gate rows land with surface/run_control/ + // permission_profile/trace_id populated and status-uok classifies them + // as "ok". When absent (direct/legacy callers), the rows stay NULL and + // classify as "legacy" — same shape as pre-v2. + const uokContext = + options && typeof options.uokContext === "object" + ? options.uokContext + : null; // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ─── // Guards must be inside the transaction so the state they check cannot // change between the read and the write (#2723). @@ -329,6 +339,7 @@ export async function handlePlanSlice(rawParams, basePath) { sliceId: params.sliceId, gateId: gid, scope: "slice", + uokContext, }); } const taskGates = ["Q5", "Q6", "Q7"]; @@ -340,6 +351,7 @@ export async function handlePlanSlice(rawParams, basePath) { gateId: gid, scope: "task", taskId: task.taskId, + uokContext, }); } } @@ -348,6 +360,7 @@ export async function handlePlanSlice(rawParams, basePath) { sliceId: params.sliceId, gateId: "Q8", scope: "slice", + uokContext, }); insertSliceEvidence( params.milestoneId, diff --git a/src/resources/extensions/sf/tools/workflow-tool-executors.js b/src/resources/extensions/sf/tools/workflow-tool-executors.js index 182b9bcf9..bd167f273 100644 --- a/src/resources/extensions/sf/tools/workflow-tool-executors.js +++ b/src/resources/extensions/sf/tools/workflow-tool-executors.js @@ -19,6 +19,7 @@ import { invalidateStateCache } from "../state.js"; import { extractVerdict } from "../verdict-parser.js"; import { logError, logWarning } from "../workflow-logger.js"; import { handleCompleteMilestone } from "./complete-milestone.js"; +import { buildAutonomousUokContext } from "../uok/auto-uok-ctx.js"; import { handleCompleteSlice } from "./complete-slice.js"; import { handleCompleteTask } from "./complete-task.js"; import { handlePlanMilestone } from "./plan-milestone.js"; @@ -722,6 +723,39 @@ export async function executePlanMilestone(params, basePath = process.cwd()) { }; } } +/** + * Build a schema-v2 UOK run-context for plan_slice from the singleton + * autonomous session, when one is active. + * + * Lazy-imports auto/session.js so headless/test code paths that never + * touch the autonomous loop don't pay the load cost. Returns null when + * no session is active or when buildAutonomousUokContext rejects the + * inputs (e.g. missing traceId); callers treat null as "fall through to + * the legacy NULL-column write". + */ +async function derivePlanSliceUokContext() { + let s; + try { + const sessionModule = await import("../auto/session.js"); + s = sessionModule.getAutoSession?.(); + } catch { + return null; + } + if (!s || typeof s !== "object") return null; + const traceId = s.currentTraceId; + if (typeof traceId !== "string" || traceId.length === 0) return null; + // AutoSession exposes isYolo(); prefs are loaded at the loop level and + // not stored on the session singleton, so we don't have them here. The + // derivePermissionProfile helper degrades gracefully to "high" when + // prefs is undefined and YOLO is off, which matches the autonomous + // loop's default. + return buildAutonomousUokContext({ + s, + traceId, + parentTrace: traceId, + }); +} + export async function executePlanSlice(params, basePath = process.cwd()) { const dbAvailable = await ensureDbOpen(basePath); if (!dbAvailable) { @@ -737,7 +771,14 @@ export async function executePlanSlice(params, basePath = process.cwd()) { }; } try { - const result = await handlePlanSlice(params, basePath); + // Derive the schema-v2 UOK run-context from the autonomous session + // when one is active. plan_slice runs as a tool inside an + // autonomous unit, so the session's currentTraceId is the parent + // flow id. When the tool is invoked outside an autonomous loop + // (interactive REPL, headless one-shot), uokContext stays null and + // the seeded gate rows fall through to the legacy null-column shape. + const uokContext = await derivePlanSliceUokContext(); + const result = await handlePlanSlice(params, basePath, { uokContext }); if ("error" in result) { return { content: [