diff --git a/src/resources/extensions/mcp-client/index.ts b/src/resources/extensions/mcp-client/index.ts index 2ed6b7729..f29a46175 100644 --- a/src/resources/extensions/mcp-client/index.ts +++ b/src/resources/extensions/mcp-client/index.ts @@ -24,6 +24,7 @@ import { Client } from "@modelcontextprotocol/sdk/client"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { readFileSync, existsSync } from "node:fs"; +import { homedir } from "node:os"; import { join } from "node:path"; import { buildHttpTransportOpts } from "./auth.js"; import type { McpHttpAuthConfig } from "./auth.js"; @@ -66,9 +67,15 @@ function readConfigs(): McpServerConfig[] { const servers: McpServerConfig[] = []; const seen = new Set(); + // Search order matters: first hit wins (seen-guard below), so put + // project-local configs first — a project can override or shadow a + // globally-registered server by re-declaring the same name. + const sfHome = process.env.SF_HOME || join(homedir(), ".sf"); const configPaths = [ join(process.cwd(), ".mcp.json"), join(process.cwd(), ".sf", "mcp.json"), + join(sfHome, "mcp.json"), // global: ~/.sf/mcp.json + join(sfHome, "agent", "mcp.json"), // global: ~/.sf/agent/mcp.json (legacy alt) ]; for (const configPath of configPaths) { diff --git a/src/resources/extensions/sf/preferences.ts b/src/resources/extensions/sf/preferences.ts index 8210bf3ef..bc0672af1 100644 --- a/src/resources/extensions/sf/preferences.ts +++ b/src/resources/extensions/sf/preferences.ts @@ -12,7 +12,7 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { join, dirname, resolve } from "node:path"; import { sfRoot } from "./paths.js"; import { parse as parseYaml } from "yaml"; @@ -110,8 +110,46 @@ function legacyGlobalPreferencesPath(): string { return join(homedir(), ".pi", "agent", "sf-preferences.md"); } +/** + * Resolve the "project root" for preferences. When SF is running inside a + * git worktree (e.g. `.sf/worktrees/M003/`), project-level prefs should + * come from the MAIN worktree's `.sf/PREFERENCES.md`, not the milestone + * branch's frozen copy. Otherwise a pref change on main never reaches an + * in-flight milestone, and we saw this in practice: updating PREFERENCES + * on main had no effect until the milestone branch merged main. + * + * Strategy: read `.git` in the current worktree. If it's a file of the + * form `gitdir: `, we're in a linked worktree — resolve the + * commondir and walk up one level to reach the main worktree. Fall back + * to cwd silently for non-worktree setups (bare clones, submodules, etc. + * where the current logic is already correct). + */ +function projectPrefsRoot(): string { + const cwd = process.cwd(); + try { + const gitPath = join(cwd, ".git"); + if (!existsSync(gitPath)) return cwd; + const stat = readFileSync(gitPath, "utf-8"); + // Linked worktrees have `.git` as a FILE: "gitdir: /abs/path/to/main/.git/worktrees/NAME" + const m = /^gitdir:\s*(.+)$/m.exec(stat.trim()); + if (!m) return cwd; + const gitdir = m[1].trim(); + // gitdir looks like /.../.git/worktrees/NAME — commondir is the /.../.git/ part; + // the main worktree is the directory containing that .git dir. + // Use the `commondir` pointer file inside the linked worktree's gitdir when present. + const commondirFile = join(gitdir, "commondir"); + if (existsSync(commondirFile)) { + const rel = readFileSync(commondirFile, "utf-8").trim(); + const commondir = resolve(gitdir, rel); // usually "../.." → /.../.git + const mainRoot = dirname(commondir); // /.../ (main worktree root) + if (existsSync(join(mainRoot, ".sf"))) return mainRoot; + } + } catch { /* non-fatal — fall back to cwd */ } + return cwd; +} + function projectPreferencesPath(): string { - return join(sfRoot(process.cwd()), "preferences.md"); + return join(sfRoot(projectPrefsRoot()), "preferences.md"); } // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake. // Check uppercase as a fallback so those files aren't silently ignored. @@ -119,7 +157,7 @@ function globalPreferencesPathUppercase(): string { return join(sfHome(), "PREFERENCES.md"); } function projectPreferencesPathUppercase(): string { - return join(sfRoot(process.cwd()), "PREFERENCES.md"); + return join(sfRoot(projectPrefsRoot()), "PREFERENCES.md"); } export function getGlobalSFPreferencesPath(): string {