Integrate UOK model policy gates and kernel loop adapter

This commit is contained in:
Jeremy McSpadden 2026-04-14 18:45:05 -05:00
parent 856c3f5cf5
commit bb1b9dce07
36 changed files with 2026 additions and 43 deletions

View file

@ -16,6 +16,8 @@ import { getLedger, getProjectTotals } from "./metrics.js";
import { unitPhaseLabel } from "./auto-dashboard.js";
import { getSessionModelOverride } from "./session-model-override.js";
import { logWarning } from "./workflow-logger.js";
import { resolveUokFlags } from "./uok/flags.js";
import { applyModelPolicyFilter } from "./uok/model-policy.js";
export interface ModelSelectionResult {
/** Routing metadata for metrics recording */
@ -75,6 +77,7 @@ export async function selectAndApplyModel(
/** Explicit /gsd model pin captured at bootstrap for long-running auto loops. */
sessionModelOverride?: { provider: string; id: string } | null,
): Promise<ModelSelectionResult> {
const uokFlags = resolveUokFlags(prefs);
const effectiveSessionModelOverride = sessionModelOverride === undefined
? getSessionModelOverride(ctx.sessionManager.getSessionId())
: (sessionModelOverride ?? undefined);
@ -97,6 +100,9 @@ export async function selectAndApplyModel(
if (modelConfig) {
const availableModels = ctx.modelRegistry.getAvailable();
const modelPolicyTraceId = `model:${ctx.sessionManager.getSessionId()}:${Date.now()}`;
const modelPolicyTurnId = `${unitType}:${unitId}`;
let policyAllowedModelKeys: Set<string> | null = null;
// ─── Dynamic Model Routing ─────────────────────────────────────────
// Dynamic routing (complexity-based downgrading) only applies in auto-mode.
@ -106,8 +112,35 @@ export async function selectAndApplyModel(
if (!isAutoMode) {
routingConfig.enabled = false;
}
// burn-max defaults to quality-first dispatch (no downgrade routing).
if (prefs?.token_profile === "burn-max") {
routingConfig.enabled = false;
}
let effectiveModelConfig = modelConfig;
let routingTierLabel = "";
let routingEligibleModels = availableModels;
if (uokFlags.modelPolicy) {
const policy = applyModelPolicyFilter(
availableModels,
{
basePath,
traceId: modelPolicyTraceId,
turnId: modelPolicyTurnId,
unitType,
currentProvider: ctx.model?.provider,
allowCrossProvider: routingConfig.cross_provider !== false,
requiredTools: pi.getActiveTools(),
},
);
routingEligibleModels = policy.eligible;
policyAllowedModelKeys = new Set(
policy.eligible.map((m) => `${m.provider.toLowerCase()}/${m.id.toLowerCase()}`),
);
if (routingEligibleModels.length === 0) {
throw new Error(`Model policy denied all candidate models for ${unitType}/${unitId}`);
}
}
// Disable routing for flat-rate providers like GitHub Copilot (#3453).
// All models cost the same per request, so downgrading to a cheaper
@ -115,7 +148,7 @@ export async function selectAndApplyModel(
// Fail-closed: if primary model can't be resolved, fall back to
// provider-level signals rather than allowing unwanted downgrades.
if (routingConfig.enabled) {
const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
const primaryModel = resolveModelId(modelConfig.primary, routingEligibleModels, ctx.model?.provider);
if (primaryModel) {
const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
@ -150,7 +183,7 @@ export async function selectAndApplyModel(
if (shouldClassify) {
let classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
const availableModelIds = availableModels.map(m => m.id);
const availableModelIds = routingEligibleModels.map(m => m.id);
// Escalate tier on retry when escalate_on_failure is enabled (default: true)
if (
@ -257,6 +290,7 @@ export async function selectAndApplyModel(
}
const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
let attemptedPolicyEligible = false;
for (const modelId of modelsToTry) {
const model = resolveModelId(modelId, availableModels, ctx.model?.provider);
@ -266,6 +300,17 @@ export async function selectAndApplyModel(
continue;
}
if (policyAllowedModelKeys) {
const key = `${model.provider.toLowerCase()}/${model.id.toLowerCase()}`;
if (!policyAllowedModelKeys.has(key)) {
if (verbose) {
ctx.ui.notify(`Model policy denied ${model.provider}/${model.id}; trying fallback.`, "warning");
}
continue;
}
attemptedPolicyEligible = true;
}
// Warn if the ID is ambiguous across providers
if (!modelId.includes("/")) {
const providers = availableModels.filter(m => m.id === modelId).map(m => m.provider);
@ -331,6 +376,10 @@ export async function selectAndApplyModel(
}
}
}
if (uokFlags.modelPolicy && policyAllowedModelKeys && !attemptedPolicyEligible) {
throw new Error(`Model policy denied dispatch for ${unitType}/${unitId} before prompt send`);
}
} else if (autoModeStartModel) {
// No model preference for this unit type — re-apply the model captured
// at auto-mode start to prevent bleed from shared global settings.json (#650).

View file

@ -33,6 +33,8 @@ import { runPostExecutionChecks, type PostExecutionResult } from "./post-executi
import type { AutoSession } from "./auto/session.js";
import type { VerificationResult as VerificationGateResult } from "./types.js";
import { join } from "node:path";
import { resolveUokFlags } from "./uok/flags.js";
import { UokGateRunner } from "./uok/gate-runner.js";
export interface VerificationContext {
s: AutoSession;
@ -158,6 +160,7 @@ export async function runPostUnitVerification(
try {
const effectivePrefs = loadEffectiveGSDPreferences();
const prefs = effectivePrefs?.preferences;
const uokFlags = resolveUokFlags(prefs);
// Read task plan verify field
const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
@ -196,6 +199,37 @@ export async function runPostUnitVerification(
}
}
if (uokFlags.gates) {
const gateRunner = new UokGateRunner();
gateRunner.register({
id: "verification-gate",
type: "verification",
execute: async () => ({
outcome: result.passed ? "pass" : "fail",
failureClass: result.runtimeErrors?.some((e) => e.blocking)
? "execution"
: "verification",
rationale: result.passed
? "verification checks passed"
: "verification checks failed",
findings: result.passed
? ""
: formatFailureContext(result),
}),
});
await gateRunner.run("verification-gate", {
basePath: s.basePath,
traceId: `verification:${s.currentUnit.id}`,
turnId: s.currentUnit.id,
milestoneId: mid ?? undefined,
sliceId: sid ?? undefined,
taskId: tid ?? undefined,
unitType: s.currentUnit.type,
unitId: s.currentUnit.id,
});
}
// Auto-fix retry preferences
const autoFixEnabled = prefs?.verification_auto_fix !== false;
const maxRetries =

View file

@ -202,6 +202,7 @@ import {
import { bootstrapAutoSession, openProjectDbIfPresent, type BootstrapDeps } from "./auto-start.js";
import { initHealthWidget } from "./health-widget.js";
import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight, type LoopDeps, type ErrorContext } from "./auto-loop.js";
import { runAutoLoopWithUok } from "./uok/kernel.js";
// Slice-level parallelism (#2340)
import { getEligibleSlices } from "./slice-parallel-eligibility.js";
import { startSliceParallel } from "./slice-parallel-orchestrator.js";
@ -1513,7 +1514,13 @@ export async function startAuto(
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
captureProjectRootEnv(s.originalBasePath || s.basePath);
await autoLoop(ctx, pi, s, buildLoopDeps());
await runAutoLoopWithUok({
ctx,
pi,
s,
deps: buildLoopDeps(),
runLegacyLoop: autoLoop,
});
cleanupAfterLoopExit(ctx);
return;
}
@ -1548,7 +1555,13 @@ export async function startAuto(
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
// Dispatch the first unit
await autoLoop(ctx, pi, s, buildLoopDeps());
await runAutoLoopWithUok({
ctx,
pi,
s,
deps: buildLoopDeps(),
runLegacyLoop: autoLoop,
});
cleanupAfterLoopExit(ctx);
}

View file

@ -21,6 +21,7 @@ import type { WorktreeResolver } from "../worktree-resolver.js";
import type { CmuxLogLevel } from "../../cmux/index.js";
import type { JournalEntry } from "../journal.js";
import type { MergeReconcileResult } from "../auto-recovery.js";
import type { UokTurnObserver } from "../uok/contracts.js";
/**
* Dependencies injected by the caller (auto.ts startAuto) so autoLoop
@ -274,4 +275,7 @@ export interface LoopDeps {
// Journal
emitJournalEvent: (entry: JournalEntry) => void;
// UOK (optional, flag-gated)
uokObserver?: UokTurnObserver;
}

View file

@ -128,6 +128,39 @@ export async function autoLoop(
const flowId = randomUUID();
let seqCounter = 0;
const nextSeq = () => ++seqCounter;
const turnId = randomUUID();
const turnStartedAt = new Date().toISOString();
let observedUnitType: string | undefined;
let observedUnitId: string | undefined;
let turnFinished = false;
const finishTurn = (
status: "completed" | "failed" | "paused" | "stopped" | "skipped" | "retry",
failureClass: "none" | "unknown" | "manual-attention" | "timeout" | "execution" = "none",
error?: string,
): void => {
if (turnFinished) return;
turnFinished = true;
deps.uokObserver?.onTurnResult({
traceId: flowId,
turnId,
iteration,
unitType: observedUnitType,
unitId: observedUnitId,
status,
failureClass,
phaseResults: [],
error,
startedAt: turnStartedAt,
finishedAt: new Date().toISOString(),
});
};
deps.uokObserver?.onTurnStart({
traceId: flowId,
turnId,
iteration,
basePath: s.basePath,
startedAt: turnStartedAt,
});
if (iteration > MAX_LOOP_ITERATIONS) {
debugLog("autoLoop", {
@ -140,6 +173,7 @@ export async function autoLoop(
pi,
`Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`,
);
finishTurn("stopped", "manual-attention", "max-iterations");
break;
}
@ -157,12 +191,14 @@ export async function autoLoop(
`Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
`Resume with /gsd auto to continue from where you left off.`,
);
finishTurn("stopped", "timeout", "memory-pressure");
break;
}
}
if (!s.cmdCtx) {
debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
finishTurn("stopped", "manual-attention", "missing-command-context");
break;
}
@ -256,27 +292,53 @@ export async function autoLoop(
isRetry: false,
previousTier: undefined,
};
observedUnitType = iterData.unitType;
observedUnitId = iterData.unitId;
// ── Progress widget (mirrors dev path in runDispatch) ──
deps.updateProgressWidget(ctx, iterData.unitType, iterData.unitId, iterData.state);
// ── Guards (shared with dev path) ──
const guardsResult = await runGuards(ic, s.currentMilestoneId ?? "workflow");
if (guardsResult.action === "break") break;
deps.uokObserver?.onPhaseResult("guard", guardsResult.action, {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
if (guardsResult.action === "break") {
finishTurn("stopped", "manual-attention", "guard-break");
break;
}
// ── Unit execution (shared with dev path) ──
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState);
if (unitPhaseResult.action === "break") break;
deps.uokObserver?.onPhaseResult("unit", unitPhaseResult.action, {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
if (unitPhaseResult.action === "break") {
finishTurn("stopped", "execution", "unit-break");
break;
}
// ── Verify first, then reconcile (only mark complete on pass) ──
debugLog("autoLoop", { phase: "custom-engine-verify", iteration, unitId: iterData.unitId });
const verifyResult = await policy.verify(iterData.unitType, iterData.unitId, { basePath: s.basePath });
if (verifyResult === "pause") {
await deps.pauseAuto(ctx, pi);
deps.uokObserver?.onPhaseResult("custom-engine", "pause", {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("paused", "manual-attention", "custom-engine-verify-pause");
break;
}
if (verifyResult === "retry") {
debugLog("autoLoop", { phase: "custom-engine-verify-retry", iteration, unitId: iterData.unitId });
deps.uokObserver?.onPhaseResult("custom-engine", "retry", {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("retry");
continue;
}
@ -299,36 +361,77 @@ export async function autoLoop(
if (reconcileResult.outcome === "milestone-complete") {
await deps.stopAuto(ctx, pi, "Workflow complete");
deps.uokObserver?.onPhaseResult("custom-engine", "milestone-complete", {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("completed");
break;
}
if (reconcileResult.outcome === "pause") {
await deps.pauseAuto(ctx, pi);
deps.uokObserver?.onPhaseResult("custom-engine", "pause", {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("paused", "manual-attention");
break;
}
if (reconcileResult.outcome === "stop") {
await deps.stopAuto(ctx, pi, reconcileResult.reason ?? "Engine stopped");
deps.uokObserver?.onPhaseResult("custom-engine", "stop", {
unitType: iterData.unitType,
unitId: iterData.unitId,
reason: reconcileResult.reason,
});
finishTurn("stopped", "manual-attention", reconcileResult.reason);
break;
}
deps.uokObserver?.onPhaseResult("custom-engine", "continue", {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("completed");
continue;
}
if (!sidecarItem) {
// ── Phase 1: Pre-dispatch ─────────────────────────────────────────
const preDispatchResult = await runPreDispatch(ic, loopState);
if (preDispatchResult.action === "break") break;
if (preDispatchResult.action === "continue") continue;
deps.uokObserver?.onPhaseResult("pre-dispatch", preDispatchResult.action);
if (preDispatchResult.action === "break") {
finishTurn("stopped", "manual-attention", "pre-dispatch-break");
break;
}
if (preDispatchResult.action === "continue") {
finishTurn("skipped");
continue;
}
const preData = preDispatchResult.data;
// ── Phase 2: Guards ───────────────────────────────────────────────
const guardsResult = await runGuards(ic, preData.mid);
if (guardsResult.action === "break") break;
deps.uokObserver?.onPhaseResult("guard", guardsResult.action);
if (guardsResult.action === "break") {
finishTurn("stopped", "manual-attention", "guard-break");
break;
}
// ── Phase 3: Dispatch ─────────────────────────────────────────────
const dispatchResult = await runDispatch(ic, preData, loopState);
if (dispatchResult.action === "break") break;
if (dispatchResult.action === "continue") continue;
deps.uokObserver?.onPhaseResult("dispatch", dispatchResult.action);
if (dispatchResult.action === "break") {
finishTurn("stopped", "manual-attention", "dispatch-break");
break;
}
if (dispatchResult.action === "continue") {
finishTurn("skipped");
continue;
}
iterData = dispatchResult.data;
observedUnitType = iterData.unitType;
observedUnitId = iterData.unitId;
} else {
// ── Sidecar path: use values from the sidecar item directly ──
const sidecarState = await deps.deriveState(s.basePath);
@ -343,22 +446,47 @@ export async function autoLoop(
midTitle: sidecarState.activeMilestone?.title,
isRetry: false, previousTier: undefined,
};
observedUnitType = iterData.unitType;
observedUnitId = iterData.unitId;
deps.uokObserver?.onPhaseResult("dispatch", "sidecar", {
unitType: iterData.unitType,
unitId: iterData.unitId,
sidecarKind: sidecarItem.kind,
});
}
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState, sidecarItem);
if (unitPhaseResult.action === "break") break;
deps.uokObserver?.onPhaseResult("unit", unitPhaseResult.action, {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
if (unitPhaseResult.action === "break") {
finishTurn("stopped", "execution", "unit-break");
break;
}
// ── Phase 5: Finalize ───────────────────────────────────────────────
const finalizeResult = await runFinalize(ic, iterData, loopState, sidecarItem);
if (finalizeResult.action === "break") break;
if (finalizeResult.action === "continue") continue;
deps.uokObserver?.onPhaseResult("finalize", finalizeResult.action, {
unitType: iterData.unitType,
unitId: iterData.unitId,
});
if (finalizeResult.action === "break") {
finishTurn("stopped", "closeout", "finalize-break");
break;
}
if (finalizeResult.action === "continue") {
finishTurn("retry");
continue;
}
consecutiveErrors = 0; // Iteration completed successfully
consecutiveCooldowns = 0;
recentErrorMessages.length = 0;
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
debugLog("autoLoop", { phase: "iteration-complete", iteration });
finishTurn("completed");
} catch (loopErr) {
// ── Blanket catch: absorb unexpected exceptions, apply graduated recovery ──
const msg = loopErr instanceof Error ? loopErr.message : String(loopErr);
@ -388,6 +516,7 @@ export async function autoLoop(
pi,
`Infrastructure error (${infraCode}): not recoverable by retry`,
);
finishTurn("failed", "execution", msg);
break;
}
@ -429,6 +558,7 @@ export async function autoLoop(
"warning",
);
await new Promise(resolve => setTimeout(resolve, waitMs));
finishTurn("retry", "timeout", msg);
continue; // Retry iteration without incrementing consecutiveErrors
}
@ -455,6 +585,7 @@ export async function autoLoop(
pi,
`${consecutiveErrors} consecutive iteration failures`,
);
finishTurn("failed", "execution", msg);
break;
} else if (consecutiveErrors === 2) {
// 2nd consecutive: try invalidating caches + re-deriving state
@ -467,6 +598,7 @@ export async function autoLoop(
// 1st error: log and retry — transient failures happen
ctx.ui.notify(`Iteration error: ${msg}. Retrying.`, "warning");
}
finishTurn("retry", "execution", msg);
}
}

View file

@ -47,6 +47,7 @@ import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
import { ensurePlanV2Graph } from "../uok/plan-v2.js";
import { resetEvidence } from "../safety/evidence-collector.js";
import { createCheckpoint, cleanupCheckpoint, rollbackToCheckpoint } from "../safety/git-checkpoint.js";
import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
@ -252,6 +253,15 @@ export async function runPreDispatch(
// Derive state
let state = await deps.deriveState(s.basePath);
if (prefs?.uok?.plan_v2?.enabled) {
const compiled = ensurePlanV2Graph(s.basePath, state);
if (!compiled.ok) {
const reason = compiled.reason ?? "Plan v2 compilation failed";
ctx.ui.notify(`Plan gate failed-closed: ${reason}`, "error");
await deps.pauseAuto(ctx, pi);
return { action: "break", reason: "plan-v2-gate-failed" };
}
}
deps.syncCmuxSidebar(prefs, state);
let mid = state.activeMilestone?.id;
let midTitle = state.activeMilestone?.title;

View file

@ -822,7 +822,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
"notifications", "cmux", "remote_questions", "git",
"post_unit_hooks", "pre_dispatch_hooks",
"dynamic_routing", "token_profile", "phases", "parallel",
"dynamic_routing", "uok", "token_profile", "phases", "parallel",
"auto_visualize", "auto_report",
"verification_commands", "verification_auto_fix", "verification_max_retries",
"search_provider", "context_selection",

View file

@ -153,7 +153,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
- `context_pause_threshold`: number (0-100) — context window usage percentage at which auto-mode should pause to suggest checkpointing. Set to `0` to disable. Default: `0` (disabled).
- `token_profile`: `"budget"`, `"balanced"`, or `"quality"` — coordinates model selection, phase skipping, and context compression. `budget` skips research/reassessment and uses cheaper models; `balanced` (default) skips research/reassessment to reduce token burn; `quality` prefers higher-quality models. See token-optimization docs.
- `token_profile`: `"budget"`, `"balanced"`, `"quality"`, or `"burn-max"` — coordinates model selection, phase skipping, and context compression. `budget` skips research/reassessment and uses cheaper models; `balanced` (default) skips research/reassessment to reduce token burn; `quality` prefers higher-quality models; `burn-max` keeps full-context defaults, disables downgrade routing, and keeps phase skips off.
- `phases`: fine-grained control over which phases run. Usually set by `token_profile`, but can be overridden. Keys:
- `skip_research`: boolean — skip milestone-level research. Default: `false`.
@ -191,6 +191,17 @@ 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.
- `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.
- `gitops.enabled`: boolean — persist turn-level git transaction records.
- `gitops.turn_action`: `"commit"` | `"snapshot"` | `"status-only"` — turn transaction mode.
- `gitops.turn_push`: boolean — whether turn transactions should include push intent metadata.
- `audit_unified.enabled`: boolean — dual-write unified audit envelope events.
- `plan_v2.enabled`: boolean — enable bounded clarify/research/draft/compile planning flow.
- `context_management`: configures context hygiene for auto-mode sessions. Keys:
- `observation_masking`: boolean — mask old tool results to reduce context bloat. Default: `true`.
- `observation_mask_turns`: number — keep this many recent turns verbatim (1-50). Default: `8`.

View file

@ -180,7 +180,7 @@ function openRawDb(path: string): unknown {
return new Database(path);
}
const SCHEMA_VERSION = 14;
const SCHEMA_VERSION = 15;
function indexExists(db: DbAdapter, name: string): boolean {
return !!db.prepare(
@ -443,6 +443,70 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS gate_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
gate_id TEXT NOT NULL,
gate_type TEXT NOT NULL DEFAULT '',
unit_type TEXT DEFAULT NULL,
unit_id TEXT DEFAULT NULL,
milestone_id TEXT DEFAULT NULL,
slice_id TEXT DEFAULT NULL,
task_id TEXT DEFAULT NULL,
outcome TEXT NOT NULL DEFAULT 'pass',
failure_class TEXT NOT NULL DEFAULT 'none',
rationale TEXT NOT NULL DEFAULT '',
findings TEXT NOT NULL DEFAULT '',
attempt INTEGER NOT NULL DEFAULT 1,
max_attempts INTEGER NOT NULL DEFAULT 1,
retryable INTEGER NOT NULL DEFAULT 0,
evaluated_at TEXT NOT NULL DEFAULT ''
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS turn_git_transactions (
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
unit_type TEXT DEFAULT NULL,
unit_id TEXT DEFAULT NULL,
stage TEXT NOT NULL DEFAULT 'turn-start',
action TEXT NOT NULL DEFAULT 'status-only',
push INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'ok',
error TEXT DEFAULT NULL,
metadata_json TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL DEFAULT '',
PRIMARY KEY (trace_id, turn_id, stage)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_events (
event_id TEXT PRIMARY KEY,
trace_id TEXT NOT NULL,
turn_id TEXT DEFAULT NULL,
caused_by TEXT DEFAULT NULL,
category TEXT NOT NULL,
type TEXT NOT NULL,
ts TEXT NOT NULL,
payload_json TEXT NOT NULL DEFAULT '{}'
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_turn_index (
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
first_ts TEXT NOT NULL,
last_ts TEXT NOT NULL,
event_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (trace_id, turn_id)
)
`);
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)");
db.exec("CREATE INDEX IF NOT EXISTS idx_replan_history_milestone ON replan_history(milestone_id, created_at)");
@ -456,6 +520,11 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
// v14 index — slice dependency lookups
db.exec("CREATE INDEX IF NOT EXISTS idx_slice_deps_target ON slice_dependencies(milestone_id, depends_on_slice_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_gate_runs_turn ON gate_runs(trace_id, turn_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_gate_runs_lookup ON gate_runs(milestone_id, slice_id, task_id, gate_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_turn_git_tx_turn ON turn_git_transactions(trace_id, turn_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_trace ON audit_events(trace_id, ts)");
db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_turn ON audit_events(trace_id, turn_id, ts)");
db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`);
db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`);
@ -810,6 +879,78 @@ function migrateSchema(db: DbAdapter): void {
});
}
if (currentVersion < 15) {
db.exec(`
CREATE TABLE IF NOT EXISTS gate_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
gate_id TEXT NOT NULL,
gate_type TEXT NOT NULL DEFAULT '',
unit_type TEXT DEFAULT NULL,
unit_id TEXT DEFAULT NULL,
milestone_id TEXT DEFAULT NULL,
slice_id TEXT DEFAULT NULL,
task_id TEXT DEFAULT NULL,
outcome TEXT NOT NULL DEFAULT 'pass',
failure_class TEXT NOT NULL DEFAULT 'none',
rationale TEXT NOT NULL DEFAULT '',
findings TEXT NOT NULL DEFAULT '',
attempt INTEGER NOT NULL DEFAULT 1,
max_attempts INTEGER NOT NULL DEFAULT 1,
retryable INTEGER NOT NULL DEFAULT 0,
evaluated_at TEXT NOT NULL DEFAULT ''
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS turn_git_transactions (
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
unit_type TEXT DEFAULT NULL,
unit_id TEXT DEFAULT NULL,
stage TEXT NOT NULL DEFAULT 'turn-start',
action TEXT NOT NULL DEFAULT 'status-only',
push INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'ok',
error TEXT DEFAULT NULL,
metadata_json TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL DEFAULT '',
PRIMARY KEY (trace_id, turn_id, stage)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_events (
event_id TEXT PRIMARY KEY,
trace_id TEXT NOT NULL,
turn_id TEXT DEFAULT NULL,
caused_by TEXT DEFAULT NULL,
category TEXT NOT NULL,
type TEXT NOT NULL,
ts TEXT NOT NULL,
payload_json TEXT NOT NULL DEFAULT '{}'
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_turn_index (
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
first_ts TEXT NOT NULL,
last_ts TEXT NOT NULL,
event_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (trace_id, turn_id)
)
`);
db.exec("CREATE INDEX IF NOT EXISTS idx_gate_runs_turn ON gate_runs(trace_id, turn_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_gate_runs_lookup ON gate_runs(milestone_id, slice_id, task_id, gate_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_turn_git_tx_turn ON turn_git_transactions(trace_id, turn_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_trace ON audit_events(trace_id, ts)");
db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_turn ON audit_events(trace_id, turn_id, ts)");
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
":version": 15,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
@ -2287,6 +2428,9 @@ export function deleteMilestone(milestoneId: string): void {
currentDb!.prepare(
`DELETE FROM quality_gates WHERE milestone_id = :mid`,
).run({ ":mid": milestoneId });
currentDb!.prepare(
`DELETE FROM gate_runs WHERE milestone_id = :mid`,
).run({ ":mid": milestoneId });
currentDb!.prepare(
`DELETE FROM tasks WHERE milestone_id = :mid`,
).run({ ":mid": milestoneId });
@ -2420,6 +2564,30 @@ export function saveGateResult(g: {
":findings": g.findings,
":evaluated_at": new Date().toISOString(),
});
const outcome =
g.verdict === "pass"
? "pass"
: g.verdict === "omitted"
? "manual-attention"
: "fail";
insertGateRun({
traceId: `quality-gate:${g.milestoneId}:${g.sliceId}`,
turnId: `gate:${g.gateId}:${g.taskId ?? "slice"}`,
gateId: g.gateId,
gateType: "quality-gate",
milestoneId: g.milestoneId,
sliceId: g.sliceId,
taskId: g.taskId ?? undefined,
outcome,
failureClass: outcome === "fail" ? "verification" : outcome === "manual-attention" ? "manual-attention" : "none",
rationale: g.rationale,
findings: g.findings,
attempt: 1,
maxAttempts: 1,
retryable: false,
evaluatedAt: new Date().toISOString(),
});
}
export function getPendingGates(milestoneId: string, sliceId: string, scope?: GateScope): GateRow[] {
@ -2513,6 +2681,156 @@ export function getPendingGateCountForTurn(
return getPendingGatesForTurn(milestoneId, sliceId, turn).length;
}
export function insertGateRun(entry: {
traceId: string;
turnId: string;
gateId: string;
gateType: string;
unitType?: string;
unitId?: string;
milestoneId?: string;
sliceId?: string;
taskId?: string;
outcome: "pass" | "fail" | "retry" | "manual-attention";
failureClass: "none" | "policy" | "input" | "execution" | "artifact" | "verification" | "closeout" | "git" | "timeout" | "manual-attention" | "unknown";
rationale?: string;
findings?: string;
attempt: number;
maxAttempts: number;
retryable: boolean;
evaluatedAt: string;
}): void {
if (!currentDb) return;
currentDb.prepare(
`INSERT INTO gate_runs (
trace_id, turn_id, gate_id, gate_type, unit_type, unit_id, milestone_id, slice_id, task_id,
outcome, failure_class, rationale, findings, attempt, max_attempts, retryable, evaluated_at
) VALUES (
:trace_id, :turn_id, :gate_id, :gate_type, :unit_type, :unit_id, :milestone_id, :slice_id, :task_id,
:outcome, :failure_class, :rationale, :findings, :attempt, :max_attempts, :retryable, :evaluated_at
)`,
).run({
":trace_id": entry.traceId,
":turn_id": entry.turnId,
":gate_id": entry.gateId,
":gate_type": entry.gateType,
":unit_type": entry.unitType ?? null,
":unit_id": entry.unitId ?? null,
":milestone_id": entry.milestoneId ?? null,
":slice_id": entry.sliceId ?? null,
":task_id": entry.taskId ?? null,
":outcome": entry.outcome,
":failure_class": entry.failureClass,
":rationale": entry.rationale ?? "",
":findings": entry.findings ?? "",
":attempt": entry.attempt,
":max_attempts": entry.maxAttempts,
":retryable": entry.retryable ? 1 : 0,
":evaluated_at": entry.evaluatedAt,
});
}
export function upsertTurnGitTransaction(entry: {
traceId: string;
turnId: string;
unitType?: string;
unitId?: string;
stage: string;
action: "commit" | "snapshot" | "status-only";
push: boolean;
status: "ok" | "failed";
error?: string;
metadata?: Record<string, unknown>;
updatedAt: string;
}): void {
if (!currentDb) return;
currentDb.prepare(
`INSERT OR REPLACE INTO turn_git_transactions (
trace_id, turn_id, unit_type, unit_id, stage, action, push, status, error, metadata_json, updated_at
) VALUES (
:trace_id, :turn_id, :unit_type, :unit_id, :stage, :action, :push, :status, :error, :metadata_json, :updated_at
)`,
).run({
":trace_id": entry.traceId,
":turn_id": entry.turnId,
":unit_type": entry.unitType ?? null,
":unit_id": entry.unitId ?? null,
":stage": entry.stage,
":action": entry.action,
":push": entry.push ? 1 : 0,
":status": entry.status,
":error": entry.error ?? null,
":metadata_json": JSON.stringify(entry.metadata ?? {}),
":updated_at": entry.updatedAt,
});
}
export function insertAuditEvent(entry: {
eventId: string;
traceId: string;
turnId?: string;
causedBy?: string;
category: string;
type: string;
ts: string;
payload: Record<string, unknown>;
}): void {
if (!currentDb) return;
transaction(() => {
currentDb!.prepare(
`INSERT OR IGNORE INTO audit_events (
event_id, trace_id, turn_id, caused_by, category, type, ts, payload_json
) VALUES (
:event_id, :trace_id, :turn_id, :caused_by, :category, :type, :ts, :payload_json
)`,
).run({
":event_id": entry.eventId,
":trace_id": entry.traceId,
":turn_id": entry.turnId ?? null,
":caused_by": entry.causedBy ?? null,
":category": entry.category,
":type": entry.type,
":ts": entry.ts,
":payload_json": JSON.stringify(entry.payload ?? {}),
});
if (entry.turnId) {
const row = currentDb!.prepare(
`SELECT event_count, first_ts, last_ts
FROM audit_turn_index
WHERE trace_id = :trace_id AND turn_id = :turn_id`,
).get({
":trace_id": entry.traceId,
":turn_id": entry.turnId,
});
if (row) {
currentDb!.prepare(
`UPDATE audit_turn_index
SET first_ts = CASE WHEN :ts < first_ts THEN :ts ELSE first_ts END,
last_ts = CASE WHEN :ts > last_ts THEN :ts ELSE last_ts END,
event_count = event_count + 1
WHERE trace_id = :trace_id AND turn_id = :turn_id`,
).run({
":trace_id": entry.traceId,
":turn_id": entry.turnId,
":ts": entry.ts,
});
} else {
currentDb!.prepare(
`INSERT INTO audit_turn_index (trace_id, turn_id, first_ts, last_ts, event_count)
VALUES (:trace_id, :turn_id, :first_ts, :last_ts, :event_count)`,
).run({
":trace_id": entry.traceId,
":turn_id": entry.turnId,
":first_ts": entry.ts,
":last_ts": entry.ts,
":event_count": 1,
});
}
}
});
}
// ─── Single-writer bypass wrappers ───────────────────────────────────────
// These wrappers exist so modules outside this file never need to call
// `_getAdapter()` for writes. Each one is a byte-equivalent replacement for

View file

@ -33,7 +33,7 @@ interface ProjectPreferences {
mainBranch: string;
verificationCommands: string[];
customInstructions: string[];
tokenProfile: "budget" | "balanced" | "quality";
tokenProfile: "budget" | "balanced" | "quality" | "burn-max";
skipResearch: boolean;
autoPush: boolean;
}
@ -413,10 +413,11 @@ async function customizeAdvancedPrefs(
{ id: "balanced", label: "Balanced", description: "Good trade-off (default)", recommended: true },
{ id: "budget", label: "Budget", description: "Minimize token usage" },
{ id: "quality", label: "Quality", description: "Maximize thoroughness" },
{ id: "burn-max", label: "Burn Max", description: "Maximum depth, no phase skips" },
],
});
if (profileChoice !== "not_yet") {
prefs.tokenProfile = profileChoice as "budget" | "balanced" | "quality";
prefs.tokenProfile = profileChoice as "budget" | "balanced" | "quality" | "burn-max";
}
// Skip research

View file

@ -355,7 +355,7 @@ export function resolveAutoSupervisorConfig(): AutoSupervisorConfig {
// ─── Token Profile Resolution ─────────────────────────────────────────────
const VALID_TOKEN_PROFILES = new Set<TokenProfile>(["budget", "balanced", "quality"]);
const VALID_TOKEN_PROFILES = new Set<TokenProfile>(["budget", "balanced", "quality", "burn-max"]);
/**
* Resolve profile defaults for a given token profile tier.
@ -400,6 +400,22 @@ export function resolveProfileDefaults(profile: TokenProfile): Partial<GSDPrefer
skip_reassess: true,
},
};
case "burn-max":
return {
// Quality-first profile: keep user-selected models, disable downgrade routing.
// Policy constraints still apply at dispatch time.
dynamic_routing: {
enabled: false,
},
context_selection: "full",
phases: {
skip_research: false,
skip_slice_research: false,
skip_reassess: false,
skip_milestone_validation: false,
reassess_after_slice: true,
},
};
}
}
@ -416,7 +432,7 @@ export function resolveEffectiveProfile(): TokenProfile {
/**
* Resolve the inline level from the active token profile.
* budget -> minimal, balanced -> standard, quality -> full.
* budget -> minimal, balanced -> standard, quality/burn-max -> full.
*/
export function resolveInlineLevel(): InlineLevel {
const profile = resolveEffectiveProfile();
@ -424,12 +440,13 @@ export function resolveInlineLevel(): InlineLevel {
case "budget": return "minimal";
case "balanced": return "standard";
case "quality": return "full";
case "burn-max": return "full";
}
}
/**
* Resolve the context selection mode from the active token profile.
* budget -> "smart", balanced/quality -> "full".
* budget -> "smart", balanced/quality/burn-max -> "full".
* Explicit preference always wins.
*/
export function resolveContextSelection(): import("./types.js").ContextSelectionMode {

View file

@ -83,6 +83,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
"post_unit_hooks",
"pre_dispatch_hooks",
"dynamic_routing",
"uok",
"token_profile",
"phases",
"auto_visualize",
@ -208,6 +209,32 @@ export interface CmuxPreferences {
browser?: boolean;
}
export type UokTurnActionMode = "commit" | "snapshot" | "status-only";
export interface UokPreferences {
enabled?: boolean;
gates?: {
enabled?: boolean;
};
model_policy?: {
enabled?: boolean;
};
execution_graph?: {
enabled?: boolean;
};
gitops?: {
enabled?: boolean;
turn_action?: UokTurnActionMode;
turn_push?: boolean;
};
audit_unified?: {
enabled?: boolean;
};
plan_v2?: {
enabled?: boolean;
};
}
/**
* Opt-in experimental features. All features in this block are disabled by
* default and must be explicitly enabled. They may change or be removed without
@ -256,6 +283,8 @@ export interface GSDPreferences {
post_unit_hooks?: PostUnitHookConfig[];
pre_dispatch_hooks?: PreDispatchHookConfig[];
dynamic_routing?: DynamicRoutingConfig;
/** Unified Orchestration Kernel controls (all flags default off). */
uok?: UokPreferences;
/** Per-model capability overrides. Deep-merged with built-in profiles for capability-aware routing (ADR-004). */
modelOverrides?: Record<string, { capabilities?: Partial<ModelCapabilities> }>;
context_management?: ContextManagementConfig;

View file

@ -22,7 +22,12 @@ import {
type GSDSkillRule,
} from "./preferences-types.js";
const VALID_TOKEN_PROFILES = new Set<TokenProfile>(["budget", "balanced", "quality"]);
const VALID_TOKEN_PROFILES = new Set<TokenProfile>(["budget", "balanced", "quality", "burn-max"]);
const VALID_UOK_TURN_ACTIONS = new Set<"commit" | "snapshot" | "status-only">([
"commit",
"snapshot",
"status-only",
]);
export function validatePreferences(preferences: GSDPreferences): {
preferences: GSDPreferences;
@ -161,12 +166,110 @@ export function validatePreferences(preferences: GSDPreferences): {
}
}
// ─── UOK Flags ──────────────────────────────────────────────────────
if (preferences.uok !== undefined) {
if (typeof preferences.uok === "object" && preferences.uok !== null) {
const raw = preferences.uok as Record<string, unknown>;
const valid: NonNullable<GSDPreferences["uok"]> = {};
if (raw.enabled !== undefined) {
if (typeof raw.enabled === "boolean") valid.enabled = raw.enabled;
else errors.push("uok.enabled must be a boolean");
}
const parseEnabledBlock = (
key: "gates" | "model_policy" | "execution_graph" | "audit_unified" | "plan_v2",
): void => {
const value = raw[key];
if (value === undefined) return;
if (typeof value !== "object" || value === null) {
errors.push(`uok.${key} must be an object`);
return;
}
const block = value as Record<string, unknown>;
const parsed: { enabled?: boolean } = {};
if (block.enabled !== undefined) {
if (typeof block.enabled === "boolean") parsed.enabled = block.enabled;
else errors.push(`uok.${key}.enabled must be a boolean`);
}
const unknown = Object.keys(block).filter((k) => k !== "enabled");
for (const unk of unknown) {
warnings.push(`unknown uok.${key} key "${unk}" — ignored`);
}
if (Object.keys(parsed).length > 0) {
valid[key] = parsed;
}
};
parseEnabledBlock("gates");
parseEnabledBlock("model_policy");
parseEnabledBlock("execution_graph");
parseEnabledBlock("audit_unified");
parseEnabledBlock("plan_v2");
if (raw.gitops !== undefined) {
if (typeof raw.gitops !== "object" || raw.gitops === null) {
errors.push("uok.gitops must be an object");
} else {
const gitops = raw.gitops as Record<string, unknown>;
const parsed: NonNullable<NonNullable<GSDPreferences["uok"]>["gitops"]> = {};
if (gitops.enabled !== undefined) {
if (typeof gitops.enabled === "boolean") parsed.enabled = gitops.enabled;
else errors.push("uok.gitops.enabled must be a boolean");
}
if (gitops.turn_action !== undefined) {
if (
typeof gitops.turn_action === "string" &&
VALID_UOK_TURN_ACTIONS.has(gitops.turn_action as "commit" | "snapshot" | "status-only")
) {
parsed.turn_action = gitops.turn_action as "commit" | "snapshot" | "status-only";
} else {
errors.push("uok.gitops.turn_action must be one of: commit, snapshot, status-only");
}
}
if (gitops.turn_push !== undefined) {
if (typeof gitops.turn_push === "boolean") parsed.turn_push = gitops.turn_push;
else errors.push("uok.gitops.turn_push must be a boolean");
}
const unknown = Object.keys(gitops).filter((k) => !["enabled", "turn_action", "turn_push"].includes(k));
for (const unk of unknown) {
warnings.push(`unknown uok.gitops key "${unk}" — ignored`);
}
if (Object.keys(parsed).length > 0) {
valid.gitops = parsed;
}
}
}
const knownUokKeys = new Set([
"enabled",
"gates",
"model_policy",
"execution_graph",
"gitops",
"audit_unified",
"plan_v2",
]);
for (const key of Object.keys(raw)) {
if (!knownUokKeys.has(key)) {
warnings.push(`unknown uok key "${key}" — ignored`);
}
}
if (Object.keys(valid).length > 0) {
validated.uok = valid;
}
} else {
errors.push("uok must be an object");
}
}
// ─── Token Profile ─────────────────────────────────────────────────
if (preferences.token_profile !== undefined) {
if (typeof preferences.token_profile === "string" && VALID_TOKEN_PROFILES.has(preferences.token_profile as TokenProfile)) {
validated.token_profile = preferences.token_profile as TokenProfile;
} else {
errors.push(`token_profile must be one of: budget, balanced, quality`);
errors.push(`token_profile must be one of: budget, balanced, quality, burn-max`);
}
}

View file

@ -50,6 +50,8 @@ export type {
AutoSupervisorConfig,
RemoteQuestionsConfig,
CmuxPreferences,
UokTurnActionMode,
UokPreferences,
CodebaseMapPreferences,
GSDPreferences,
LoadedGSDPreferences,
@ -378,6 +380,29 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
dynamic_routing: (base.dynamic_routing || override.dynamic_routing)
? { ...(base.dynamic_routing ?? {}), ...(override.dynamic_routing ?? {}) } as DynamicRoutingConfig
: undefined,
uok: (base.uok || override.uok)
? {
enabled: override.uok?.enabled ?? base.uok?.enabled,
gates: (base.uok?.gates || override.uok?.gates)
? { ...(base.uok?.gates ?? {}), ...(override.uok?.gates ?? {}) }
: undefined,
model_policy: (base.uok?.model_policy || override.uok?.model_policy)
? { ...(base.uok?.model_policy ?? {}), ...(override.uok?.model_policy ?? {}) }
: undefined,
execution_graph: (base.uok?.execution_graph || override.uok?.execution_graph)
? { ...(base.uok?.execution_graph ?? {}), ...(override.uok?.execution_graph ?? {}) }
: undefined,
gitops: (base.uok?.gitops || override.uok?.gitops)
? { ...(base.uok?.gitops ?? {}), ...(override.uok?.gitops ?? {}) }
: undefined,
audit_unified: (base.uok?.audit_unified || override.uok?.audit_unified)
? { ...(base.uok?.audit_unified ?? {}), ...(override.uok?.audit_unified ?? {}) }
: undefined,
plan_v2: (base.uok?.plan_v2 || override.uok?.plan_v2)
? { ...(base.uok?.plan_v2 ?? {}), ...(override.uok?.plan_v2 ?? {}) }
: undefined,
}
: undefined,
token_profile: override.token_profile ?? base.token_profile,
phases: (base.phases || override.phases)
? { ...(base.phases ?? {}), ...(override.phases ?? {}) }

View file

@ -39,6 +39,22 @@ dynamic_routing:
budget_pressure:
cross_provider:
hooks:
uok:
enabled: false
gates:
enabled: false
model_policy:
enabled: false
execution_graph:
enabled: false
gitops:
enabled: false
turn_action: status-only
turn_push: false
audit_unified:
enabled: false
plan_v2:
enabled: false
auto_visualize:
auto_report:
parallel:

View file

@ -1267,7 +1267,7 @@ test("auto-loop.ts barrel re-exports autoLoop, runUnit, and resolveAgentEnd", ()
);
});
test("auto.ts startAuto calls autoLoop (not dispatchNextUnit as first dispatch)", () => {
test("auto.ts startAuto dispatches through the UOK kernel wrapper (legacy loop adapter)", () => {
const src = readFileSync(
resolve(import.meta.dirname, "..", "auto.ts"),
"utf-8",
@ -1279,8 +1279,12 @@ test("auto.ts startAuto calls autoLoop (not dispatchNextUnit as first dispatch)"
const fnBlock =
fnEnd > -1 ? src.slice(fnIdx, fnEnd) : src.slice(fnIdx, fnIdx + 5000);
assert.ok(
fnBlock.includes("autoLoop("),
"startAuto must call autoLoop() instead of dispatchNextUnit()",
fnBlock.includes("runAutoLoopWithUok("),
"startAuto must dispatch through runAutoLoopWithUok()",
);
assert.ok(
fnBlock.includes("runLegacyLoop: autoLoop"),
"startAuto must preserve the legacy autoLoop adapter in kernel dispatch",
);
});

View file

@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ===');
const adapter = _getAdapter()!;
// Verify schema version is current (v14 after indexes + slice_dependencies)
// Verify schema version is current (v15 with UOK projection tables)
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
assertEq(versionRow?.['v'], 14, 'schema version should be 14');
assertEq(versionRow?.['v'], 15, 'schema version should be 15');
// Verify slices table has full_summary_md and full_uat_md columns
const cols = adapter.prepare("PRAGMA table_info(slices)").all();

View file

@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
const adapter = _getAdapter()!;
// Verify schema version is current (v14 after indexes + slice_dependencies)
// Verify schema version is current (v15 with UOK projection tables)
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
assertEq(versionRow?.['v'], 14, 'schema version should be 14');
assertEq(versionRow?.['v'], 15, 'schema version should be 15');
// Verify all 4 new tables exist
const tables = adapter.prepare(

View file

@ -80,7 +80,7 @@ describe('gsd-db', () => {
// Check schema_version table
const adapter = _getAdapter()!;
const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
assert.deepStrictEqual(version?.['version'], 14, 'schema version should be 14');
assert.deepStrictEqual(version?.['version'], 15, 'schema version should be 15');
// Check tables exist by querying them
const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();

View file

@ -363,7 +363,7 @@ test('md-importer: schema v1→v2 migration', () => {
openDatabase(':memory:');
const adapter = _getAdapter();
const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
assert.deepStrictEqual(version?.v, 14, 'new DB should be at schema version 14');
assert.deepStrictEqual(version?.v, 15, 'new DB should be at schema version 15');
// Artifacts table should exist
const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
@ -413,4 +413,3 @@ test('md-importer: round-trip fidelity', () => {
});
// ═══════════════════════════════════════════════════════════════════════════

View file

@ -323,10 +323,9 @@ test('memory-store: schema includes memories table', () => {
const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get();
assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist');
// Verify schema version is 14 (after indexes + slice_dependencies)
// Verify schema version is 15 (UOK gate/git/audit projection tables included)
const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
assert.deepStrictEqual(version?.['v'], 14, 'schema version should be 14');
assert.deepStrictEqual(version?.['v'], 15, 'schema version should be 15');
closeDatabase();
});

View file

@ -34,11 +34,12 @@ const typesSrc = readFileSync(join(__dirname, "..", "types.ts"), "utf-8");
// Type Definitions
// ═══════════════════════════════════════════════════════════════════════════
test("types: TokenProfile type exported with budget/balanced/quality", () => {
test("types: TokenProfile type exported with budget/balanced/quality/burn-max", () => {
assert.ok(typesSrc.includes("export type TokenProfile"), "TokenProfile should be exported");
assert.match(typesSrc, /["']budget["']/, "should include budget");
assert.match(typesSrc, /["']balanced["']/, "should include balanced");
assert.match(typesSrc, /["']quality["']/, "should include quality");
assert.match(typesSrc, /["']burn-max["']/, "should include burn-max");
});
test("types: InlineLevel type exported with full/standard/minimal", () => {
@ -91,7 +92,7 @@ test("preferences: KNOWN_PREFERENCE_KEYS includes token_profile and phases", ()
// Profile Resolution
// ═══════════════════════════════════════════════════════════════════════════
test("profile: resolveProfileDefaults exists and handles all 3 tiers", () => {
test("profile: resolveProfileDefaults exists and handles all 4 tiers", () => {
assert.ok(
preferencesSrc.includes("export function resolveProfileDefaults"),
"resolveProfileDefaults should be exported",
@ -99,8 +100,9 @@ test("profile: resolveProfileDefaults exists and handles all 3 tiers", () => {
assert.ok(
preferencesSrc.includes('case "budget"') &&
preferencesSrc.includes('case "balanced"') &&
preferencesSrc.includes('case "quality"'),
"resolveProfileDefaults should handle all 3 tiers",
preferencesSrc.includes('case "quality"') &&
preferencesSrc.includes('case "burn-max"'),
"resolveProfileDefaults should handle all 4 tiers",
);
});
@ -158,6 +160,7 @@ test("profile: resolveInlineLevel maps profile to inline level", () => {
assert.ok(preferencesSrc.includes('case "budget": return "minimal"'), "budget → minimal");
assert.ok(preferencesSrc.includes('case "balanced": return "standard"'), "balanced → standard");
assert.ok(preferencesSrc.includes('case "quality": return "full"'), "quality → full");
assert.ok(preferencesSrc.includes('case "burn-max": return "full"'), "burn-max → full");
});
// ═══════════════════════════════════════════════════════════════════════════
@ -167,7 +170,7 @@ test("profile: resolveInlineLevel maps profile to inline level", () => {
test("validate: validatePreferences handles token_profile", () => {
assert.ok(
preferencesSrc.includes("preferences.token_profile") &&
preferencesSrc.includes("budget, balanced, quality"),
preferencesSrc.includes("budget, balanced, quality, burn-max"),
"validatePreferences should validate token_profile enum values",
);
});

View file

@ -0,0 +1,85 @@
import test from "node:test";
import assert from "node:assert/strict";
import type {
AuditEventEnvelope,
GateResult,
TurnContract,
TurnResult,
UokNodeKind,
} from "../uok/contracts.ts";
import { buildAuditEnvelope } from "../uok/audit.ts";
test("uok contracts serialize/deserialize turn envelopes", () => {
const contract: TurnContract = {
traceId: "trace-1",
turnId: "turn-1",
iteration: 1,
basePath: "/tmp/project",
unitType: "execute-task",
unitId: "M001.S01.T01",
startedAt: new Date().toISOString(),
};
const gate: GateResult = {
gateId: "Q3",
gateType: "policy",
outcome: "pass",
failureClass: "none",
attempt: 1,
maxAttempts: 1,
retryable: false,
evaluatedAt: new Date().toISOString(),
};
const result: TurnResult = {
traceId: contract.traceId,
turnId: contract.turnId,
iteration: contract.iteration,
unitType: contract.unitType,
unitId: contract.unitId,
status: "completed",
failureClass: "none",
phaseResults: [
{ phase: "dispatch", action: "next", ts: new Date().toISOString() },
{ phase: "unit", action: "continue", ts: new Date().toISOString() },
{ phase: "finalize", action: "next", ts: new Date().toISOString() },
],
gateResults: [gate],
startedAt: contract.startedAt,
finishedAt: new Date().toISOString(),
};
const roundTrip = JSON.parse(JSON.stringify(result)) as TurnResult;
assert.equal(roundTrip.turnId, "turn-1");
assert.equal(roundTrip.gateResults?.[0]?.gateId, "Q3");
assert.equal(roundTrip.phaseResults.length, 3);
});
test("uok contracts include required DAG node kinds", () => {
const required: UokNodeKind[] = [
"unit",
"hook",
"subagent",
"team-worker",
"verification",
"reprocess",
];
assert.deepEqual(required.length, 6);
});
test("uok audit envelope includes trace/turn/causality fields", () => {
const event: AuditEventEnvelope = buildAuditEnvelope({
traceId: "trace-xyz",
turnId: "turn-xyz",
causedBy: "turn-start",
category: "orchestration",
type: "turn-result",
payload: { status: "completed" },
});
assert.equal(event.traceId, "trace-xyz");
assert.equal(event.turnId, "turn-xyz");
assert.equal(event.causedBy, "turn-start");
assert.equal(event.payload.status, "completed");
});

View file

@ -0,0 +1,70 @@
import test from "node:test";
import assert from "node:assert/strict";
import { closeDatabase, openDatabase, _getAdapter } from "../gsd-db.ts";
import { UokGateRunner } from "../uok/gate-runner.ts";
test.beforeEach(() => {
closeDatabase();
const ok = openDatabase(":memory:");
assert.equal(ok, true);
});
test.afterEach(() => {
closeDatabase();
});
test("uok gate runner retries timeout failures using deterministic matrix", async () => {
const runner = new UokGateRunner();
let calls = 0;
runner.register({
id: "timeout-gate",
type: "verification",
execute: async (_ctx, attempt) => {
calls += 1;
if (attempt < 2) {
return {
outcome: "fail",
failureClass: "timeout",
rationale: "first attempt timed out",
};
}
return {
outcome: "pass",
failureClass: "none",
rationale: "second attempt passed",
};
},
});
const result = await runner.run("timeout-gate", {
basePath: process.cwd(),
traceId: "trace-a",
turnId: "turn-a",
milestoneId: "M001",
sliceId: "S01",
taskId: "T01",
});
assert.equal(result.outcome, "pass");
assert.equal(calls, 2);
const adapter = _getAdapter();
const rows = adapter?.prepare("SELECT gate_id, outcome, attempt FROM gate_runs ORDER BY id").all() ?? [];
assert.equal(rows.length, 2);
assert.equal(rows[0]?.["outcome"], "retry");
assert.equal(rows[1]?.["outcome"], "pass");
});
test("uok gate runner returns manual-attention for unknown gate id", async () => {
const runner = new UokGateRunner();
const result = await runner.run("missing-gate", {
basePath: process.cwd(),
traceId: "trace-b",
turnId: "turn-b",
});
assert.equal(result.outcome, "manual-attention");
assert.equal(result.failureClass, "unknown");
});

View file

@ -0,0 +1,40 @@
import test from "node:test";
import assert from "node:assert/strict";
import { validatePreferences } from "../preferences-validation.ts";
test("uok preferences validate nested flags and turn_action", () => {
const input = {
uok: {
enabled: true,
gates: { enabled: true },
model_policy: { enabled: true },
execution_graph: { enabled: false },
gitops: {
enabled: true,
turn_action: "status-only",
turn_push: false,
},
audit_unified: { enabled: true },
plan_v2: { enabled: true },
},
};
const result = validatePreferences(input as never);
assert.equal(result.errors.length, 0);
assert.equal(result.preferences.uok?.enabled, true);
assert.equal(result.preferences.uok?.gitops?.turn_action, "status-only");
assert.equal(result.preferences.uok?.plan_v2?.enabled, true);
});
test("uok preferences reject invalid turn_action", () => {
const result = validatePreferences({
uok: {
gitops: {
turn_action: "push-everything",
},
},
} as never);
assert.ok(result.errors.some((e) => e.includes("uok.gitops.turn_action")));
});

View file

@ -306,7 +306,7 @@ export interface HookDispatchResult {
export type BudgetEnforcementMode = "warn" | "pause" | "halt";
export type TokenProfile = "budget" | "balanced" | "quality";
export type TokenProfile = "budget" | "balanced" | "quality" | "burn-max";
export type InlineLevel = "full" | "standard" | "minimal";

View file

@ -0,0 +1,51 @@
import { appendFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import { gsdRoot } from "../paths.js";
import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
import type { AuditEventEnvelope } from "./contracts.js";
function auditLogPath(basePath: string): string {
return join(gsdRoot(basePath), "audit", "events.jsonl");
}
function ensureAuditDir(basePath: string): void {
mkdirSync(join(gsdRoot(basePath), "audit"), { recursive: true });
}
export function buildAuditEnvelope(args: {
traceId: string;
turnId?: string;
causedBy?: string;
category: AuditEventEnvelope["category"];
type: string;
payload?: Record<string, unknown>;
}): AuditEventEnvelope {
return {
eventId: randomUUID(),
traceId: args.traceId,
turnId: args.turnId,
causedBy: args.causedBy,
category: args.category,
type: args.type,
ts: new Date().toISOString(),
payload: args.payload ?? {},
};
}
export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope): void {
try {
ensureAuditDir(basePath);
appendFileSync(auditLogPath(basePath), `${JSON.stringify(event)}\n`, "utf-8");
} catch {
// Best-effort: audit writes must never break orchestration.
}
if (!isDbAvailable()) return;
try {
insertAuditEvent(event);
} catch {
// Projection failures are non-fatal while legacy readers are still active.
}
}

View file

@ -0,0 +1,135 @@
export type FailureClass =
| "none"
| "policy"
| "input"
| "execution"
| "artifact"
| "verification"
| "closeout"
| "git"
| "timeout"
| "manual-attention"
| "unknown";
export type GateOutcome = "pass" | "fail" | "retry" | "manual-attention";
export interface GateResult {
gateId: string;
gateType: string;
outcome: GateOutcome;
failureClass: FailureClass;
rationale?: string;
findings?: string;
attempt: number;
maxAttempts: number;
retryable: boolean;
evaluatedAt: string;
}
export type TurnPhase =
| "pre-dispatch"
| "dispatch"
| "unit"
| "finalize"
| "guard"
| "custom-engine";
export type TurnStatus =
| "completed"
| "failed"
| "paused"
| "stopped"
| "skipped"
| "retry";
export interface TurnContract {
traceId: string;
turnId: string;
iteration: number;
basePath: string;
unitType?: string;
unitId?: string;
sidecarKind?: string;
startedAt: string;
metadata?: Record<string, unknown>;
}
export interface TurnCloseoutRecord {
traceId: string;
turnId: string;
unitType?: string;
unitId?: string;
status: TurnStatus;
failureClass: FailureClass;
gitAction: "commit" | "snapshot" | "status-only";
gitPushed: boolean;
activityFile?: string;
finishedAt: string;
}
export interface TurnResult {
traceId: string;
turnId: string;
iteration: number;
unitType?: string;
unitId?: string;
status: TurnStatus;
failureClass: FailureClass;
phaseResults: Array<{
phase: TurnPhase;
action: string;
ts: string;
data?: Record<string, unknown>;
}>;
gateResults?: GateResult[];
closeout?: TurnCloseoutRecord;
error?: string;
startedAt: string;
finishedAt: string;
}
export interface AuditEventEnvelope {
eventId: string;
traceId: string;
turnId?: string;
causedBy?: string;
category:
| "orchestration"
| "gate"
| "model-policy"
| "gitops"
| "verification"
| "metrics"
| "plan"
| "execution";
type: string;
ts: string;
payload: Record<string, unknown>;
}
export type UokNodeKind =
| "unit"
| "hook"
| "subagent"
| "team-worker"
| "verification"
| "reprocess";
export interface UokGraphNode {
id: string;
kind: UokNodeKind;
dependsOn: string[];
writes?: string[];
reads?: string[];
metadata?: Record<string, unknown>;
}
export interface UokTurnObserver {
onTurnStart(contract: TurnContract): void;
onPhaseResult(
phase: TurnPhase,
action: string,
data?: Record<string, unknown>,
): void;
onTurnResult(result: TurnResult): void;
}

View file

@ -0,0 +1,121 @@
import type { UokGraphNode } from "./contracts.js";
export interface ExecutionGraphRunOptions {
parallel?: boolean;
maxWorkers?: number;
}
export interface ExecutionGraphResult {
order: string[];
conflicts: Array<{ nodeA: string; nodeB: string; file: string }>;
}
export type ExecutionNodeHandler = (node: UokGraphNode) => Promise<void>;
export class ExecutionGraphScheduler {
private readonly handlers = new Map<string, ExecutionNodeHandler>();
registerHandler(kind: UokGraphNode["kind"], handler: ExecutionNodeHandler): void {
this.handlers.set(kind, handler);
}
async run(nodes: UokGraphNode[], options?: ExecutionGraphRunOptions): Promise<ExecutionGraphResult> {
const sorted = topologicalSort(nodes);
const conflicts = detectFileConflicts(nodes);
// Default deterministic serial execution remains the reference path.
if (!options?.parallel) {
for (const node of sorted) {
const handler = this.handlers.get(node.kind);
if (handler) await handler(node);
}
return { order: sorted.map((n) => n.id), conflicts };
}
// Parallel mode only for nodes whose dependencies are already satisfied.
const maxWorkers = Math.max(1, Math.min(8, options.maxWorkers ?? 2));
const remaining = new Map(nodes.map((n) => [n.id, n]));
const done = new Set<string>();
const order: string[] = [];
while (remaining.size > 0) {
const ready = Array.from(remaining.values()).filter((node) =>
node.dependsOn.every((dep) => done.has(dep)),
);
if (ready.length === 0) {
throw new Error("Execution graph deadlock detected: no ready nodes and graph not complete");
}
const batch = ready.slice(0, maxWorkers);
await Promise.all(
batch.map(async (node) => {
const handler = this.handlers.get(node.kind);
if (handler) await handler(node);
done.add(node.id);
order.push(node.id);
remaining.delete(node.id);
}),
);
}
return { order, conflicts };
}
}
function topologicalSort(nodes: UokGraphNode[]): UokGraphNode[] {
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
const inDegree = new Map(nodes.map((n) => [n.id, 0]));
for (const node of nodes) {
for (const dep of node.dependsOn) {
if (nodeMap.has(dep)) {
inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1);
}
}
}
const queue = nodes
.filter((n) => (inDegree.get(n.id) ?? 0) === 0)
.sort((a, b) => a.id.localeCompare(b.id));
const ordered: UokGraphNode[] = [];
while (queue.length > 0) {
const current = queue.shift()!;
ordered.push(current);
for (const next of nodes) {
if (!next.dependsOn.includes(current.id)) continue;
const deg = (inDegree.get(next.id) ?? 0) - 1;
inDegree.set(next.id, deg);
if (deg === 0) {
queue.push(next);
queue.sort((a, b) => a.id.localeCompare(b.id));
}
}
}
if (ordered.length !== nodes.length) {
throw new Error("Execution graph has cyclic dependencies");
}
return ordered;
}
function detectFileConflicts(nodes: UokGraphNode[]): Array<{ nodeA: string; nodeB: string; file: string }> {
const conflicts: Array<{ nodeA: string; nodeB: string; file: string }> = [];
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
const writesA = new Set(a.writes ?? []);
if (writesA.size === 0) continue;
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
for (const file of b.writes ?? []) {
if (writesA.has(file)) {
conflicts.push({ nodeA: a.id, nodeB: b.id, file });
}
}
}
}
return conflicts;
}

View file

@ -0,0 +1,34 @@
import type { GSDPreferences } from "../preferences.js";
import { loadEffectiveGSDPreferences } from "../preferences.js";
export interface UokFlags {
enabled: boolean;
gates: boolean;
modelPolicy: boolean;
executionGraph: boolean;
gitops: boolean;
gitopsTurnAction: "commit" | "snapshot" | "status-only";
gitopsTurnPush: boolean;
auditUnified: boolean;
planV2: boolean;
}
export function resolveUokFlags(prefs: GSDPreferences | undefined): UokFlags {
const uok = prefs?.uok;
return {
enabled: uok?.enabled === true,
gates: uok?.gates?.enabled === true,
modelPolicy: uok?.model_policy?.enabled === true,
executionGraph: uok?.execution_graph?.enabled === true,
gitops: uok?.gitops?.enabled === true,
gitopsTurnAction: uok?.gitops?.turn_action ?? "status-only",
gitopsTurnPush: uok?.gitops?.turn_push === true,
auditUnified: uok?.audit_unified?.enabled === true,
planV2: uok?.plan_v2?.enabled === true,
};
}
export function loadUokFlags(): UokFlags {
const prefs = loadEffectiveGSDPreferences()?.preferences;
return resolveUokFlags(prefs);
}

View file

@ -0,0 +1,146 @@
import type { FailureClass, GateResult } from "./contracts.js";
import { insertGateRun } from "../gsd-db.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
export interface GateRunnerContext {
basePath: string;
traceId: string;
turnId: string;
milestoneId?: string;
sliceId?: string;
taskId?: string;
unitType?: string;
unitId?: string;
}
export interface GateExecutionInput {
id: string;
type: string;
execute: (ctx: GateRunnerContext, attempt: number) => Promise<{
outcome: "pass" | "fail" | "retry" | "manual-attention";
rationale?: string;
findings?: string;
failureClass?: FailureClass;
}>;
}
const RETRY_MATRIX: Record<FailureClass, number> = {
none: 0,
policy: 0,
input: 0,
execution: 1,
artifact: 1,
verification: 1,
closeout: 1,
git: 1,
timeout: 2,
"manual-attention": 0,
unknown: 0,
};
export class UokGateRunner {
private readonly registry = new Map<string, GateExecutionInput>();
register(gate: GateExecutionInput): void {
this.registry.set(gate.id, gate);
}
list(): GateExecutionInput[] {
return Array.from(this.registry.values());
}
async run(id: string, ctx: GateRunnerContext): Promise<GateResult> {
const gate = this.registry.get(id);
if (!gate) {
return {
gateId: id,
gateType: "unknown",
outcome: "manual-attention",
failureClass: "unknown",
rationale: `Gate ${id} not registered`,
attempt: 1,
maxAttempts: 1,
retryable: false,
evaluatedAt: new Date().toISOString(),
};
}
let attempt = 0;
let final: GateResult | null = null;
const maxAttemptsByFailureClass = RETRY_MATRIX;
while (attempt < 3) {
attempt += 1;
const now = new Date().toISOString();
const result = await gate.execute(ctx, attempt);
const failureClass = result.failureClass ?? (result.outcome === "pass" ? "none" : "unknown");
const retryBudget = maxAttemptsByFailureClass[failureClass] ?? 0;
const retryable = result.outcome !== "pass" && attempt <= retryBudget;
final = {
gateId: gate.id,
gateType: gate.type,
outcome: retryable ? "retry" : result.outcome,
failureClass,
rationale: result.rationale,
findings: result.findings,
attempt,
maxAttempts: Math.max(1, retryBudget),
retryable,
evaluatedAt: now,
};
insertGateRun({
traceId: ctx.traceId,
turnId: ctx.turnId,
gateId: final.gateId,
gateType: final.gateType,
unitType: ctx.unitType,
unitId: ctx.unitId,
milestoneId: ctx.milestoneId,
sliceId: ctx.sliceId,
taskId: ctx.taskId,
outcome: final.outcome,
failureClass: final.failureClass,
rationale: final.rationale,
findings: final.findings,
attempt: final.attempt,
maxAttempts: final.maxAttempts,
retryable: final.retryable,
evaluatedAt: final.evaluatedAt,
});
emitUokAuditEvent(
ctx.basePath,
buildAuditEnvelope({
traceId: ctx.traceId,
turnId: ctx.turnId,
category: "gate",
type: "gate-run",
payload: {
gateId: final.gateId,
gateType: final.gateType,
outcome: final.outcome,
failureClass: final.failureClass,
attempt: final.attempt,
maxAttempts: final.maxAttempts,
retryable: final.retryable,
},
}),
);
if (!retryable) break;
}
return final ?? {
gateId: gate.id,
gateType: gate.type,
outcome: "manual-attention",
failureClass: "unknown",
attempt: 1,
maxAttempts: 1,
retryable: false,
evaluatedAt: new Date().toISOString(),
};
}
}

View file

@ -0,0 +1,75 @@
import { isDbAvailable, upsertTurnGitTransaction } from "../gsd-db.js";
import type { TurnCloseoutRecord } from "./contracts.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
export type TurnGitStage = "turn-start" | "stage" | "checkpoint" | "publish" | "record";
interface GitTxArgs {
basePath: string;
traceId: string;
turnId: string;
unitType?: string;
unitId?: string;
stage: TurnGitStage;
action: "commit" | "snapshot" | "status-only";
push: boolean;
status: "ok" | "failed";
error?: string;
metadata?: Record<string, unknown>;
}
export function writeTurnGitTransaction(args: GitTxArgs): void {
if (!isDbAvailable()) return;
upsertTurnGitTransaction({
traceId: args.traceId,
turnId: args.turnId,
unitType: args.unitType,
unitId: args.unitId,
stage: args.stage,
action: args.action,
push: args.push,
status: args.status,
error: args.error,
metadata: args.metadata,
updatedAt: new Date().toISOString(),
});
emitUokAuditEvent(
args.basePath,
buildAuditEnvelope({
traceId: args.traceId,
turnId: args.turnId,
category: "gitops",
type: `turn-git-${args.stage}`,
payload: {
unitType: args.unitType,
unitId: args.unitId,
action: args.action,
push: args.push,
status: args.status,
error: args.error,
...(args.metadata ?? {}),
},
}),
);
}
export function writeTurnCloseoutGitRecord(basePath: string, record: TurnCloseoutRecord): void {
writeTurnGitTransaction({
basePath,
traceId: record.traceId,
turnId: record.turnId,
unitType: record.unitType,
unitId: record.unitId,
stage: "record",
action: record.gitAction,
push: record.gitPushed,
status: record.failureClass === "git" ? "failed" : "ok",
error: record.failureClass === "git" ? "git closeout failure" : undefined,
metadata: {
turnStatus: record.status,
finishedAt: record.finishedAt,
activityFile: record.activityFile,
},
});
}

View file

@ -0,0 +1,98 @@
import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
import { appendFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import type { AutoSession } from "../auto/session.js";
import type { LoopDeps } from "../auto/loop-deps.js";
import { gsdRoot } from "../paths.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { resolveUokFlags } from "./flags.js";
import { createTurnObserver } from "./loop-adapter.js";
interface RunAutoLoopWithUokArgs {
ctx: ExtensionContext;
pi: ExtensionAPI;
s: AutoSession;
deps: LoopDeps;
runLegacyLoop: (
ctx: ExtensionContext,
pi: ExtensionAPI,
s: AutoSession,
deps: LoopDeps,
) => Promise<void>;
}
function parityLogPath(basePath: string): string {
return join(gsdRoot(basePath), "runtime", "uok-parity.jsonl");
}
function writeParityEvent(basePath: string, event: Record<string, unknown>): void {
try {
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
appendFileSync(parityLogPath(basePath), `${JSON.stringify(event)}\n`, "utf-8");
} catch {
// parity telemetry must never block orchestration
}
}
export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise<void> {
const { ctx, pi, s, deps, runLegacyLoop } = args;
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
const flags = resolveUokFlags(prefs);
writeParityEvent(s.basePath, {
ts: new Date().toISOString(),
path: flags.enabled ? "uok-wrapper" : "legacy-wrapper",
flags,
phase: "enter",
});
if (flags.auditUnified) {
emitUokAuditEvent(
s.basePath,
buildAuditEnvelope({
traceId: `session:${String(s.autoStartTime || Date.now())}`,
category: "orchestration",
type: "uok-kernel-enter",
payload: {
flags,
sessionId: ctx.sessionManager?.getSessionId?.(),
},
}),
);
}
const decoratedDeps: LoopDeps = flags.enabled
? {
...deps,
uokObserver: createTurnObserver({
basePath: s.basePath,
gitAction: flags.gitopsTurnAction,
gitPush: flags.gitopsTurnPush,
enableAudit: flags.auditUnified,
enableGitops: flags.gitops,
}),
}
: deps;
try {
await runLegacyLoop(ctx, pi, s, decoratedDeps);
writeParityEvent(s.basePath, {
ts: new Date().toISOString(),
path: flags.enabled ? "uok-wrapper" : "legacy-wrapper",
flags,
phase: "exit",
status: "ok",
});
} catch (err) {
writeParityEvent(s.basePath, {
ts: new Date().toISOString(),
path: flags.enabled ? "uok-wrapper" : "legacy-wrapper",
flags,
phase: "exit",
status: "error",
error: err instanceof Error ? err.message : String(err),
});
throw err;
}
}

View file

@ -0,0 +1,162 @@
import type {
TurnCloseoutRecord,
TurnContract,
TurnResult,
UokTurnObserver,
} from "./contracts.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { writeTurnCloseoutGitRecord, writeTurnGitTransaction } from "./gitops.js";
export interface CreateTurnObserverOptions {
basePath: string;
gitAction: "commit" | "snapshot" | "status-only";
gitPush: boolean;
enableAudit: boolean;
enableGitops: boolean;
}
export function createTurnObserver(options: CreateTurnObserverOptions): UokTurnObserver {
let current: TurnContract | null = null;
const phaseResults: TurnResult["phaseResults"] = [];
return {
onTurnStart(contract): void {
current = contract;
phaseResults.length = 0;
if (options.enableGitops) {
writeTurnGitTransaction({
basePath: options.basePath,
traceId: contract.traceId,
turnId: contract.turnId,
unitType: contract.unitType,
unitId: contract.unitId,
stage: "turn-start",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: {
iteration: contract.iteration,
sidecarKind: contract.sidecarKind,
},
});
}
if (options.enableAudit) {
emitUokAuditEvent(
options.basePath,
buildAuditEnvelope({
traceId: contract.traceId,
turnId: contract.turnId,
category: "orchestration",
type: "turn-start",
payload: {
iteration: contract.iteration,
unitType: contract.unitType,
unitId: contract.unitId,
sidecarKind: contract.sidecarKind,
},
}),
);
}
},
onPhaseResult(phase, action, data): void {
phaseResults.push({
phase,
action,
ts: new Date().toISOString(),
data,
});
if (!current || !options.enableGitops) return;
if (phase === "dispatch") {
writeTurnGitTransaction({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: data?.unitType as string | undefined,
unitId: data?.unitId as string | undefined,
stage: "stage",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: { action },
});
}
if (phase === "unit") {
writeTurnGitTransaction({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: data?.unitType as string | undefined,
unitId: data?.unitId as string | undefined,
stage: "checkpoint",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: { action },
});
}
if (phase === "finalize") {
writeTurnGitTransaction({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: data?.unitType as string | undefined,
unitId: data?.unitId as string | undefined,
stage: "publish",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: { action },
});
}
},
onTurnResult(result): void {
const merged: TurnResult = {
...result,
phaseResults: result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults],
};
if (options.enableAudit) {
emitUokAuditEvent(
options.basePath,
buildAuditEnvelope({
traceId: merged.traceId,
turnId: merged.turnId,
category: "orchestration",
type: "turn-result",
payload: {
unitType: merged.unitType,
unitId: merged.unitId,
status: merged.status,
failureClass: merged.failureClass,
error: merged.error,
phaseCount: merged.phaseResults.length,
},
}),
);
}
if (options.enableGitops) {
const closeout: TurnCloseoutRecord = merged.closeout ?? {
traceId: merged.traceId,
turnId: merged.turnId,
unitType: merged.unitType,
unitId: merged.unitId,
status: merged.status,
failureClass: merged.failureClass,
gitAction: options.gitAction,
gitPushed: options.gitPush,
finishedAt: merged.finishedAt,
};
writeTurnCloseoutGitRecord(options.basePath, closeout);
}
current = null;
phaseResults.length = 0;
},
};
}

View file

@ -0,0 +1,112 @@
import type { TaskMetadata } from "../model-router.js";
import { computeTaskRequirements, filterToolsForProvider } from "../model-router.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
export interface ModelCandidate {
id: string;
provider: string;
api: string;
}
export interface ModelPolicyDecision {
modelId: string;
provider: string;
allowed: boolean;
reason: string;
}
export interface ModelPolicyOptions {
basePath: string;
traceId: string;
turnId?: string;
unitType?: string;
taskMetadata?: TaskMetadata;
currentProvider?: string;
allowCrossProvider?: boolean;
requiredTools?: string[];
deniedProviders?: string[];
allowedApis?: string[];
}
export function buildRequirementVector(unitType?: string, taskMetadata?: TaskMetadata): Partial<Record<string, number>> {
if (!unitType) return {};
return computeTaskRequirements(unitType, taskMetadata) as unknown as Partial<Record<string, number>>;
}
export function applyModelPolicyFilter(
candidates: ModelCandidate[],
options: ModelPolicyOptions,
): {
eligible: ModelCandidate[];
decisions: ModelPolicyDecision[];
requirements: Partial<Record<string, number>>;
} {
const requiredTools = options.requiredTools ?? [];
const deniedProviders = new Set((options.deniedProviders ?? []).map((p) => p.toLowerCase()));
const allowedApis = options.allowedApis ? new Set(options.allowedApis) : null;
const requirements = buildRequirementVector(options.unitType, options.taskMetadata);
const decisions: ModelPolicyDecision[] = [];
const eligible: ModelCandidate[] = [];
for (const model of candidates) {
let allowed = true;
let reason = "allowed";
if (options.allowCrossProvider === false && options.currentProvider && model.provider !== options.currentProvider) {
allowed = false;
reason = `cross-provider routing disabled (${model.provider} != ${options.currentProvider})`;
}
if (allowed && deniedProviders.has(model.provider.toLowerCase())) {
allowed = false;
reason = `provider denied by policy: ${model.provider}`;
}
if (allowed && allowedApis && !allowedApis.has(model.api)) {
allowed = false;
reason = `transport/api denied by policy: ${model.api}`;
}
if (allowed && requiredTools.length > 0) {
const compatibility = filterToolsForProvider(requiredTools, model.api);
if (compatibility.filtered.length > 0) {
allowed = false;
reason = `tool policy denied (${compatibility.filtered.join(", ")}) for ${model.api}`;
}
}
const decision: ModelPolicyDecision = {
modelId: model.id,
provider: model.provider,
allowed,
reason,
};
decisions.push(decision);
emitUokAuditEvent(
options.basePath,
buildAuditEnvelope({
traceId: options.traceId,
turnId: options.turnId,
category: "model-policy",
type: allowed ? "model-policy-allow" : "model-policy-deny",
payload: {
modelId: model.id,
provider: model.provider,
api: model.api,
reason,
unitType: options.unitType,
requirements,
},
}),
);
if (allowed) eligible.push(model);
}
return {
eligible,
decisions,
requirements,
};
}

View file

@ -0,0 +1,87 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { GSDState } from "../types.js";
import { gsdRoot } from "../paths.js";
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "../gsd-db.js";
import type { UokGraphNode } from "./contracts.js";
export interface PlanV2CompileResult {
ok: boolean;
reason?: string;
graphPath?: string;
nodeCount?: number;
}
function graphOutputPath(basePath: string): string {
return join(gsdRoot(basePath), "runtime", "uok-plan-v2-graph.json");
}
export function compileUnitGraphFromState(basePath: string, state: GSDState): PlanV2CompileResult {
const mid = state.activeMilestone?.id;
if (!mid) return { ok: false, reason: "no active milestone" };
if (!isDbAvailable()) return { ok: false, reason: "database not available" };
const slices = getMilestoneSlices(mid).sort((a, b) => Number(a.sequence ?? 0) - Number(b.sequence ?? 0));
const nodes: UokGraphNode[] = [];
for (const slice of slices) {
const sid = slice.id;
const tasks = getSliceTasks(mid, sid)
.sort((a, b) => Number(a.sequence ?? 0) - Number(b.sequence ?? 0));
let previousTaskNodeId: string | null = null;
for (const task of tasks) {
const nodeId = `execute-task:${mid}:${sid}:${task.id}`;
const dependsOn = previousTaskNodeId ? [previousTaskNodeId] : [];
nodes.push({
id: nodeId,
kind: "unit",
dependsOn,
writes: task.key_files,
metadata: {
unitType: "execute-task",
unitId: `${mid}.${sid}.${task.id}`,
title: task.title,
status: task.status,
},
});
previousTaskNodeId = nodeId;
}
if (previousTaskNodeId) {
nodes.push({
id: `complete-slice:${mid}:${sid}`,
kind: "verification",
dependsOn: [previousTaskNodeId],
metadata: {
unitType: "complete-slice",
unitId: `${mid}.${sid}`,
title: slice.title,
status: slice.status,
},
});
}
}
const output = {
compiledAt: new Date().toISOString(),
milestoneId: mid,
nodes,
};
const outPath = graphOutputPath(basePath);
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
writeFileSync(outPath, JSON.stringify(output, null, 2) + "\n", "utf-8");
return { ok: true, graphPath: outPath, nodeCount: nodes.length };
}
export function ensurePlanV2Graph(basePath: string, state: GSDState): PlanV2CompileResult {
const compiled = compileUnitGraphFromState(basePath, state);
if (!compiled.ok) return compiled;
if ((compiled.nodeCount ?? 0) <= 0) {
return { ok: false, reason: "compiled graph is empty" };
}
return compiled;
}