diff --git a/.sf/preferences.yaml b/.sf/preferences.yaml index 71fc90273..a8b72bc65 100644 --- a/.sf/preferences.yaml +++ b/.sf/preferences.yaml @@ -19,3 +19,11 @@ custom_instructions: [] models: {} skill_discovery: {} auto_supervisor: {} +# Solo-mode git defaults: sf commits + pushes without operator confirmation +# during autonomous mode. Matches MODE_DEFAULTS.solo from preferences-types.js. +git: + auto_push: true + push_branches: false + pre_merge_check: auto + merge_strategy: squash + isolation: none diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.js b/src/resources/extensions/sf/bootstrap/register-hooks.js index 0943b4ce6..a7aac93b5 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.js +++ b/src/resources/extensions/sf/bootstrap/register-hooks.js @@ -538,6 +538,18 @@ export function registerHooks(pi, ecosystemHandlers = []) { } catch { /* non-fatal — codex catalog refresh must never block session start */ } + // Pre-warm the Sift search index so the first agent query in this + // session doesn't pay the cold-build cost. Sift indexes lazily on + // first `sift search` invocation per cache key. Fires a cheap + // detached search against the project root; the actual index build + // runs in parallel with the rest of session_start and is ready by + // the time an agent reaches for the search-tool. + try { + const { prewarmSiftIndex } = await import("../sift-prewarm.js"); + prewarmSiftIndex(process.cwd()).catch(() => {}); + } catch { + /* non-fatal — sift prewarm must never block session start */ + } // Audit benchmark coverage — compare the dispatchable model set // (catalog ∩ user policy) against the static benchmark file and write // ~/.sf/benchmark-coverage.json. Surfaces models routed via /v1/models diff --git a/src/resources/extensions/sf/sift-prewarm.js b/src/resources/extensions/sf/sift-prewarm.js new file mode 100644 index 000000000..70b8d13e5 --- /dev/null +++ b/src/resources/extensions/sf/sift-prewarm.js @@ -0,0 +1,86 @@ +/** + * sift-prewarm.js — fire-and-forget Sift index warmup. + * + * Purpose: Sift (the Rust search binary at ~/.cargo/bin/sift) builds its + * index lazily on first `sift search` invocation per cache key. In an + * SF session the first real Sift query — which usually happens deep + * inside an execute-task unit when an agent needs to look up a symbol + * or pattern — pays the full cold-index build cost (can be tens of + * seconds on a large repo). Subsequent queries hit a warm cache and + * are fast. + * + * This module fires an inexpensive `sift search` against the project + * root at SF session_start, fully detached and stdio-ignored, so the + * index build happens in parallel with the rest of session startup. + * By the time an agent actually needs Sift, the index is warm. + * + * Cheapest possible search: + * - --retrievers bm25 (no embedding model load, no reranker) + * - --reranking none + * - --limit 1 (don't waste cycles materializing results) + * + * Failures are silently swallowed: if `sift` isn't installed, the + * binary errors, or the spawn fails, SF carries on as before — Sift + * tooling already handles "sift unavailable" gracefully elsewhere. + * + * Consumer: bootstrap/register-hooks.js session_start handler, plus + * the autonomous loop's periodic re-warm hook (TBD if added). + */ +import { spawn } from "node:child_process"; + +/** + * Spawn a Sift warmup search against basePath. Detached + stdio-ignored + * so it does not hold the parent SF process. Returns a Promise that + * resolves on spawn-error or process-exit; never rejects. + * + * @param {string} basePath — repo root to warm + * @returns {Promise<{started: boolean, reason?: string}>} + */ +export function prewarmSiftIndex(basePath) { + return new Promise((resolve) => { + let proc; + try { + proc = spawn( + "sift", + [ + "search", + "--json", + "--limit", + "1", + "--retrievers", + "bm25", + "--reranking", + "none", + "--retriever-timeout-ms", + "60000", + basePath, + "sf-prewarm-index", + ], + { + cwd: basePath, + stdio: "ignore", + detached: true, + }, + ); + } catch (err) { + resolve({ + started: false, + reason: + err && typeof err === "object" && "message" in err + ? String(err.message) + : String(err), + }); + return; + } + // Detach so SF process can exit without waiting on the warmup. + try { + proc.unref(); + } catch { + // best-effort + } + proc.on("error", (err) => + resolve({ started: false, reason: String(err.message ?? err) }), + ); + proc.on("exit", () => resolve({ started: true })); + }); +}