Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 5 additions & 8 deletions internal/agents/kimi/adapter.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Package kimi provides Kimi Code CLI agent integration.
//
// Integration Note:
// This adapter natively relies on Astral's `uv` package manager
// (`uv tool install kimi-cli`) to securely download and run Kimi CLI,
// This adapter natively relies on Astral's `uv` package manager
// (`uv tool install kimi-cli`) to securely download and run Kimi CLI,
// avoiding upstream's pipe-to-shell bootstrap scripts.
package kimi

Expand Down Expand Up @@ -243,7 +243,6 @@ Skills root:
"%s"`, gentlemanYaml, skillsRoot)
}


// --- Helpers ---

func defaultStat(path string) statResult {
Expand Down Expand Up @@ -272,7 +271,7 @@ func binaryName() string {
}

// BootstrapTemplate ensures the base KIMI.md template exists in the agent's config directory.
// It is used by the installation pipeline to guarantee that modular components
// It is used by the installation pipeline to guarantee that modular components
// (SDD, Engram) can be included even if the Persona component is not installed.
func (a *Adapter) BootstrapTemplate(homeDir string) error {
kimiDir := a.GlobalConfigDir(homeDir)
Expand All @@ -281,9 +280,9 @@ func (a *Adapter) BootstrapTemplate(homeDir string) error {
}

skeletonPath := a.SystemPromptFile(homeDir)

// We always write the skeleton to ensure any missing includes are restored.
// Since KIMI.md is the 'router' for modular Jinja components, it should
// Since KIMI.md is the 'router' for modular Jinja components, it should
// remain managed by the framework.
content := assets.MustRead("kimi/KIMI.md")
if _, err := filemerge.WriteFileAtomic(skeletonPath, []byte(content), 0o644); err != nil {
Expand All @@ -301,5 +300,3 @@ func (a *Adapter) BootstrapTemplate(homeDir string) error {

return nil
}


18 changes: 7 additions & 11 deletions internal/agents/kimi/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,6 @@ func TestAdapter_Detect_FallbackPaths(t *testing.T) {
t.Fatal(err)
}



a := &Adapter{
lookPath: func(string) (string, error) {
return "", os.ErrNotExist // Not in PATH
Expand Down Expand Up @@ -266,27 +264,27 @@ func TestAdapter_PostInstallMessage(t *testing.T) {
if tt.os == "windows" {
homeDir = `C:\Users\test`
}

msg := a.PostInstallMessage(homeDir)

// Construct expected path to verify against quoted output
gentlemanYaml := filepath.Join(homeDir, ".kimi", "agents", "gentleman.yaml")

// Normalize the expected string to the current host's separator.
// Since the code uses filepath.Join, it will use \ on Windows and / on Linux.
// The test should expect the host's actual separator if we want it to PASS
// while running on that host.
normalizedExpected := filepath.FromSlash(tt.expected)

// On Windows, if we are simulating we want backslashes.
// If we are on Windows and testing 'Unix paths' case, it will fail because
// If we are on Windows and testing 'Unix paths' case, it will fail because
// the code (running on Windows) used \. This is expected.
// We skip the cross-platform check if it contradicts the host's logic,
// We skip the cross-platform check if it contradicts the host's logic,
// or we only check the one matching the current host.
// On Windows, if we are simulating we want backslashes.
// If we are on Windows and testing 'Unix paths' case, it will fail because
// If we are on Windows and testing 'Unix paths' case, it will fail because
// the code (running on Windows) used \. This is expected.
// We skip the cross-platform check if it contradicts the host's logic,
// We skip the cross-platform check if it contradicts the host's logic,
// or we only check the one matching the current host.
if (runtime.GOOS == "windows" && tt.os == "windows") || (runtime.GOOS != "windows" && tt.os == "linux") {
// Verify path is present
Expand All @@ -302,5 +300,3 @@ func TestAdapter_PostInstallMessage(t *testing.T) {
})
}
}


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 @@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -600,6 +601,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
21 changes: 17 additions & 4 deletions internal/components/engram/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1267,7 +1267,7 @@ func TestEngramGoInstallFromMain_UsesGoEnvForBinDir(t *testing.T) {
t.Fatalf("engramGoInstallFromMain: unexpected error: %v", err)
}

wantDir := fakeInstallDir
wantDir := filepath.FromSlash(fakeInstallDir)
gotDir := filepath.Dir(binaryPath)
if gotDir != wantDir {
t.Errorf("binary dir = %q, want %q (from go env GOBIN)", gotDir, wantDir)
Expand All @@ -1278,9 +1278,22 @@ func TestEngramGoInstallFromMain_BypassesPublicGoProxy(t *testing.T) {
binDir := t.TempDir()
goPath := filepath.Join(binDir, "go")
recordPath := filepath.Join(t.TempDir(), "go-env.txt")
fakeGo := filepath.Join(binDir, "go")
script := "#!/usr/bin/env bash\n" +
"printf 'GONOSUMDB=%s\\nGOPRIVATE=%s\\nGONOPROXY=%s\\n' \"${GONOSUMDB:-}\" \"${GOPRIVATE:-}\" \"${GONOPROXY:-}\" > \"$GO_ENV_RECORD\"\n"

var fakeGo string
var script string
if runtime.GOOS == "windows" {
fakeGo = filepath.Join(binDir, "go.bat")
script = "@echo off\n" +
"(\n" +
"echo GONOSUMDB=%GONOSUMDB%\n" +
"echo GOPRIVATE=%GOPRIVATE%\n" +
"echo GONOPROXY=%GONOPROXY%\n" +
") > \"%GO_ENV_RECORD%\"\n"
} else {
fakeGo = filepath.Join(binDir, "go")
script = "#!/usr/bin/env bash\n" +
"printf 'GONOSUMDB=%s\\nGOPRIVATE=%s\\nGONOPROXY=%s\\n' \"${GONOSUMDB:-}\" \"${GOPRIVATE:-}\" \"${GONOPROXY:-}\" > \"$GO_ENV_RECORD\"\n"
}
if err := os.WriteFile(fakeGo, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
Expand Down
17 changes: 13 additions & 4 deletions internal/components/gga/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gga
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -119,6 +120,10 @@ func TestBuildConfigDifferentProviders(t *testing.T) {
func TestInjectWritesConfigAndAgents(t *testing.T) {
home := t.TempDir()

if runtime.GOOS == "windows" {
t.Setenv("APPDATA", filepath.Join(home, "AppData", "Roaming"))
}

result, err := Inject(home, []model.AgentID{model.AgentClaudeCode})
if err != nil {
t.Fatalf("Inject() error = %v", err)
Expand Down Expand Up @@ -163,6 +168,10 @@ func TestInjectWritesConfigAndAgents(t *testing.T) {
func TestInjectIsIdempotent(t *testing.T) {
home := t.TempDir()

if runtime.GOOS == "windows" {
t.Setenv("APPDATA", filepath.Join(home, "AppData", "Roaming"))
}

first, err := Inject(home, []model.AgentID{model.AgentOpenCode})
if err != nil {
t.Fatalf("Inject() first error = %v", err)
Expand Down Expand Up @@ -238,10 +247,10 @@ func TestGGAConfigDirDarwin(t *testing.T) {

func TestGGAConfigDirWindows(t *testing.T) {
tests := []struct {
name string
homeDir string
appDataEnv string
wantSuffix string
name string
homeDir string
appDataEnv string
wantSuffix string
}{
{
name: "APPDATA set to standard roaming path",
Expand Down
4 changes: 3 additions & 1 deletion internal/components/permissions/inject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,6 @@ func TestInjectCodexWritesGentleDevPermissionsProfile(t *testing.T) {
`"~/.gitconfig" = "read"`,
`"~/.local/state/nix/profiles/home-manager/home-path" = "read"`,
`"~/.nix-profile" = "read"`,
`"/nix/store" = "read"`,
`":tmpdir" = "write"`,
`":slash_tmp" = "write"`,
`glob_scan_max_depth = 6`,
Expand All @@ -396,6 +395,9 @@ func TestInjectCodexWritesGentleDevPermissionsProfile(t *testing.T) {
`[permissions.gentle-dev.workspace_roots]`,
`"~" = true`,
}
if codexPermissionsGOOS != "windows" {
wantSubstrings = append(wantSubstrings, `"/nix/store" = "read"`)
}
for _, want := range wantSubstrings {
if !strings.Contains(text, want) {
t.Fatalf("config.toml missing %q; got:\n%s", want, text)
Expand Down
2 changes: 1 addition & 1 deletion internal/components/uninstall/cleaners_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ func TestReadManagedFile_RejectsSymlink(t *testing.T) {
}
link := filepath.Join(dir, "link.json")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("Symlink() error = %v", err)
t.Skipf("skipping symlink test; symlink creation failed (likely due to missing privileges on Windows): %v", err)
}

_, err := readManagedFile(link)
Expand Down
2 changes: 1 addition & 1 deletion internal/model/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ func ModelCapability(modelID string) string {
}
}
return "capable"
}
}
10 changes: 5 additions & 5 deletions internal/model/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "testing"

func TestModelCapability(t *testing.T) {
tests := []struct {
modelID string
modelID string
wantCapability string
}{
{"gemini-3-flash", "small"},
Expand All @@ -13,9 +13,9 @@ func TestModelCapability(t *testing.T) {
{"claude-sonnet-4", "capable"},
{"gpt-4o", "capable"},
{"", "capable"},
{"GEMINI-3-FLASH", "small"}, // case-insensitive
{"GPT-4O-MINI", "small"}, // case-insensitive
{"Claude-Haiku", "small"}, // case-insensitive
{"GEMINI-3-FLASH", "small"}, // case-insensitive
{"GPT-4O-MINI", "small"}, // case-insensitive
{"Claude-Haiku", "small"}, // case-insensitive
{"anthropic/claude-haiku-3-5", "small"},
{"openai/gpt-4o-mini-2024-07-18", "small"},
{"google/gemini-2.0-flash", "small"},
Expand All @@ -34,4 +34,4 @@ func TestModelCapability(t *testing.T) {
}
})
}
}
}
2 changes: 1 addition & 1 deletion internal/model/codex_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ var codexPresetMatrix = map[CodexPresetKey]map[string]CodexCarrilDefault{
},
}


// CodexOrchestratorAssignment is the explicit top-level Codex session model
// selected by a Gentle AI preset. It is separate from delegated SDD carriles.
type CodexOrchestratorAssignment struct {
Expand Down Expand Up @@ -197,6 +196,7 @@ type CodexTierGroup struct {
// resolveProfileAssignments agree on the same canonical tier values:
//
// These efforts are Gentle AI workload policy, not Codex defaults.
//
// Carril LowCost Recommended Powerful
// sdd-strong medium medium high
// sdd-mid medium medium high
Expand Down
70 changes: 35 additions & 35 deletions internal/model/selection.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
package model

type Selection struct {
Agents []AgentID
Components []ComponentID
Skills []SkillID
Persona PersonaID
Preset PresetID
SDDMode SDDModeID
SDDProfileStrategy SDDProfileStrategyID
StrictTDD bool
CodexMultiAgent bool // deprecated: Codex now always writes features.multi_agent = true; retained for state/back-compat
ModelAssignments map[string]ModelAssignment // key = sub-agent name (e.g., "sdd-init")
ClaudeModelAssignments map[string]ClaudeModelAlias // key = phase name; value = fable|opus|sonnet|haiku
ClaudePhaseAssignments map[string]ClaudePhaseAssignment // key = phase name; value = Claude model+effort
KiroModelAssignments map[string]KiroModelAlias // key = phase name; value = Kiro-native model alias
CodexModelAssignments map[string]CodexEffort // key = phase name; value = low|medium|high|xhigh
CodexOrchestratorAssignment *CodexOrchestratorAssignment // non-nil = apply curated top-level Codex model/effort
ClearCodexOrchestratorAssignment bool // true = clear persisted curated assignment while preserving config.toml
CodexCarrilModelAssignments map[string]string // key = carril profile (sdd-strong|sdd-mid|sdd-cheap); value = model id
CodexPhaseModelAssignments map[string]string // key = phase name; value = model id (Custom per-phase picker only)
Profiles []Profile // named SDD profiles to generate/update during sync
OpenCodePlugins []OpenCodeCommunityPluginID // optional community OpenCode TUI plugins
CommunityTools []CommunityToolID // optional cross-agent community tools/plugins
Agents []AgentID
Components []ComponentID
Skills []SkillID
Persona PersonaID
Preset PresetID
SDDMode SDDModeID
SDDProfileStrategy SDDProfileStrategyID
StrictTDD bool
CodexMultiAgent bool // deprecated: Codex now always writes features.multi_agent = true; retained for state/back-compat
ModelAssignments map[string]ModelAssignment // key = sub-agent name (e.g., "sdd-init")
ClaudeModelAssignments map[string]ClaudeModelAlias // key = phase name; value = fable|opus|sonnet|haiku
ClaudePhaseAssignments map[string]ClaudePhaseAssignment // key = phase name; value = Claude model+effort
KiroModelAssignments map[string]KiroModelAlias // key = phase name; value = Kiro-native model alias
CodexModelAssignments map[string]CodexEffort // key = phase name; value = low|medium|high|xhigh
CodexOrchestratorAssignment *CodexOrchestratorAssignment // non-nil = apply curated top-level Codex model/effort
ClearCodexOrchestratorAssignment bool // true = clear persisted curated assignment while preserving config.toml
CodexCarrilModelAssignments map[string]string // key = carril profile (sdd-strong|sdd-mid|sdd-cheap); value = model id
CodexPhaseModelAssignments map[string]string // key = phase name; value = model id (Custom per-phase picker only)
Profiles []Profile // named SDD profiles to generate/update during sync
OpenCodePlugins []OpenCodeCommunityPluginID // optional community OpenCode TUI plugins
CommunityTools []CommunityToolID // optional cross-agent community tools/plugins
}

func (s Selection) HasCommunityTool(tool CommunityToolID) bool {
Expand Down Expand Up @@ -63,18 +63,18 @@ type SyncOverrides struct {
// TargetAgents forces TUI sync to run the adapter(s) affected by the
// override, even when persisted install state omits them. This is used by
// model/profile configurators, where the user picked a concrete target agent.
TargetAgents []AgentID
ModelAssignments map[string]ModelAssignment // nil = no override; empty map = reset to defaults
ClaudeModelAssignments map[string]ClaudeModelAlias // nil = no override; empty map = reset to defaults
ClaudePhaseAssignments map[string]ClaudePhaseAssignment // nil = no override; empty map = reset to defaults
KiroModelAssignments map[string]KiroModelAlias // nil = no override; empty map = reset to defaults
CodexModelAssignments map[string]CodexEffort // nil = no override; empty map = reset to defaults
CodexOrchestratorAssignment *CodexOrchestratorAssignment // non-nil = apply curated top-level Codex model/effort
ClearCodexOrchestratorAssignment bool // true = clear persisted curated assignment while preserving config.toml
CodexCarrilModelAssignments map[string]string // nil = no override; empty map = reset to defaults
CodexPhaseModelAssignments map[string]string // nil = no override (partial sync); non-nil empty = clear (preset selected); non-nil non-empty = custom per-phase assignments
SDDMode SDDModeID // "" = no override; when non-empty, overrides the sync's default SDD mode
SDDProfileStrategy SDDProfileStrategyID // "" = auto; otherwise explicit sync profile strategy
StrictTDD *bool // nil = no override; non-nil = override strict TDD mode
Profiles []Profile // NEW: profile creation/updates during sync
TargetAgents []AgentID
ModelAssignments map[string]ModelAssignment // nil = no override; empty map = reset to defaults
ClaudeModelAssignments map[string]ClaudeModelAlias // nil = no override; empty map = reset to defaults
ClaudePhaseAssignments map[string]ClaudePhaseAssignment // nil = no override; empty map = reset to defaults
KiroModelAssignments map[string]KiroModelAlias // nil = no override; empty map = reset to defaults
CodexModelAssignments map[string]CodexEffort // nil = no override; empty map = reset to defaults
CodexOrchestratorAssignment *CodexOrchestratorAssignment // non-nil = apply curated top-level Codex model/effort
ClearCodexOrchestratorAssignment bool // true = clear persisted curated assignment while preserving config.toml
CodexCarrilModelAssignments map[string]string // nil = no override; empty map = reset to defaults
CodexPhaseModelAssignments map[string]string // nil = no override (partial sync); non-nil empty = clear (preset selected); non-nil non-empty = custom per-phase assignments
SDDMode SDDModeID // "" = no override; when non-empty, overrides the sync's default SDD mode
SDDProfileStrategy SDDProfileStrategyID // "" = auto; otherwise explicit sync profile strategy
StrictTDD *bool // nil = no override; non-nil = override strict TDD mode
Profiles []Profile // NEW: profile creation/updates during sync
}
6 changes: 3 additions & 3 deletions internal/model/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ func TestAgentAntigravity(t *testing.T) {
// 1.1 — all six TriggerEvent constants exist with correct string values.
func TestTriggerEvent_ClosedSet(t *testing.T) {
events := []struct {
name string
got TriggerEvent
want string
name string
got TriggerEvent
want string
}{
{"EventPreCommit", EventPreCommit, "pre-commit"},
{"EventPrePush", EventPrePush, "pre-push"},
Expand Down
6 changes: 3 additions & 3 deletions internal/system/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,9 @@ func TestResolvePlatformProfileMatrix(t *testing.T) {
// used by effectiveMethod to implement the brew → go-install → binary auto-detect order.
func TestGoAvailableInPlatformProfile(t *testing.T) {
tests := []struct {
name string
tools map[string]ToolStatus
wantGoAvail bool
name string
tools map[string]ToolStatus
wantGoAvail bool
}{
{
name: "go in tools and installed → GoAvailable true",
Expand Down
10 changes: 5 additions & 5 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1261,12 +1261,12 @@ func (m Model) handleKeyPress(key tea.KeyMsg) (tea.Model, tea.Cmd) {
phaseOverride = map[string]string{} // explicit clear signal for the preset path
}
m.PendingSyncOverrides = &model.SyncOverrides{
TargetAgents: []model.AgentID{model.AgentCodex},
CodexModelAssignments: assignments,
CodexOrchestratorAssignment: m.Selection.CodexOrchestratorAssignment,
TargetAgents: []model.AgentID{model.AgentCodex},
CodexModelAssignments: assignments,
CodexOrchestratorAssignment: m.Selection.CodexOrchestratorAssignment,
ClearCodexOrchestratorAssignment: m.Selection.ClearCodexOrchestratorAssignment,
CodexCarrilModelAssignments: presetCarrilModels,
CodexPhaseModelAssignments: phaseOverride,
CodexCarrilModelAssignments: presetCarrilModels,
CodexPhaseModelAssignments: phaseOverride,
}
m = m.withResetSyncState()
m.setScreen(ScreenSync)
Expand Down
Loading
Loading