refactor: remove legacy autonomous aliases

This commit is contained in:
Mikael Hugo 2026-05-05 18:47:50 +02:00
parent 861c4b6cf6
commit 46db1e95ef
18 changed files with 33 additions and 102 deletions

View file

@ -6,22 +6,16 @@ describe("parseArgs", () => {
it("parses optional-value extension flags with implicit and explicit values", () => {
const extensionFlags = new Map([
["genai-proxy", { type: "string" as const, allowNoValue: true }],
["gemini-cli-proxy", { type: "string" as const, allowNoValue: true }],
]);
const defaultFlagArgs = parseArgs(["--genai-proxy"], extensionFlags);
const explicitFlagArgs = parseArgs(["--genai-proxy=8080"], extensionFlags);
const legacyFlagArgs = parseArgs(
["--gemini-cli-proxy=3001"],
extensionFlags,
);
assert.deepEqual(
[
defaultFlagArgs.unknownFlags.get("genai-proxy"),
explicitFlagArgs.unknownFlags.get("genai-proxy"),
legacyFlagArgs.unknownFlags.get("gemini-cli-proxy"),
],
[true, "8080", "3001"],
[true, "8080"],
);
});
});

View file

@ -588,9 +588,7 @@ async function runHeadlessFromAutonomous(
}
// `sf autonomous [args...]` — shorthand for headless autonomous mode (#2732).
// The legacy `sf auto` spelling is still accepted for compatibility, but all
// generated prompts use `/sf autonomous`.
if (cliFlags.messages[0] === "auto" || cliFlags.messages[0] === "autonomous") {
if (cliFlags.messages[0] === "autonomous") {
await runHeadlessFromAutonomous([
"autonomous",
...cliFlags.messages.slice(1),
@ -878,10 +876,7 @@ if (!cliFlags.worktree && !isPrintMode) {
// the TUI cannot render and the process hangs. Redirect to headless mode
// which handles non-interactive output gracefully.
// ---------------------------------------------------------------------------
if (
(cliFlags.messages[0] === "auto" || cliFlags.messages[0] === "autonomous") &&
!process.stdout.isTTY
) {
if (cliFlags.messages[0] === "autonomous" && !process.stdout.isTTY) {
process.stderr.write(
"[forge] stdout is not a terminal — running autonomous mode in headless mode.\n",
);

View file

@ -382,7 +382,7 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
options.bare = true;
}
} else if (!commandSeen) {
if (arg === "autonomous" || arg === "auto") {
if (arg === "autonomous") {
options.command = "autonomous";
options.auto = true; // autonomous subcommand implies --auto
} else {

View file

@ -2,7 +2,6 @@ import { createProxyServer } from "./proxy-server.js";
const PROXY_COMMAND_NAME = "genai-proxy";
const PROXY_FLAG_NAME = "genai-proxy";
const LEGACY_PROXY_FLAG_NAME = "gemini-cli-proxy";
const DEFAULT_PROXY_PORT = 3000;
export function installGenaiProxyExtension(api, dependencies) {
let proxyServer = null;
@ -31,12 +30,6 @@ export function installGenaiProxyExtension(api, dependencies) {
allowNoValue: true,
onStartup: startProxyFromFlag,
});
api.registerFlag(LEGACY_PROXY_FLAG_NAME, {
description: "Legacy alias for --genai-proxy",
type: "string",
allowNoValue: true,
onStartup: startProxyFromFlag,
});
api.registerCommand(PROXY_COMMAND_NAME, {
description: "Manage the GenAI proxy server",
handler: async (args, context) => {

View file

@ -11,7 +11,11 @@ export {
INFRA_ERROR_CODES,
isInfrastructureError,
} from "./auto/infra-errors.js";
export { autoLoop, runLegacyAutoLoop, runUokKernelLoop } from "./auto/loop.js";
export {
autoLoop,
runStandardAutoLoop,
runUokKernelLoop,
} from "./auto/loop.js";
export {
_hasPendingResolve,
_resetPendingResolve,

View file

@ -337,11 +337,7 @@ export {
function registerSigtermHandler(currentBasePath) {
const prefs = loadEffectiveSFPreferences()?.preferences;
const flags = resolveUokFlags(prefs);
const pathLabel = flags.legacyFallback
? "legacy-fallback"
: flags.enabled
? "uok-kernel"
: "legacy-wrapper";
const pathLabel = flags.enabled ? "uok-kernel" : "standard-loop";
const onSignal = () => {
// Write UOK parity exit heartbeat before process.exit(0) bypasses
// the finally block in runAutoLoopWithUok. Fixes the enter/exit
@ -1736,7 +1732,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
s,
deps: buildLoopDeps(),
runKernelLoop: runUokKernelLoop,
runLegacyLoop: autoLoop,
runStandardLoop: autoLoop,
});
cleanupAfterLoopExit(ctx);
return;
@ -1785,7 +1781,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
s,
deps: buildLoopDeps(),
runKernelLoop: runUokKernelLoop,
runLegacyLoop: autoLoop,
runStandardLoop: autoLoop,
});
cleanupAfterLoopExit(ctx);
}

View file

@ -255,7 +255,7 @@ async function runUnitPhaseViaContract(
loopState,
sidecarItem,
) {
if (dispatchContract === "legacy-direct") {
if (dispatchContract === "standard-direct") {
return runUnitPhase(ic, iterData, loopState, sidecarItem);
}
const scheduler = new ExecutionGraphScheduler();
@ -308,7 +308,7 @@ async function enforceMinRequestInterval(s, prefs) {
* dispatchNextUnit handleAgentEnd dispatchNextUnit chain.
*/
export async function autoLoop(ctx, pi, s, deps, options) {
const dispatchContract = options?.dispatchContract ?? "legacy-direct";
const dispatchContract = options?.dispatchContract ?? "standard-direct";
debugLog("autoLoop", { phase: "enter" });
let iteration = 0;
// Load persisted stuck state so counters survive session restarts (#3704)
@ -1119,6 +1119,6 @@ export async function autoLoop(ctx, pi, s, deps, options) {
export async function runUokKernelLoop(ctx, pi, s, deps) {
return autoLoop(ctx, pi, s, deps, { dispatchContract: "uok-scheduler" });
}
export async function runLegacyAutoLoop(ctx, pi, s, deps) {
return autoLoop(ctx, pi, s, deps, { dispatchContract: "legacy-direct" });
export async function runStandardAutoLoop(ctx, pi, s, deps) {
return autoLoop(ctx, pi, s, deps, { dispatchContract: "standard-direct" });
}

View file

@ -17,8 +17,7 @@ import { guardRemoteSession, projectRoot } from "../context.js";
/**
* Parse --yolo flag and optional file path from the autonomous command string.
* Supports: `/sf autonomous --yolo path/to/file.md` or
* `/sf autonomous -y path/to/file.md`. The legacy `/sf auto` spelling is still
* normalized for compatibility, but is not a distinct user-facing mode.
* `/sf autonomous -y path/to/file.md`.
*/
function parseYoloFlag(trimmed) {
const yoloRe = /(?:--yolo|-y)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)/;
@ -51,7 +50,7 @@ export function parseMilestoneTarget(input) {
* Dispatch entry point for the autonomous command family.
*
* Handles `/sf autonomous`, `/sf next`, `/sf stop`, `/sf pause`, and their flag
* variants. Legacy `/sf auto` is accepted as a hidden compatibility spelling.
* variants.
* Returns `true` when the command was recognised and routed (caller stops
* searching), `false` when the command isn't autonomous-related.
*
@ -69,8 +68,6 @@ export function parseMilestoneTarget(input) {
export async function handleAutoCommand(trimmed, ctx, pi) {
const isAutonomousVerb =
trimmed === "autonomous" || trimmed.startsWith("autonomous ");
const isLegacyAutoVerb = trimmed === "auto" || trimmed.startsWith("auto ");
const isAutonomousFamily = isAutonomousVerb || isLegacyAutoVerb;
/**
* Route an autonomous launch through either the headless (in-process) or
* detached (spawned subprocess) entry point depending on `SF_HEADLESS`.
@ -114,9 +111,8 @@ export async function handleAutoCommand(trimmed, ctx, pi) {
});
return true;
}
if (isAutonomousFamily) {
const normalized = trimmed.replace(/^(?:auto|autonomous)\b/, "autonomous");
const { yoloSeedFile, rest: afterYolo } = parseYoloFlag(normalized);
if (isAutonomousVerb) {
const { yoloSeedFile, rest: afterYolo } = parseYoloFlag(trimmed);
const { milestoneId, rest: afterMilestone } =
parseMilestoneTarget(afterYolo);
const verboseMode = afterMilestone.includes("--verbose");

View file

@ -210,8 +210,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
- `uok`: orchestration kernel controls. Keys:
- `enabled`: boolean — enable kernel wrappers and contract observers. Default: `true`.
- `legacy_fallback.enabled`: boolean — emergency release fallback that forces legacy orchestration behavior even when `uok.enabled` is `true`. Default: `false`.
- Runtime override: set `SF_UOK_FORCE_LEGACY=1` (or `SF_UOK_LEGACY_FALLBACK=1`) to force legacy behavior for the current process.
- `gates.enabled`: boolean — route checks through the unified gate runner and persist `gate_runs`. Default: `true`.
- `model_policy.enabled`: boolean — enforce policy filtering before model capability scoring. Default: `true`.
- `execution_graph.enabled`: boolean — enable DAG scheduler facade/adapters for execution. Default: `true`.

View file

@ -240,7 +240,6 @@ export function validatePreferences(preferences) {
valid[normalizedTargetKey] = parsed;
}
};
parseEnabledBlock("legacy_fallback");
parseEnabledBlock("gates");
parseEnabledBlock("model_policy");
parseEnabledBlock("execution_graph");
@ -297,7 +296,6 @@ export function validatePreferences(preferences) {
}
const knownUokKeys = new Set([
"enabled",
"legacy_fallback",
"gates",
"model_policy",
"execution_graph",

View file

@ -440,13 +440,6 @@ function mergePreferences(base, override) {
base.uok || override.uok
? {
enabled: override.uok?.enabled ?? base.uok?.enabled,
legacy_fallback:
base.uok?.legacy_fallback || override.uok?.legacy_fallback
? {
...(base.uok?.legacy_fallback ?? {}),
...(override.uok?.legacy_fallback ?? {}),
}
: undefined,
gates:
base.uok?.gates || override.uok?.gates
? { ...(base.uok?.gates ?? {}), ...(override.uok?.gates ?? {}) }

View file

@ -63,8 +63,6 @@ dynamic_routing:
hooks:
uok:
enabled: true
legacy_fallback:
enabled: false
gates:
enabled: true
model_policy:

View file

@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
/**
* Read the last UOK parity report from <basePath>/.sf/runtime/uok-parity-report.json
* and surface any divergences/fallbacks via ctx.ui?.notify?.().
* and surface any divergences or errors via ctx.ui?.notify?.().
*
* Never throws all errors are swallowed so session_start is never blocked.
*/
@ -17,12 +17,10 @@ export async function summarizeParityReport(basePath, ctx, pi) {
return;
}
const mismatches = report.criticalMismatches?.length ?? 0;
const fallbacks = report.fallbackInvocations ?? 0;
const errors = report.statuses?.error ?? 0;
if (mismatches > 0 || fallbacks > 0 || errors > 0) {
if (mismatches > 0 || errors > 0) {
const msg =
`UOK parity report shows ${mismatches} critical mismatch${mismatches === 1 ? "" : "es"}, ` +
`${fallbacks} fallback invocation${fallbacks === 1 ? "" : "s"}, ` +
`${errors} error${errors === 1 ? "" : "s"} since ${report.generatedAt}. ` +
`Inspect .sf/runtime/uok-parity-report.json.`;
ctx.ui?.notify?.(msg, "warning");

View file

@ -1,25 +1,10 @@
import { loadEffectiveSFPreferences } from "../preferences.js";
function envForcesLegacyFallback() {
const raw =
process.env.SF_UOK_FORCE_LEGACY ?? process.env.SF_UOK_LEGACY_FALLBACK;
if (!raw) return false;
const normalized = raw.trim().toLowerCase();
return (
normalized === "1" ||
normalized === "true" ||
normalized === "yes" ||
normalized === "on"
);
}
export function resolveUokFlags(prefs) {
const uok = prefs?.uok;
const legacyFallback =
uok?.legacy_fallback?.enabled === true || envForcesLegacyFallback();
const enabledByPreference = uok?.enabled ?? true;
return {
enabled: enabledByPreference && !legacyFallback,
legacyFallback,
enabled: enabledByPreference,
gates: uok?.gates?.enabled ?? true,
modelPolicy: uok?.model_policy?.enabled ?? true,
executionGraph: uok?.execution_graph?.enabled ?? true,

View file

@ -21,11 +21,10 @@ function refreshParityReport(basePath) {
}
}
function resolveKernelPathLabel(flags) {
if (flags.legacyFallback) return "legacy-fallback";
return flags.enabled ? "uok-kernel" : "legacy-wrapper";
return flags.enabled ? "uok-kernel" : "standard-loop";
}
export async function runAutoLoopWithUok(args) {
const { ctx, pi, s, deps, runKernelLoop, runLegacyLoop } = args;
const { ctx, pi, s, deps, runKernelLoop, runStandardLoop } = args;
const prefs = deps.loadEffectiveSFPreferences()?.preferences;
const flags = resolveUokFlags(prefs);
const previousReport = refreshParityReport(s.basePath);
@ -73,10 +72,10 @@ export async function runAutoLoopWithUok(args) {
let status = "ok";
let error;
try {
if (flags.enabled && !flags.legacyFallback) {
if (flags.enabled) {
await runKernelLoop(ctx, pi, s, decoratedDeps);
} else {
await runLegacyLoop(ctx, pi, s, deps);
await runStandardLoop(ctx, pi, s, deps);
}
} catch (err) {
status = "error";

View file

@ -48,7 +48,6 @@ export function buildParityReport(events, sourcePath) {
const paths = {};
const statuses = {};
const criticalMismatches = [];
let fallbackInvocations = 0;
let enterEvents = 0;
let exitEvents = 0;
let totalDiffs = 0;
@ -75,13 +74,12 @@ export function buildParityReport(events, sourcePath) {
}
continue;
}
// Legacy heartbeat event
// Kernel heartbeat event
const heartbeat = event;
increment(paths, heartbeat.path);
increment(statuses, heartbeat.status);
if (heartbeat.phase === "enter") enterEvents += 1;
if (heartbeat.phase === "exit") exitEvents += 1;
if (heartbeat.path === "legacy-fallback") fallbackInvocations += 1;
if (heartbeat.status === "error") {
criticalMismatches.push(heartbeat.error ?? "parity event reported error");
}
@ -99,7 +97,6 @@ export function buildParityReport(events, sourcePath) {
paths,
statuses,
criticalMismatches,
fallbackInvocations,
enterEvents,
exitEvents,
missingExitEvents,

View file

@ -34,14 +34,14 @@ const EXPLICIT_SUBCOMMANDS = new Set([
* Detect whether the current subcommand should be auto-redirected
* to headless mode when stdout is not a TTY.
*
* Returns true when: the subcommand is "autonomous", or legacy "auto", AND stdout is piped.
* Returns true when the subcommand is "autonomous" and stdout is piped.
*/
function shouldRedirectAutoToHeadless(
subcommand: string | undefined,
stdoutIsTTY: boolean,
): boolean {
if (stdoutIsTTY) return false;
return subcommand === "auto" || subcommand === "autonomous";
return subcommand === "autonomous";
}
/**
@ -65,19 +65,12 @@ function isExplicitSubcommand(subcommand: string | undefined): boolean {
// ─── shouldRedirectAutoToHeadless ─────────────────────────────────────────
test("redirects legacy 'auto' to headless when stdout is piped", () => {
assert.ok(shouldRedirectAutoToHeadless("auto", false));
});
test("redirects 'autonomous' to headless when stdout is piped", () => {
assert.ok(shouldRedirectAutoToHeadless("autonomous", false));
});
test("does NOT redirect legacy 'auto' when stdout is a TTY", () => {
assert.ok(!shouldRedirectAutoToHeadless("auto", true));
});
test("does NOT redirect non-auto subcommands when stdout is piped", () => {
assert.ok(!shouldRedirectAutoToHeadless("auto", false));
assert.ok(!shouldRedirectAutoToHeadless("headless", false));
assert.ok(!shouldRedirectAutoToHeadless("config", false));
assert.ok(!shouldRedirectAutoToHeadless("update", false));
@ -114,7 +107,7 @@ test("identifies explicitly handled subcommands", () => {
assert.ok(isExplicitSubcommand("web"));
});
test("does NOT identify legacy 'auto' as explicit subcommand", () => {
test("does NOT identify 'auto' as explicit subcommand", () => {
assert.ok(!isExplicitSubcommand("auto"));
});

View file

@ -109,7 +109,7 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions {
options.bare = true;
}
} else if (!commandSeen) {
if (arg === "autonomous" || arg === "auto") {
if (arg === "autonomous") {
options.command = "autonomous";
options.auto = true;
} else {
@ -190,12 +190,6 @@ test("autonomous command preserves command arguments", () => {
assert.deepEqual(opts.commandArgs, ["M001", "extra-context"]);
});
test("legacy auto command normalizes to autonomous", () => {
const opts = parseHeadlessArgs(["node", "sf", "headless", "auto", "M001"]);
assert.equal(opts.command, "autonomous");
assert.deepEqual(opts.commandArgs, ["M001"]);
});
test("invalid --output-format value throws", () => {
assert.throws(
() =>