From d8fd70e57fbeb876343786bb95ce8849791b2ec9 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 17 May 2026 20:24:51 +0200 Subject: [PATCH] fix(sf): keep web autonomy on proven routes --- .../extensions/sf/auto-model-selection.js | 12 +++++++++ src/resources/extensions/sf/auto.js | 2 +- src/resources/extensions/sf/auto/session.js | 2 +- .../extensions/sf/commands/catalog.js | 1 + src/resources/extensions/sf/commands/index.js | 2 +- .../sf/steerable-autonomous-extension.js | 6 ++--- .../sf/steerable-autonomous-panel.js | 6 ++--- src/resources/extensions/sf/subagent/index.js | 22 +++++++--------- .../sf/tests/enabled-models-fallback.test.mjs | 25 +++++++++++++++++++ .../sf/tests/sift-retriever-scope.test.mjs | 22 +++++++++++++++- src/resources/extensions/sf/ui/index.js | 8 +++--- 11 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/resources/extensions/sf/auto-model-selection.js b/src/resources/extensions/sf/auto-model-selection.js index 03a79ab50..c4aa7e9a9 100644 --- a/src/resources/extensions/sf/auto-model-selection.js +++ b/src/resources/extensions/sf/auto-model-selection.js @@ -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 diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index f08f30347..df9e88dd8 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -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", }); diff --git a/src/resources/extensions/sf/auto/session.js b/src/resources/extensions/sf/auto/session.js index c6c589dbf..2a07f369e 100644 --- a/src/resources/extensions/sf/auto/session.js +++ b/src/resources/extensions/sf/auto/session.js @@ -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() { diff --git a/src/resources/extensions/sf/commands/catalog.js b/src/resources/extensions/sf/commands/catalog.js index 5e8c8aed2..028042680 100644 --- a/src/resources/extensions/sf/commands/catalog.js +++ b/src/resources/extensions/sf/commands/catalog.js @@ -38,6 +38,7 @@ export const BASE_RUNTIME_COMMANDS = new Set([ "terminal", "exit", "quit", + "stop", ]); /** * Top-level SF subcommands with descriptions. diff --git a/src/resources/extensions/sf/commands/index.js b/src/resources/extensions/sf/commands/index.js index 828c05920..73d24dbea 100644 --- a/src/resources/extensions/sf/commands/index.js +++ b/src/resources/extensions/sf/commands/index.js @@ -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") diff --git a/src/resources/extensions/sf/steerable-autonomous-extension.js b/src/resources/extensions/sf/steerable-autonomous-extension.js index 2135ec631..7b3ef88b7 100644 --- a/src/resources/extensions/sf/steerable-autonomous-extension.js +++ b/src/resources/extensions/sf/steerable-autonomous-extension.js @@ -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) => { diff --git a/src/resources/extensions/sf/steerable-autonomous-panel.js b/src/resources/extensions/sf/steerable-autonomous-panel.js index a56fecc36..150a8c77c 100644 --- a/src/resources/extensions/sf/steerable-autonomous-panel.js +++ b/src/resources/extensions/sf/steerable-autonomous-panel.js @@ -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"); diff --git a/src/resources/extensions/sf/subagent/index.js b/src/resources/extensions/sf/subagent/index.js index 12beb7d78..0c072b4d4 100644 --- a/src/resources/extensions/sf/subagent/index.js +++ b/src/resources/extensions/sf/subagent/index.js @@ -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(); diff --git a/src/resources/extensions/sf/tests/enabled-models-fallback.test.mjs b/src/resources/extensions/sf/tests/enabled-models-fallback.test.mjs index b21d9f3ae..56379e9ae 100644 --- a/src/resources/extensions/sf/tests/enabled-models-fallback.test.mjs +++ b/src/resources/extensions/sf/tests/enabled-models-fallback.test.mjs @@ -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 ───────────────────────────── diff --git a/src/resources/extensions/sf/tests/sift-retriever-scope.test.mjs b/src/resources/extensions/sf/tests/sift-retriever-scope.test.mjs index 209adef23..46803af40 100644 --- a/src/resources/extensions/sf/tests/sift-retriever-scope.test.mjs +++ b/src/resources/extensions/sf/tests/sift-retriever-scope.test.mjs @@ -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, diff --git a/src/resources/extensions/sf/ui/index.js b/src/resources/extensions/sf/ui/index.js index ab2e4f091..8222ed3b9 100644 --- a/src/resources/extensions/sf/ui/index.js +++ b/src/resources/extensions/sf/ui/index.js @@ -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");