fix(sf): keep web autonomy on proven routes

This commit is contained in:
Mikael Hugo 2026-05-17 20:24:51 +02:00
parent 8f097f8dca
commit d8fd70e57f
11 changed files with 81 additions and 27 deletions

View file

@ -822,6 +822,18 @@ export async function selectAndApplyModel(
ctx.ui.notify(`Model ${modelId} not found, trying fallback.`, "info");
continue;
}
if (
isAutoMode &&
!allowsFreeTierAutoRoute(unitType) &&
routingConfig?.allow_free_models !== true &&
isFreeTierModelRoute(model.provider, model.id)
) {
ctx.ui.notify(
`Skipping free-tier model ${model.provider}/${model.id} for main autonomous unit ${unitType}; trying next fallback.`,
"warning",
);
continue;
}
enabledModelsResolvedCount++;
// ── Enforce operator enabledModels allowlist ──────────────────────
// Applied as the first model-level filter so disallowed providers

View file

@ -1493,7 +1493,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
const confirmed = await showConfirm(ctx, {
title: "Switch to Build mode?",
message:
"You're in Ask mode. Build mode runs autonomously with broad permissions — SF may still pause at gates or risky operations. Use Ctrl+Y for YOLO (no stops at all).",
"You're in Ask mode. Build mode runs autonomously with broad permissions — SF may still pause at gates or risky operations. Use Ctrl+Alt+Y for YOLO (no stops at all).",
confirmLabel: "Switch to Build",
declineLabel: "Stay in Ask",
});

View file

@ -574,7 +574,7 @@ export class AutoSession {
* (build + autonomous + deep + unrestricted). On deactivation, restores
* the mode that was active before YOLO was turned on.
*
* Consumer: Ctrl+Y keybinding in steerable-autonomous-extension.js.
* Consumer: Ctrl+Alt+Y keybinding in steerable-autonomous-extension.js.
* Returns the new yolo state (true = on, false = off).
*/
toggleYolo() {

View file

@ -38,6 +38,7 @@ export const BASE_RUNTIME_COMMANDS = new Set([
"terminal",
"exit",
"quit",
"stop",
]);
/**
* Top-level SF subcommands with descriptions.

View file

@ -134,7 +134,7 @@ export function registerSFCommands(pi) {
getArgumentCompletions: (prefix) =>
getSfTopLevelCommandCompletions(command.cmd, prefix),
handler: async (args, ctx) => {
// Cache this command ctx so shortcut handlers (Ctrl+Y) can fall back
// Cache this command ctx so shortcut handlers (Ctrl+Alt+Y) can fall back
// to a valid ExtensionCommandContext that has newSession().
// Import lazily to avoid a circular dep at module load time.
importExtensionModule(import.meta.url, "../auto/session.js")

View file

@ -3,7 +3,7 @@
*
* Purpose: provide Shift+Tab for run-control cycling (manual assisted
* autonomous) when the session is idle, and for steering/asking questions
* during autonomous execution (Copilot Auto style). Also integrates Ctrl+Y
* during autonomous execution (Copilot Auto style). Also integrates Ctrl+Alt+Y
* for YOLO mode (bypass git prompts).
*
* Consumer: index.js steerableAutonomousExtension(pi) on every startup.
@ -32,7 +32,7 @@ export default function steerableAutonomousExtension(api) {
}
});
// Handle key events - Shift+Tab and Ctrl+Y
// Handle key events - Shift+Tab and Ctrl+Alt+Y
api.registerShortcut("shift+tab", {
description:
"Cycle mode (Ask ↔ Build) or open steerable panel during autonomous",
@ -77,7 +77,7 @@ export default function steerableAutonomousExtension(api) {
},
});
api.registerShortcut("ctrl+y", {
api.registerShortcut("ctrl+alt+y", {
description:
"Toggle YOLO mode (build + autonomous + deep + unrestricted; bypass git prompts). If not running, starts the autonomous loop immediately.",
handler: async (ctx) => {

View file

@ -3,7 +3,7 @@
*
* Provides Shift+Tab interface for steering and asking questions
* during autonomous execution, similar to Copilot Auto.
* Also integrates Ctrl+Y for YOLO mode (bypass git prompts).
* Also integrates Ctrl+Alt+Y for YOLO mode (bypass git prompts).
*/
import { createInterface } from "node:readline";
@ -56,7 +56,7 @@ const CONTROL_CATEGORIES = [
items: [
{ key: "p", label: "Pause autonomous", action: "pause" },
{ key: "s", label: "Stop execution", action: "stop" },
{ key: "y", label: "YOLO mode (Ctrl+Y)", action: "yolo" },
{ key: "y", label: "YOLO mode (Ctrl+Alt+Y)", action: "yolo" },
{ key: "h", label: "Help/commands", action: "help" },
{ key: "esc", label: "Close panel", action: "close" },
],
@ -113,7 +113,7 @@ function renderPanel(currentStatus = "") {
// Add footer
panelContent.push("");
panelContent.push(
"\x1b[90mShift+Tab or / to open/close • Ctrl+Y for YOLO\x1b[0m",
"\x1b[90mShift+Tab or / to open/close • Ctrl+Alt+Y for YOLO\x1b[0m",
);
return renderBox(panelContent, "🎛️ Steerable Autonomous Mode");

View file

@ -208,24 +208,20 @@ function isCodebaseSearchError(details) {
*
* Consumer: `codebase_search.execute`.
*/
function buildCodebaseSearchArgs(strategy, query, scope, projectRoot) {
export function buildCodebaseSearchArgs(strategy, query, scope, projectRoot) {
// Scope-aware retriever selection: repo-root scope uses bm25+phrase (fast,
// avoids the long first-time vector index build on full workspace), while
// scoped subdirs get vector+reranking for semantic signal. Timeouts are
// sized to accommodate cold-cache embedding builds.
const { retrievers, reranking } = chooseSiftRetrievers(scope, projectRoot);
const args = [
"search",
"--strategy",
strategy,
"--retrievers",
retrievers,
"--reranking",
reranking,
"--agent",
query,
scope,
];
const args = ["search", "--strategy", strategy, "--agent", query, scope];
// Sift's agent runtime intentionally supports a narrower option surface than
// direct search; passing direct-search retriever/reranker flags makes agent
// search fail before it can return any evidence. Keep the computed defaults
// here as documentation for the direct `sift_search` tool, but do not pass
// them while `--agent` is active.
void retrievers;
void reranking;
// --verbose gives more progress info from the Rust binary when the
// operator explicitly asked for debug-level sift logging.
const siftLogLevel = (process.env.SF_SIFT_LOG_LEVEL ?? "info").toLowerCase();

View file

@ -269,6 +269,31 @@ describe("free-tier autonomous routing policy", () => {
["openrouter/qwen/qwen3-coder:free", "kimi-coding/kimi-k2.6"],
);
});
test("main_worker_final_fallback_loop_skips_explicit_free_primary", async () => {
makeEnv({
enabledModels: undefined,
prefsYaml: [
"version: 1",
"models:",
" execution:",
" model: openrouter/qwen/qwen3-coder:free",
" fallbacks:",
" - kimi-coding/kimi-k2.6",
"",
].join("\n"),
});
const notified = [];
const applied = await dispatch([FREE, PAID], notified);
assert.ok(applied !== null, "paid fallback should be applied");
assert.equal(applied.provider, "kimi-coding");
assert.equal(applied.id, "kimi-k2.6");
assert.ok(
notified.some((n) => n.msg.includes("Skipping free-tier model")),
`expected free-tier skip notification; got ${JSON.stringify(notified)}`,
);
});
});
// ── Part 2: fallback chain respects enabledModels ─────────────────────────────

View file

@ -10,11 +10,12 @@ import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it, vi } from "vitest";
import { describe, it } from "vitest";
import {
chooseSiftRetrievers,
resolveSiftSearchScopes,
} from "../code-intelligence.js";
import { buildCodebaseSearchArgs } from "../subagent/index.js";
// ── chooseSiftRetrievers unit tests ────────────────────────────────────────
@ -99,6 +100,25 @@ describe("sift-search-tool buildSiftArgs via chooseSiftRetrievers", () => {
});
});
describe("codebase_search agent args", () => {
it("does_not_pass_direct_search_retriever_flags_to_sift_agent_mode", () => {
const args = buildCodebaseSearchArgs(
"page-index-hybrid",
"crash loop classifier",
"src/resources/extensions/sf",
"/repo",
);
assert.ok(args.includes("--agent"), "agent mode must remain enabled");
assert.ok(!args.includes("--retrievers"));
assert.ok(!args.includes("--reranking"));
assert.deepEqual(args.slice(-2), [
"crash loop classifier",
"src/resources/extensions/sf",
]);
});
});
// ── ensureSiftIndexWarmup regression guard ─────────────────────────────────
//
// Warmup always passes "." (repo root) as scope. After the refactor,

View file

@ -174,8 +174,8 @@ export default function sfTui(pi) {
"Cycle permission profile (restricted→normal→trusted→unrestricted)",
handler: () => cyclePermissionProfile(ctx),
});
// Ctrl+G — open current project in $EDITOR (or notify if none)
pi.registerShortcut(Key.ctrl("g"), {
// Ctrl+Alt+E — open current project in $EDITOR (or notify if none)
pi.registerShortcut(Key.ctrlAlt("e"), {
description: "Open project root in $EDITOR",
handler: () => {
const editor = process.env.EDITOR || process.env.VISUAL;
@ -193,8 +193,8 @@ export default function sfTui(pi) {
ctx.ui.notify(`Opened ${editor} ${projectRoot() ?? "."}`, "info");
},
});
// Ctrl+T — toggle reasoning display
pi.registerShortcut(Key.ctrl("t"), {
// Ctrl+Alt+T — toggle reasoning display
pi.registerShortcut(Key.ctrlAlt("t"), {
description: "Toggle extended thinking / reasoning display",
handler: () => {
const current = getExperimentalFlag("show_reasoning");