Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6364fbf
fix(antigravity): use global config directory for mcp_config.json
moferrin Jun 17, 2026
93dc9b3
refactor(test): rename cliMCPPath to globalMCPPath per review
moferrin Jun 17, 2026
cd3f926
refactor(antigravity): centralize global MCP config path into exporte…
moferrin Jun 22, 2026
c8467e2
refactor(antigravity): replace mutable var with GlobalMCPConfigPath f…
moferrin Jun 22, 2026
5ff2c5e
docs(antigravity): update MCP config paths to reflect new global shar…
moferrin Jun 22, 2026
a5eb6ed
fix(antigravity): check OS IDE paths and use engram tool profile
moferrin Jun 24, 2026
2beb3b9
test(antigravity): add missing checks from review feedback
moferrin Jun 24, 2026
5168d2c
test(antigravity): isolate Windows APPDATA in variant dir test
moferrin Jun 24, 2026
e81f36a
fix(antigravity): derive Engram plugin path from active variant
moferrin Jul 8, 2026
0bc0228
docs(antigravity): align spec and agents docs with variant-aware paths
moferrin Jul 8, 2026
199d06d
test(engram): sandbox APPDATA on Windows for Antigravity inject test
moferrin Jul 8, 2026
fac7496
test(engram): sandbox APPDATA in remaining Antigravity tests
moferrin Jul 8, 2026
bb37720
fix(golden): add missing runtime import for APPDATA sandbox
moferrin Jul 9, 2026
cc8eab8
docs(openspec): align Antigravity Engram invocation with --tools=agent
moferrin Jul 9, 2026
4a7a90a
fix(system): resolve Antigravity variant in config scanner
moferrin Jul 9, 2026
7a1e992
fix(antigravity): use native variant paths and fix test flakes
moferrin Jul 9, 2026
9d29458
test(antigravity): align adapter tests with new variant paths
moferrin Jul 9, 2026
c178ed9
test(engram): clean up obsolete APPDATA mocks for Antigravity
moferrin Jul 9, 2026
016391c
test(engram): restore APPDATA mock for VSCode test
moferrin Jul 9, 2026
9921994
test(components): remove obsolete APPDATA mock for Antigravity in gol…
moferrin Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,12 @@ Kiro uses native custom agents in `~/.kiro/agents/`. `gentle-ai` writes phase ag

### Antigravity

- Skills at `~/.gemini/antigravity/skills/` (native Antigravity feature)
- MCP config at `~/.gemini/antigravity/mcp_config.json`
- Skills at `<variantDir>/skills/` where `<variantDir>` is the resolved Antigravity variant (IDE β†’ Desktop β†’ `~/.gemini/antigravity-cli/` fallback)
- Engram plugin at `<variantDir>/plugins/gentle-ai-engram/`
- MCP config at `~/.gemini/config/mcp_config.json` (shared global location)
- System prompt appended to `~/.gemini/GEMINI.md` (shared with Gemini CLI β€” collision check warns if both are installed)
- Mission Control handles built-in sub-agent delegation (Browser, Terminal) automatically
- Settings managed via the IDE's Agent settings UI, not via `settings.json`
- Settings initialized via `<variantDir>/settings.json` but primarily managed via the IDE's Agent settings UI

### Kimi Code

Expand Down
23 changes: 18 additions & 5 deletions internal/agents/antigravity/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import (
"github.com/gentleman-programming/gentle-ai/internal/system"
)

// GlobalMCPConfigPath returns the absolute path to the shared global MCP
// configuration file under homeDir. Antigravity uses this single location
// across CLI, Desktop, and IDE variants.
func GlobalMCPConfigPath(homeDir string) string {
return filepath.Join(homeDir, ".gemini", "config", "mcp_config.json")
}

type statResult struct {
isDir bool
err error
Expand All @@ -24,13 +31,19 @@ func NewAdapter() *Adapter {
}
}

// antigravityVariantDir returns the resolved variant directory under ~/.gemini.
// Prefers "antigravity-desktop" when it exists, falls back to "antigravity-cli".
// antigravityVariantDir returns the resolved variant directory.
// Checks OS-specific IDE paths, then legacy desktop, falls back to CLI.
func (a *Adapter) antigravityVariantDir(homeDir string) string {
desktop := filepath.Join(homeDir, ".gemini", "antigravity-desktop")
if stat := a.statPath(desktop); stat.err == nil {
ideDir := filepath.Join(homeDir, ".gemini", "antigravity-ide")
if stat := a.statPath(ideDir); stat.err == nil && stat.isDir {
return ideDir
}

desktop := filepath.Join(homeDir, ".gemini", "antigravity")
if stat := a.statPath(desktop); stat.err == nil && stat.isDir {
return desktop
}

return filepath.Join(homeDir, ".gemini", "antigravity-cli")
}

Expand Down Expand Up @@ -105,7 +118,7 @@ func (a *Adapter) MCPStrategy() model.MCPStrategy {
// --- MCP ---

func (a *Adapter) MCPConfigPath(homeDir string, _ string) string {
return filepath.Join(a.antigravityVariantDir(homeDir), "mcp_config.json")
return GlobalMCPConfigPath(homeDir)
}
Comment on lines 120 to 122

// --- Optional capabilities ---
Expand Down
43 changes: 25 additions & 18 deletions internal/agents/antigravity/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,42 +31,47 @@ func makeStatFn(existingDirs ...string) func(string) statResult {
func TestAntigravityVariantDir(t *testing.T) {
home := t.TempDir()
cliDir := filepath.Join(home, ".gemini", "antigravity-cli")
desktopDir := filepath.Join(home, ".gemini", "antigravity-desktop")
desktopDir := filepath.Join(home, ".gemini", "antigravity")
ideDir := filepath.Join(home, ".gemini", "antigravity-ide")

tests := []struct {
name string
existingDirs []string
wantSuffix string // last two path segments expected
wantDir string
}{
{
name: "only CLI dir exists resolves to CLI",
existingDirs: []string{cliDir},
wantSuffix: filepath.Join(".gemini", "antigravity-cli"),
wantDir: cliDir,
},
{
name: "only Desktop dir exists resolves to Desktop",
existingDirs: []string{desktopDir},
wantSuffix: filepath.Join(".gemini", "antigravity-desktop"),
wantDir: desktopDir,
},
{
name: "both exist prefers Desktop",
existingDirs: []string{cliDir, desktopDir},
wantSuffix: filepath.Join(".gemini", "antigravity-desktop"),
name: "only IDE dir exists resolves to IDE",
existingDirs: []string{ideDir},
wantDir: ideDir,
},
{
name: "all exist prefers IDE over Desktop over CLI",
existingDirs: []string{cliDir, desktopDir, ideDir},
wantDir: ideDir,
},
{
name: "neither exists falls back to CLI",
existingDirs: []string{},
wantSuffix: filepath.Join(".gemini", "antigravity-cli"),
wantDir: cliDir,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Adapter{statPath: makeStatFn(tt.existingDirs...)}
got := a.antigravityVariantDir(home)
want := filepath.Join(home, tt.wantSuffix)
if got != want {
t.Fatalf("antigravityVariantDir() = %q, want %q", got, want)
if got != tt.wantDir {
t.Fatalf("antigravityVariantDir() = %q, want %q", got, tt.wantDir)
}
})
}
Expand All @@ -77,7 +82,7 @@ func TestAntigravityVariantDir(t *testing.T) {
func TestDetect(t *testing.T) {
home := t.TempDir()
cliDir := filepath.Join(home, ".gemini", "antigravity-cli")
desktopDir := filepath.Join(home, ".gemini", "antigravity-desktop")
desktopDir := filepath.Join(home, ".gemini", "antigravity")

tests := []struct {
name string
Expand Down Expand Up @@ -174,14 +179,15 @@ func TestConfigPathsCLIOnly(t *testing.T) {
if got := a.SettingsPath(home); got != filepath.Join(cliDir, "settings.json") {
t.Fatalf("SettingsPath() = %q, want %q", got, filepath.Join(cliDir, "settings.json"))
}
if got := a.MCPConfigPath(home, "ctx7"); got != filepath.Join(cliDir, "mcp_config.json") {
t.Fatalf("MCPConfigPath() = %q, want %q", got, filepath.Join(cliDir, "mcp_config.json"))
wantMCP := GlobalMCPConfigPath(home)
if got := a.MCPConfigPath(home, "ctx7"); got != wantMCP {
t.Fatalf("MCPConfigPath() = %q, want %q", got, wantMCP)
}
}

func TestConfigPathsDesktopOnly(t *testing.T) {
home := t.TempDir()
desktopDir := filepath.Join(home, ".gemini", "antigravity-desktop")
desktopDir := filepath.Join(home, ".gemini", "antigravity")
a := &Adapter{statPath: makeStatFn(desktopDir)}

if got := a.GlobalConfigDir(home); got != desktopDir {
Expand All @@ -193,15 +199,16 @@ func TestConfigPathsDesktopOnly(t *testing.T) {
if got := a.SettingsPath(home); got != filepath.Join(desktopDir, "settings.json") {
t.Fatalf("SettingsPath() = %q, want %q", got, filepath.Join(desktopDir, "settings.json"))
}
if got := a.MCPConfigPath(home, "ctx7"); got != filepath.Join(desktopDir, "mcp_config.json") {
t.Fatalf("MCPConfigPath() = %q, want %q", got, filepath.Join(desktopDir, "mcp_config.json"))
wantMCP := GlobalMCPConfigPath(home)
if got := a.MCPConfigPath(home, "ctx7"); got != wantMCP {
t.Fatalf("MCPConfigPath() = %q, want %q", got, wantMCP)
}
}

func TestConfigPathsBothExistPrefersDesktop(t *testing.T) {
home := t.TempDir()
cliDir := filepath.Join(home, ".gemini", "antigravity-cli")
desktopDir := filepath.Join(home, ".gemini", "antigravity-desktop")
desktopDir := filepath.Join(home, ".gemini", "antigravity")
a := &Adapter{statPath: makeStatFn(cliDir, desktopDir)}

if got := a.GlobalConfigDir(home); got != desktopDir {
Expand Down
4 changes: 4 additions & 0 deletions internal/cli/run_engram_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -214,6 +215,9 @@ func TestRunInstallBetaEngramUsesMainGoInstallAndInstalledBinary(t *testing.T) {
home := t.TempDir()
gobin := filepath.Join(home, "go-bin")
betaEngram := filepath.Join(gobin, "engram")
if runtime.GOOS == "windows" {
betaEngram += ".exe"
}

restoreCommand := runCommand
restoreLookPath := cmdLookPath
Expand Down
6 changes: 5 additions & 1 deletion internal/cli/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,11 @@ func RenderSyncReport(result SyncResult) string {

if len(result.ChangedFiles) > 0 {
for _, path := range result.ChangedFiles {
fmt.Fprintf(&b, " - %s\n", path)
suffix := ""
if strings.HasSuffix(path, filepath.Join(".gemini", "config", "mcp_config.json")) {
suffix = " (shared global configuration)"
}
fmt.Fprintf(&b, " - %s%s\n", path, suffix)
}
}

Expand Down
11 changes: 3 additions & 8 deletions internal/components/engram/inject.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ func engramOverlayJSON(agentID model.AgentID, cmd string) []byte {
}
} else {
args := []string{"mcp", "--tools=agent"}
if agentID == model.AgentAntigravity {
// Antigravity should launch the default Engram MCP server without
// narrowing the exposed tool set.
args = []string{"mcp"}
}
cfg = map[string]any{
"mcpServers": map[string]any{
"engram": map[string]any{
Expand Down Expand Up @@ -250,8 +245,8 @@ func ensureJSONFileIfMissing(path string) (filemerge.WriteResult, error) {
return filemerge.WriteFileAtomic(path, []byte("{}\n"), 0o644)
}

func installAntigravityEngramPlugin(homeDir, engramCommand string) (bool, []string, error) {
pluginDir := filepath.Join(homeDir, ".gemini", "antigravity-cli", "plugins", "gentle-ai-engram")
func installAntigravityEngramPlugin(variantDir, engramCommand string) (bool, []string, error) {
pluginDir := filepath.Join(variantDir, "plugins", "gentle-ai-engram")
files := make([]string, 0, 3)
changed := false

Expand Down Expand Up @@ -360,7 +355,7 @@ func injectWithOptions(configHomeDir, promptDir string, adapter agents.Adapter,
changed = changed || settingsWrite.Changed
files = append(files, adapter.SettingsPath(configHomeDir))

pluginChanged, pluginFiles, pluginErr := installAntigravityEngramPlugin(configHomeDir, engramCommand)
pluginChanged, pluginFiles, pluginErr := installAntigravityEngramPlugin(adapter.GlobalConfigDir(configHomeDir), engramCommand)
if pluginErr != nil {
return InjectionResult{}, pluginErr
}
Expand Down
59 changes: 51 additions & 8 deletions internal/components/engram/inject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,10 @@ func TestInjectGeminiToolsFlagPresent(t *testing.T) {
}
}

func TestInjectAntigravityWritesMCPToCLIConfig(t *testing.T) {
func TestInjectAntigravityWritesMCPToGlobalConfig(t *testing.T) {
home := t.TempDir()


result, err := Inject(home, antigravityAdapter())
if err != nil {
t.Fatalf("Inject(antigravity) error = %v", err)
Expand All @@ -568,17 +569,22 @@ func TestInjectAntigravityWritesMCPToCLIConfig(t *testing.T) {
t.Fatalf("Inject(antigravity) changed = false")
}

cliMCPPath := filepath.Join(home, ".gemini", "antigravity-cli", "mcp_config.json")
content, err := os.ReadFile(cliMCPPath)
globalMCPPath := antigravity.GlobalMCPConfigPath(home)
content, err := os.ReadFile(globalMCPPath)
if err != nil {
t.Fatalf("ReadFile(%q) error = %v", cliMCPPath, err)
t.Fatalf("ReadFile(%q) error = %v", globalMCPPath, err)
}

oldCLIPath := filepath.Join(home, ".gemini", "antigravity-cli", "mcp_config.json")
if _, err := os.Stat(oldCLIPath); !os.IsNotExist(err) {
t.Fatalf("legacy CLI MCP path %q should not be written for antigravity; stat err = %v", oldCLIPath, err)
}
text := string(content)
if !strings.Contains(text, `"args": [`) || !strings.Contains(text, `"mcp"`) {
t.Fatalf("Antigravity MCP config must launch Engram MCP; got:\n%s", text)
}
if strings.Contains(text, `--tools=`) {
t.Fatalf("Antigravity should use Engram's default MCP invocation without tool-profile flags; got:\n%s", text)
if !strings.Contains(text, `"--tools=agent"`) {
t.Fatalf("Antigravity should use Engram's MCP invocation with --tools=agent; got:\n%s", text)
}

pluginPath := filepath.Join(home, ".gemini", "antigravity-cli", "plugins", "gentle-ai-engram", "plugin.json")
Expand All @@ -592,8 +598,8 @@ func TestInjectAntigravityWritesMCPToCLIConfig(t *testing.T) {
t.Fatalf("ReadFile(%q) error = %v", pluginMCPPath, err)
}
pluginMCPText := string(pluginMCPContent)
if !strings.Contains(pluginMCPText, `"mcp"`) || strings.Contains(pluginMCPText, `--tools=`) {
t.Fatalf("Antigravity Engram plugin MCP config should expose default Engram MCP tools; got:\n%s", pluginMCPText)
if !strings.Contains(pluginMCPText, `"mcp"`) || !strings.Contains(pluginMCPText, `"--tools=agent"`) {
t.Fatalf("Antigravity Engram plugin MCP config should expose Engram MCP tools via --tools=agent; got:\n%s", pluginMCPText)
}

hooksPath := filepath.Join(home, ".gemini", "antigravity-cli", "plugins", "gentle-ai-engram", "hooks.json")
Expand Down Expand Up @@ -626,9 +632,46 @@ func TestInjectAntigravityWritesMCPToCLIConfig(t *testing.T) {
}
}

func TestInjectAntigravityIDEVariantReceivesPlugin(t *testing.T) {
home := t.TempDir()

// Create the IDE path on disk so the adapter's real os.Stat resolves to the IDE variant.
ideDir := filepath.Join(home, ".gemini", "antigravity-ide")
if err := os.MkdirAll(ideDir, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", ideDir, err)
}

result, err := Inject(home, antigravity.NewAdapter())
if err != nil {
t.Fatalf("Inject(antigravity-IDE) error = %v", err)
}
if !result.Changed {
t.Fatalf("Inject(antigravity-IDE) changed = false")
}

// Plugin files must land inside the IDE variant directory.
for _, rel := range []string{
filepath.Join("plugins", "gentle-ai-engram", "plugin.json"),
filepath.Join("plugins", "gentle-ai-engram", "mcp_config.json"),
filepath.Join("plugins", "gentle-ai-engram", "hooks.json"),
} {
p := filepath.Join(ideDir, rel)
if _, err := os.Stat(p); err != nil {
t.Fatalf("expected plugin file at IDE variant path %q; stat error = %v", p, err)
}
}

// Legacy CLI directory must NOT be created.
legacyCLI := filepath.Join(home, ".gemini", "antigravity-cli")
if _, err := os.Stat(legacyCLI); !os.IsNotExist(err) {
t.Fatalf("legacy CLI directory %q should not exist when IDE variant is active; stat err = %v", legacyCLI, err)
}
Comment on lines +664 to +668
}

func TestInjectAntigravityInitializesEmptySettingsWhenGeminiMissing(t *testing.T) {
home := t.TempDir()


first, err := Inject(home, antigravityAdapter())
if err != nil {
t.Fatalf("Inject(antigravity) first error = %v", err)
Expand Down
6 changes: 4 additions & 2 deletions internal/components/golden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,8 @@ func TestGoldenPersona_Antigravity_Gentleman(t *testing.T) {
func TestGoldenEngram_Antigravity(t *testing.T) {
home := t.TempDir()



engram.SetLookPathForTest(t, "/opt/homebrew/bin/engram", "")

result, err := engram.Inject(home, antigravityAdapter())
Expand All @@ -896,8 +898,8 @@ func TestGoldenEngram_Antigravity(t *testing.T) {
t.Fatalf("engram.Inject(antigravity) changed = false")
}

// MCP config written to ~/.gemini/antigravity-cli/mcp_config.json.
mcpJSON := readTestFile(t, filepath.Join(home, ".gemini", "antigravity-cli", "mcp_config.json"))
// MCP config written to ~/.gemini/config/mcp_config.json.
mcpJSON := readTestFile(t, antigravity.GlobalMCPConfigPath(home))
assertGolden(t, "engram-antigravity-mcp.golden", mcpJSON)
Comment on lines +901 to 903

// GEMINI.md must contain the engram-protocol section.
Expand Down
22 changes: 21 additions & 1 deletion internal/system/config_scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func knownAgentConfigDirs(homeDir string) []ConfigState {
{Agent: "cursor", Path: filepath.Join(homeDir, ".cursor")},
{Agent: "vscode-copilot", Path: vscodeCopilotGlobalConfigDir(homeDir)},
{Agent: "codex", Path: filepath.Join(homeDir, ".codex")},
{Agent: "antigravity", Path: filepath.Join(homeDir, ".gemini", "antigravity-cli")},
{Agent: "antigravity", Path: antigravityConfigDir(homeDir)},
{Agent: "windsurf", Path: filepath.Join(homeDir, ".codeium", "windsurf")},
{Agent: "kimi", Path: filepath.Join(homeDir, ".kimi")},
{Agent: "qwen-code", Path: filepath.Join(homeDir, ".qwen")},
Expand All @@ -48,6 +48,26 @@ func knownAgentConfigDirs(homeDir string) []ConfigState {
}
}

// antigravityConfigDir mirrors antigravity.Adapter.antigravityVariantDir
// without importing the agents package (import cycle). Returns the first
// existing variant directory: IDE β†’ Desktop β†’ CLI fallback.
//
// TODO(follow-up): remove this shim once the system ← agents import cycle is
// resolved and ScanConfigs delegates directly to agents.DiscoverInstalled.
func antigravityConfigDir(homeDir string) string {
ideDir := filepath.Join(homeDir, ".gemini", "antigravity-ide")
if info, err := os.Stat(ideDir); err == nil && info.IsDir() {
return ideDir
}

desktop := filepath.Join(homeDir, ".gemini", "antigravity")
if info, err := os.Stat(desktop); err == nil && info.IsDir() {
return desktop
}

return filepath.Join(homeDir, ".gemini", "antigravity-cli")
}

// vscodeCopilotGlobalConfigDir returns ~/.copilot, the GlobalConfigDir used by
// the vscode-copilot adapter across all platforms. The vscode adapter's
// SystemPromptDir and SettingsPath are OS-dependent, but GlobalConfigDir is not.
Expand Down
Loading