diff --git a/cmd/entire/cli/agent/agent.go b/cmd/entire/cli/agent/agent.go index 58810654d..fe35b73e7 100644 --- a/cmd/entire/cli/agent/agent.go +++ b/cmd/entire/cli/agent/agent.go @@ -65,6 +65,14 @@ type Agent interface { // FormatResumeCommand returns command to resume a session FormatResumeCommand(sessionID string) string + + // IsInstalled checks if the agent's CLI binary is available in PATH. + // Returns (true, nil) if found, (false, nil) if not found, + // and (false, err) only for unexpected OS errors. + IsInstalled() (bool, error) + + // InstallURL returns the installation documentation URL for this agent. + InstallURL() string } // HookSupport is implemented by agents with lifecycle hooks. diff --git a/cmd/entire/cli/agent/agent_test.go b/cmd/entire/cli/agent/agent_test.go index 6b9c5f397..6f641a611 100644 --- a/cmd/entire/cli/agent/agent_test.go +++ b/cmd/entire/cli/agent/agent_test.go @@ -24,10 +24,9 @@ func (m *mockAgent) SupportsHooks() bool { return false } func (m *mockAgent) ParseHookInput(_ HookType, _ io.Reader) (*HookInput, error) { return nil, nil } -func (m *mockAgent) GetSessionID(_ *HookInput) string { return "" } -func (m *mockAgent) TransformSessionID(agentID string) string { return agentID } -func (m *mockAgent) ProtectedDirs() []string { return nil } -func (m *mockAgent) GetSessionDir(_ string) (string, error) { return "", nil } +func (m *mockAgent) GetSessionID(_ *HookInput) string { return "" } +func (m *mockAgent) ProtectedDirs() []string { return nil } +func (m *mockAgent) GetSessionDir(_ string) (string, error) { return "", nil } func (m *mockAgent) ResolveSessionFile(sessionDir, agentSessionID string) string { return sessionDir + "/" + agentSessionID + ".jsonl" } @@ -36,6 +35,8 @@ func (m *mockAgent) ResolveSessionFile(sessionDir, agentSessionID string) string func (m *mockAgent) ReadSession(_ *HookInput) (*AgentSession, error) { return nil, nil } func (m *mockAgent) WriteSession(_ *AgentSession) error { return nil } func (m *mockAgent) FormatResumeCommand(_ string) string { return "" } +func (m *mockAgent) IsInstalled() (bool, error) { return false, nil } +func (m *mockAgent) InstallURL() string { return "" } // mockHookSupport implements both Agent and HookSupport interfaces. type mockHookSupport struct { diff --git a/cmd/entire/cli/agent/claudecode/claude.go b/cmd/entire/cli/agent/claudecode/claude.go index bfa394646..e522769b2 100644 --- a/cmd/entire/cli/agent/claudecode/claude.go +++ b/cmd/entire/cli/agent/claudecode/claude.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "regexp" "time" @@ -24,7 +25,11 @@ func init() { // ClaudeCodeAgent implements the Agent interface for Claude Code. // //nolint:revive // ClaudeCodeAgent is clearer than Agent in this context -type ClaudeCodeAgent struct{} +type ClaudeCodeAgent struct { + // lookPath checks if a binary exists in PATH. Defaults to exec.LookPath when nil. + // Exported for testing. + LookPath func(file string) (string, error) +} // NewClaudeCodeAgent creates a new Claude Code agent instance. func NewClaudeCodeAgent() agent.Agent { @@ -69,6 +74,27 @@ func (c *ClaudeCodeAgent) DetectPresence() (bool, error) { return false, nil } +// IsInstalled checks if the `claude` binary is available in PATH. +func (c *ClaudeCodeAgent) IsInstalled() (bool, error) { + lookPath := c.LookPath + if lookPath == nil { + lookPath = exec.LookPath + } + _, err := lookPath("claude") + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return false, nil + } + return false, fmt.Errorf("look path claude: %w", err) + } + return true, nil +} + +// InstallURL returns the installation documentation URL for Claude Code. +func (c *ClaudeCodeAgent) InstallURL() string { + return "https://docs.anthropic.com/en/docs/claude-code" +} + // GetHookConfigPath returns the path to Claude's hook config file. func (c *ClaudeCodeAgent) GetHookConfigPath() string { return ".claude/settings.json" diff --git a/cmd/entire/cli/agent/claudecode/claude_test.go b/cmd/entire/cli/agent/claudecode/claude_test.go index 4b7f6ee74..4e5ddf719 100644 --- a/cmd/entire/cli/agent/claudecode/claude_test.go +++ b/cmd/entire/cli/agent/claudecode/claude_test.go @@ -1,12 +1,71 @@ package claudecode import ( + "errors" + "os/exec" "strings" "testing" "github.com/entireio/cli/cmd/entire/cli/agent" ) +func TestIsInstalled_Found(t *testing.T) { + t.Parallel() + + c := &ClaudeCodeAgent{ + LookPath: func(file string) (string, error) { + if file == "claude" { + return "/usr/bin/claude", nil + } + return "", exec.ErrNotFound + }, + } + installed, err := c.IsInstalled() + + if err != nil { + t.Fatalf("IsInstalled() error = %v", err) + } + if !installed { + t.Error("IsInstalled() = false, want true") + } +} + +func TestIsInstalled_NotFound(t *testing.T) { + t.Parallel() + + c := &ClaudeCodeAgent{ + LookPath: func(_ string) (string, error) { + return "", exec.ErrNotFound + }, + } + installed, err := c.IsInstalled() + + if err != nil { + t.Fatalf("IsInstalled() error = %v", err) + } + if installed { + t.Error("IsInstalled() = true, want false") + } +} + +func TestIsInstalled_OSError(t *testing.T) { + t.Parallel() + + c := &ClaudeCodeAgent{ + LookPath: func(_ string) (string, error) { + return "", errors.New("permission denied") + }, + } + installed, err := c.IsInstalled() + + if err == nil { + t.Fatal("IsInstalled() should return error for OS errors") + } + if installed { + t.Error("IsInstalled() = true, want false on error") + } +} + func TestResolveSessionFile(t *testing.T) { t.Parallel() ag := &ClaudeCodeAgent{} diff --git a/cmd/entire/cli/agent/geminicli/gemini.go b/cmd/entire/cli/agent/geminicli/gemini.go index 63fc8969f..ba9ca406f 100644 --- a/cmd/entire/cli/agent/geminicli/gemini.go +++ b/cmd/entire/cli/agent/geminicli/gemini.go @@ -9,6 +9,7 @@ import ( "io" "log/slog" "os" + "os/exec" "path/filepath" "regexp" "time" @@ -26,7 +27,11 @@ func init() { // GeminiCLIAgent implements the Agent interface for Gemini CLI. // //nolint:revive // GeminiCLIAgent is clearer than Agent in this context -type GeminiCLIAgent struct{} +type GeminiCLIAgent struct { + // LookPath checks if a binary exists in PATH. Defaults to exec.LookPath when nil. + // Exported for testing. + LookPath func(file string) (string, error) +} func NewGeminiCLIAgent() agent.Agent { return &GeminiCLIAgent{} @@ -70,6 +75,27 @@ func (g *GeminiCLIAgent) DetectPresence() (bool, error) { return false, nil } +// IsInstalled checks if the `gemini` binary is available in PATH. +func (g *GeminiCLIAgent) IsInstalled() (bool, error) { + lookPath := g.LookPath + if lookPath == nil { + lookPath = exec.LookPath + } + _, err := lookPath("gemini") + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return false, nil + } + return false, fmt.Errorf("look path gemini: %w", err) + } + return true, nil +} + +// InstallURL returns the installation documentation URL for Gemini CLI. +func (g *GeminiCLIAgent) InstallURL() string { + return "https://github.com/google-gemini/gemini-cli" +} + // GetHookConfigPath returns the path to Gemini's hook config file. func (g *GeminiCLIAgent) GetHookConfigPath() string { return ".gemini/settings.json" diff --git a/cmd/entire/cli/agent/geminicli/gemini_test.go b/cmd/entire/cli/agent/geminicli/gemini_test.go index c4c22ac8d..c6f7d2d32 100644 --- a/cmd/entire/cli/agent/geminicli/gemini_test.go +++ b/cmd/entire/cli/agent/geminicli/gemini_test.go @@ -3,8 +3,10 @@ package geminicli import ( "bytes" "encoding/json" + "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -15,6 +17,63 @@ import ( // Test constants const testSessionID = "abc123" +func TestIsInstalled_Found(t *testing.T) { + t.Parallel() + + ag := &GeminiCLIAgent{ + LookPath: func(file string) (string, error) { + if file == "gemini" { + return "/usr/bin/gemini", nil + } + return "", exec.ErrNotFound + }, + } + installed, err := ag.IsInstalled() + + if err != nil { + t.Fatalf("IsInstalled() error = %v", err) + } + if !installed { + t.Error("IsInstalled() = false, want true") + } +} + +func TestIsInstalled_NotFound(t *testing.T) { + t.Parallel() + + ag := &GeminiCLIAgent{ + LookPath: func(_ string) (string, error) { + return "", exec.ErrNotFound + }, + } + installed, err := ag.IsInstalled() + + if err != nil { + t.Fatalf("IsInstalled() error = %v", err) + } + if installed { + t.Error("IsInstalled() = true, want false") + } +} + +func TestIsInstalled_OSError(t *testing.T) { + t.Parallel() + + ag := &GeminiCLIAgent{ + LookPath: func(_ string) (string, error) { + return "", errors.New("permission denied") + }, + } + installed, err := ag.IsInstalled() + + if err == nil { + t.Fatal("IsInstalled() should return error for OS errors") + } + if installed { + t.Error("IsInstalled() = true, want false on error") + } +} + func TestNewGeminiCLIAgent(t *testing.T) { ag := NewGeminiCLIAgent() if ag == nil { diff --git a/cmd/entire/cli/integration_test/testenv.go b/cmd/entire/cli/integration_test/testenv.go index 224a58854..f71c803da 100644 --- a/cmd/entire/cli/integration_test/testenv.go +++ b/cmd/entire/cli/integration_test/testenv.go @@ -104,10 +104,12 @@ func (env *TestEnv) Cleanup() { // cliEnv returns the environment variables for CLI execution. // Includes both Claude and Gemini project dirs so tests work for any agent. +// Sets ENTIRE_SKIP_AGENT_CHECK=1 to bypass agent installation checks in tests. func (env *TestEnv) cliEnv() []string { return append(os.Environ(), "ENTIRE_TEST_CLAUDE_PROJECT_DIR="+env.ClaudeProjectDir, "ENTIRE_TEST_GEMINI_PROJECT_DIR="+env.GeminiProjectDir, + "ENTIRE_SKIP_AGENT_CHECK=1", ) } diff --git a/cmd/entire/cli/setup.go b/cmd/entire/cli/setup.go index bb04eecae..afb25df75 100644 --- a/cmd/entire/cli/setup.go +++ b/cmd/entire/cli/setup.go @@ -96,13 +96,27 @@ Strategies: manual-commit (default), auto-commit`, printWrongAgentError(cmd.ErrOrStderr(), agentName) return NewSilentError(errors.New("wrong agent name")) } + + // If agent isn't in PATH, warn but still proceed (hooks are only run when the agent runs). + if os.Getenv("ENTIRE_SKIP_AGENT_CHECK") != "1" { + installed, err := ag.IsInstalled() + if err != nil { + return fmt.Errorf("error checking if %s is installed: %w", agentName, err) + } + if !installed { + fmt.Fprintf(cmd.ErrOrStderr(), + "Note: %s is not in PATH. Hooks were installed; install it from %s when you're ready to capture sessions.\n", + agentName, ag.InstallURL()) + } + } + return setupAgentHooksNonInteractive(cmd.OutOrStdout(), ag, strategyFlag, localDev, forceHooks, skipPushSessions, telemetry) } // If strategy is specified via flag, skip interactive selection if strategyFlag != "" { - return runEnableWithStrategy(cmd.OutOrStdout(), strategyFlag, localDev, ignoreUntracked, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry) + return runEnableWithStrategy(cmd.OutOrStdout(), cmd.ErrOrStderr(), strategyFlag, localDev, ignoreUntracked, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry) } - return runEnableInteractive(cmd.OutOrStdout(), localDev, ignoreUntracked, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry) + return runEnableInteractive(cmd.OutOrStdout(), cmd.ErrOrStderr(), localDev, ignoreUntracked, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry) }, } @@ -221,7 +235,7 @@ func isFullyEnabled() (enabled bool, agentDesc string, configPath string) { // runEnableWithStrategy enables Entire with a specified strategy (non-interactive). // The selectedStrategy can be either a display name (manual-commit, auto-commit) // or an internal name (manual-commit, auto-commit). -func runEnableWithStrategy(w io.Writer, selectedStrategy string, localDev, _, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry bool) error { +func runEnableWithStrategy(w, errW io.Writer, selectedStrategy string, localDev, _, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry bool) error { // Map the strategy to internal name if it's a display name internalStrategy := selectedStrategy if mapped, ok := strategyDisplayToInternal[selectedStrategy]; ok { @@ -234,17 +248,16 @@ func runEnableWithStrategy(w io.Writer, selectedStrategy string, localDev, _, us return fmt.Errorf("unknown strategy: %s (use manual-commit or auto-commit)", selectedStrategy) } - // Detect default agent - ag := agent.Default() - agentType := string(agent.AgentTypeClaudeCode) - if ag != nil { - agentType = string(ag.Type()) + // Find an installed agent + ag, err := findInstalledAgent(errW) + if err != nil { + return err } - fmt.Fprintf(w, "Agent: %s (use --agent to change)\n\n", agentType) + fmt.Fprintf(w, "Agent: %s (use --agent to change)\n\n", ag.Type()) - // Setup Claude Code hooks (agent hooks don't depend on settings) - if _, err := setupClaudeCodeHook(localDev, forceHooks); err != nil { - return fmt.Errorf("failed to setup Claude Code hooks: %w", err) + // Setup agent hooks (agent hooks don't depend on settings) + if _, err := setupAgentHooks(ag, localDev, forceHooks); err != nil { + return fmt.Errorf("failed to setup agent hooks: %w", err) } // Setup .entire directory @@ -320,7 +333,7 @@ func runEnableWithStrategy(w io.Writer, selectedStrategy string, localDev, _, us } // runEnableInteractive runs the interactive enable flow. -func runEnableInteractive(w io.Writer, localDev, _, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry bool) error { +func runEnableInteractive(w, errW io.Writer, localDev, _, useLocalSettings, useProjectSettings, forceHooks, skipPushSessions, telemetry bool) error { // Check if already fully enabled — show summary and return early. // Skip early return if any configuration flags are set (user wants to reconfigure). hasConfigFlags := forceHooks || skipPushSessions || !telemetry || useLocalSettings || useProjectSettings || localDev @@ -334,17 +347,16 @@ func runEnableInteractive(w io.Writer, localDev, _, useLocalSettings, useProject } } - // Detect default agent - ag := agent.Default() - agentType := string(agent.AgentTypeClaudeCode) - if ag != nil { - agentType = string(ag.Type()) + // Find an installed agent + ag, err := findInstalledAgent(errW) + if err != nil { + return err } - fmt.Fprintf(w, "Agent: %s (use --agent to change)\n\n", agentType) + fmt.Fprintf(w, "Agent: %s (use --agent to change)\n\n", ag.Type()) - // Setup Claude Code hooks (agent hooks don't depend on settings) - if _, err := setupClaudeCodeHook(localDev, forceHooks); err != nil { - return fmt.Errorf("failed to setup Claude Code hooks: %w", err) + // Setup agent hooks (agent hooks don't depend on settings) + if _, err := setupAgentHooks(ag, localDev, forceHooks); err != nil { + return fmt.Errorf("failed to setup agent hooks: %w", err) } // Setup .entire directory @@ -495,25 +507,110 @@ func checkDisabledGuard(w io.Writer) bool { return false } -// setupClaudeCodeHook sets up Claude Code hooks. -// This is a convenience wrapper that uses the agent package. -// Returns the number of hooks installed (0 if already installed). -func setupClaudeCodeHook(localDev, forceHooks bool) (int, error) { //nolint:unparam // already present in codebase - ag, err := agent.Get(agent.AgentNameClaudeCode) +// isInstalledWithHookSupport returns true if the agent is installed (binary in PATH) +// and implements HookSupport, so we can install hooks for it. +// Returns (false, err) when IsInstalled() returns an unexpected OS error, so callers +// can propagate it instead of showing a generic "not found" message. +func isInstalledWithHookSupport(ag agent.Agent) (bool, error) { + if _, ok := ag.(agent.HookSupport); !ok { + return false, nil + } + installed, err := ag.IsInstalled() if err != nil { - return 0, fmt.Errorf("failed to get claude-code agent: %w", err) + return false, fmt.Errorf("checking installation status: %w", err) + } + return installed, nil +} + +// findInstalledAgentFn is the function used to find an installed agent. +// It can be overridden in tests to return a mock agent. +var findInstalledAgentFn = findInstalledAgentImpl + +// findInstalledAgent calls the findInstalledAgentFn function variable. +// This indirection allows tests to mock the agent detection. +func findInstalledAgent(errW io.Writer) (agent.Agent, error) { + return findInstalledAgentFn(errW) +} + +// findInstalledAgentImpl returns the first installed agent that supports hooks. +// It tries the default agent first, then checks all registered agents. +// Only agents that are both installed (binary in PATH) and implement HookSupport +// are returned, so the enable flow can always install hooks. +// Returns the agent and nil error if found, or nil agent and an error with +// helpful install instructions written to errW (stderr). OS errors from +// IsInstalled() (e.g. permission errors on PATH) are propagated to the caller. +// +// If ENTIRE_SKIP_AGENT_CHECK=1 is set (used by integration tests), the default +// agent is returned without checking if it's installed. +func findInstalledAgentImpl(errW io.Writer) (agent.Agent, error) { + // Allow tests to skip the agent check + if os.Getenv("ENTIRE_SKIP_AGENT_CHECK") == "1" { + ag := agent.Default() + if ag != nil { + return ag, nil + } } + var firstErr error + // Try the default agent first + ag := agent.Default() + if ag != nil { + ok, err := isInstalledWithHookSupport(ag) + if err != nil { + firstErr = err + } else if ok { + return ag, nil + } + } + + // Check all registered agents + for _, name := range agent.List() { + a, err := agent.Get(name) + if err != nil { + continue + } + ok, err := isInstalledWithHookSupport(a) + if err != nil { + if firstErr == nil { + firstErr = err + } + } else if ok { + return a, nil + } + } + + // If we hit an OS error while checking, propagate it + if firstErr != nil { + return nil, fmt.Errorf("error checking if agent is installed: %w", firstErr) + } + + // No hook-supporting agents found - provide helpful error to stderr + fmt.Fprintln(errW, "No AI agents with hook support found in PATH.") + fmt.Fprintln(errW) + fmt.Fprintln(errW, "Please install one of the following:") + for _, name := range agent.List() { + a, err := agent.Get(name) + if err != nil { + continue + } + fmt.Fprintf(errW, " - %s: %s\n", a.Description(), a.InstallURL()) + } + return nil, NewSilentError(errors.New("no agents installed")) +} + +// setupAgentHooks installs hooks for the given agent if it supports them. +// Returns the number of hooks installed. +// +//nolint:unparam // count is returned for API consistency with InstallHooks +func setupAgentHooks(ag agent.Agent, localDev, forceHooks bool) (int, error) { hookAgent, ok := ag.(agent.HookSupport) if !ok { - return 0, errors.New("claude-code agent does not support hooks") + return 0, nil // Agent doesn't support hooks, not an error } - count, err := hookAgent.InstallHooks(localDev, forceHooks) if err != nil { - return 0, fmt.Errorf("failed to install claude-code hooks: %w", err) + return 0, fmt.Errorf("failed to install hooks for %s: %w", ag.Name(), err) } - return count, nil } diff --git a/cmd/entire/cli/setup_test.go b/cmd/entire/cli/setup_test.go index 41c5bb719..23056a9e6 100644 --- a/cmd/entire/cli/setup_test.go +++ b/cmd/entire/cli/setup_test.go @@ -344,6 +344,7 @@ func TestDetermineSettingsTarget_SettingsNotExists_NoFlags(t *testing.T) { } func TestRunEnableWithStrategy_PreservesExistingSettings(t *testing.T) { + t.Setenv("ENTIRE_SKIP_AGENT_CHECK", "1") setupTestRepo(t) // Create initial settings with strategy_options (like push enabled) @@ -358,8 +359,8 @@ func TestRunEnableWithStrategy_PreservesExistingSettings(t *testing.T) { writeSettings(t, initialSettings) // Run enable with a different strategy - var stdout bytes.Buffer - err := runEnableWithStrategy(&stdout, "auto-commit", false, false, false, true, false, false, false) + var stdout, stderr bytes.Buffer + err := runEnableWithStrategy(&stdout, &stderr, "auto-commit", false, false, false, true, false, false, false) if err != nil { t.Fatalf("runEnableWithStrategy() error = %v", err) } @@ -388,6 +389,7 @@ func TestRunEnableWithStrategy_PreservesExistingSettings(t *testing.T) { } func TestRunEnableWithStrategy_PreservesLocalSettings(t *testing.T) { + t.Setenv("ENTIRE_SKIP_AGENT_CHECK", "1") setupTestRepo(t) // Create project settings @@ -402,8 +404,8 @@ func TestRunEnableWithStrategy_PreservesLocalSettings(t *testing.T) { writeLocalSettings(t, localSettings) // Run enable with --local flag - var stdout bytes.Buffer - err := runEnableWithStrategy(&stdout, "auto-commit", false, false, true, false, false, false, false) + var stdout, stderr bytes.Buffer + err := runEnableWithStrategy(&stdout, &stderr, "auto-commit", false, false, true, false, false, false, false) if err != nil { t.Fatalf("runEnableWithStrategy() error = %v", err) } @@ -878,3 +880,64 @@ func TestEnableCmd_AgentFlagEmptyValue(t *testing.T) { t.Error("should not contain default cobra/pflag error message") } } + +func TestAgentInstallURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + agent string + wantURL string + }{ + { + name: "claude-code", + agent: "claude-code", + wantURL: "https://docs.anthropic.com/en/docs/claude-code", + }, + { + name: "gemini", + agent: "gemini", + wantURL: "https://github.com/google-gemini/gemini-cli", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ag, err := agent.Get(agent.AgentName(tt.agent)) + if err != nil { + t.Fatalf("agent.Get(%q): %v", tt.agent, err) + } + got := ag.InstallURL() + if got != tt.wantURL { + t.Errorf("InstallURL() = %q, want %q", got, tt.wantURL) + } + }) + } +} + +func TestFindInstalledAgent_ReturnsAgent(t *testing.T) { + t.Parallel() + + var stderrBuf bytes.Buffer + ag, err := findInstalledAgent(&stderrBuf) + + // This test is environment-dependent: + // - If claude or gemini is installed, ag should be non-nil + // - If neither is installed, err should be non-nil with helpful message on stderr + if ag != nil { + // Agent was found - verify it has a valid name + if ag.Name() == "" { + t.Error("findInstalledAgent() returned agent with empty name") + } + } else { + // No agent found - verify helpful error message was printed to stderr + if err == nil { + t.Error("findInstalledAgent() returned nil agent and nil error") + } + output := stderrBuf.String() + if !strings.Contains(output, "No AI agents with hook support found in PATH") { + t.Errorf("expected helpful error message on stderr, got: %s", output) + } + } +} diff --git a/cmd/entire/cli/strategy/manual_commit_rewind.go b/cmd/entire/cli/strategy/manual_commit_rewind.go index ac30eafcd..5c8a06803 100644 --- a/cmd/entire/cli/strategy/manual_commit_rewind.go +++ b/cmd/entire/cli/strategy/manual_commit_rewind.go @@ -792,7 +792,7 @@ func ResolveAgentForRewind(agentType agent.AgentType) (agent.Agent, error) { // ResolveSessionFilePath determines the correct file path for an agent's session transcript. // Checks session state for transcript_path first (needed for agents like Gemini that store // transcripts at paths that GetSessionDir can't reconstruct, e.g. SHA-256 hashed directories). -// Falls back to the agent's ExtractAgentSessionID + ResolveSessionFile with fallbackSessionDir. +// Falls back to the agent's ResolveSessionFile with fallbackSessionDir. func ResolveSessionFilePath(sessionID string, ag agent.Agent, fallbackSessionDir string) string { state, err := LoadSessionState(sessionID) if err == nil && state != nil && state.TranscriptPath != "" {