diff --git a/internal/code/claude_docker.go b/internal/code/claude_docker.go index 78a7642..fbcc218 100644 --- a/internal/code/claude_docker.go +++ b/internal/code/claude_docker.go @@ -35,6 +35,8 @@ func NewClaudeDocker(workspace *models.Workspace, cfg *config.Config) (Code, err if err != nil { return nil, fmt.Errorf("failed to create MCP config: %w", err) } + // Set MCP config path in workspace for tracking and cleanup + workspace.MCPConfigPath = mcpConfigPath log.Infof("MCP config file created at: %s", mcpConfigPath) // Check if corresponding container is already running diff --git a/internal/code/mcp_config.go b/internal/code/mcp_config.go index 8154ec2..a071492 100644 --- a/internal/code/mcp_config.go +++ b/internal/code/mcp_config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strconv" "github.com/qiniu/codeagent/internal/config" @@ -129,8 +130,24 @@ func (g *MCPConfigGenerator) CreateTempConfig() (string, error) { return "", err } - // 创建临时文件在/tmp目录中 - tempFile, err := os.CreateTemp(g.config.Workspace.BaseDir, "codeagent-mcp-*.json") + // 创建MCP配置文件在workspace目录中,与代码仓和session目录保持一致 + var mcpConfigDir string + if g.workspace.SessionPath != "" { + // 如果有session目录,将MCP配置放在session目录的同级 + mcpConfigDir = filepath.Dir(g.workspace.SessionPath) + } else { + // 如果没有session目录,将MCP配置放在代码仓的同级 + mcpConfigDir = filepath.Dir(g.workspace.Path) + } + + // 确保目录存在 + if err := os.MkdirAll(mcpConfigDir, 0755); err != nil { + return "", fmt.Errorf("failed to create MCP config directory: %w", err) + } + + // 创建临时文件在workspace相关目录中 + // TODO(CarlJi): mcp config file name should be align with the workspace name + tempFile, err := os.CreateTemp(mcpConfigDir, "codeagent-mcp-*.json") if err != nil { return "", fmt.Errorf("failed to create temp file: %w", err) } @@ -146,5 +163,6 @@ func (g *MCPConfigGenerator) CreateTempConfig() (string, error) { return "", err } + log.Infof("Created MCP config file: %s", tempFile.Name()) return tempFile.Name(), nil } diff --git a/internal/workspace/git_service.go b/internal/workspace/git_service.go index f342a94..5e099ca 100644 --- a/internal/workspace/git_service.go +++ b/internal/workspace/git_service.go @@ -23,6 +23,7 @@ type GitService interface { CreateAndCheckoutBranch(repoPath, branchName string) error CheckoutBranch(repoPath, branchName string) error CreateTrackingBranch(repoPath, branchName string) error + FetchAndCheckoutPR(repoPath string, prNumber int) error } type gitService struct{} @@ -46,18 +47,25 @@ func (g *gitService) CloneRepository(repoURL, clonePath, branch string, createNe if createNewBranch { // Clone the default branch first, then create new branch cmd = exec.Command("git", "clone", "--depth", "50", repoURL, clonePath) + log.Infof("Executing Git command: %s", cmd.String()) } else { // Try to clone specific branch directly cmd = exec.Command("git", "clone", "--depth", "50", "--branch", branch, repoURL, clonePath) + log.Infof("Executing Git command: %s", cmd.String()) } output, err := cmd.CombinedOutput() if err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) if !createNewBranch { // If direct branch clone failed, try cloning default branch first log.Warnf("Failed to clone specific branch %s directly, cloning default branch: %v", branch, err) cmd = exec.Command("git", "clone", "--depth", "50", repoURL, clonePath) + log.Infof("Executing fallback Git command: %s", cmd.String()) output, err = cmd.CombinedOutput() + if err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) + } } if err != nil { return GitError("clone", clonePath, fmt.Errorf("%s: %w", string(output), err)) @@ -103,8 +111,10 @@ func (g *gitService) CloneRepository(repoURL, clonePath, branch string, createNe func (g *gitService) GetRemoteURL(repoPath string) (string, error) { cmd := exec.Command("git", "remote", "get-url", "origin") cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) output, err := cmd.Output() if err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return "", GitError("get_remote_url", repoPath, err) } return strings.TrimSpace(string(output)), nil @@ -114,8 +124,10 @@ func (g *gitService) GetRemoteURL(repoPath string) (string, error) { func (g *gitService) GetCurrentBranch(repoPath string) (string, error) { cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) output, err := cmd.Output() if err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return "", GitError("get_current_branch", repoPath, err) } return strings.TrimSpace(string(output)), nil @@ -125,8 +137,10 @@ func (g *gitService) GetCurrentBranch(repoPath string) (string, error) { func (g *gitService) GetCurrentCommit(repoPath string) (string, error) { cmd := exec.Command("git", "rev-parse", "HEAD") cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) output, err := cmd.Output() if err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return "", fmt.Errorf("failed to get current commit: %w", err) } return strings.TrimSpace(string(output)), nil @@ -136,8 +150,10 @@ func (g *gitService) GetCurrentCommit(repoPath string) (string, error) { func (g *gitService) GetBranchCommit(repoPath, branch string) (string, error) { cmd := exec.Command("git", "rev-parse", fmt.Sprintf("origin/%s", branch)) cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) output, err := cmd.Output() if err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return "", fmt.Errorf("failed to get branch commit for %s: %w", branch, err) } return strings.TrimSpace(string(output)), nil @@ -183,7 +199,9 @@ func (g *gitService) ValidateBranch(repoPath, expectedBranch string) bool { func (g *gitService) ConfigureSafeDirectory(repoPath string) error { cmd := exec.Command("git", "config", "--local", "--add", "safe.directory", repoPath) cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) if output, err := cmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return GitError("config_safe_directory", repoPath, fmt.Errorf("%s: %w", string(output), err)) } return nil @@ -193,7 +211,9 @@ func (g *gitService) ConfigureSafeDirectory(repoPath string) error { func (g *gitService) ConfigurePullStrategy(repoPath string) error { cmd := exec.Command("git", "config", "--local", "pull.rebase", "true") cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) if output, err := cmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return fmt.Errorf("failed to configure pull strategy: %w, output: %s", err, string(output)) } return nil @@ -203,7 +223,9 @@ func (g *gitService) ConfigurePullStrategy(repoPath string) error { func (g *gitService) CreateAndCheckoutBranch(repoPath, branchName string) error { cmd := exec.Command("git", "checkout", "-b", branchName) cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) if output, err := cmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return fmt.Errorf("failed to create new branch %s: %w, output: %s", branchName, err, string(output)) } return nil @@ -213,7 +235,9 @@ func (g *gitService) CreateAndCheckoutBranch(repoPath, branchName string) error func (g *gitService) CheckoutBranch(repoPath, branchName string) error { cmd := exec.Command("git", "checkout", branchName) cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) if output, err := cmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return fmt.Errorf("failed to checkout branch %s: %w, output: %s", branchName, err, string(output)) } return nil @@ -223,8 +247,64 @@ func (g *gitService) CheckoutBranch(repoPath, branchName string) error { func (g *gitService) CreateTrackingBranch(repoPath, branchName string) error { cmd := exec.Command("git", "checkout", "-b", branchName, fmt.Sprintf("origin/%s", branchName)) cmd.Dir = repoPath + log.Infof("Executing Git command: %s", cmd.String()) if output, err := cmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err) return fmt.Errorf("failed to create tracking branch %s: %w, output: %s", branchName, err, string(output)) } return nil } + +// FetchAndCheckoutPR fetches and checks out PR content using GitHub's PR refs +// Always uses force mode to handle updates, force pushes, and ensure latest content +func (g *gitService) FetchAndCheckoutPR(repoPath string, prNumber int) error { + log.Infof("Fetching PR #%d content using GitHub PR refs (force mode)", prNumber) + + prBranchName := fmt.Sprintf("pr-%d", prNumber) + currentBranch, err := g.GetCurrentBranch(repoPath) + + // If we're already on the PR branch, use a lightweight in-place update + if err == nil && currentBranch == prBranchName { + log.Infof("Already on PR branch %s, performing in-place sync", prBranchName) + + // Step 1: First fetch the PR content to FETCH_HEAD without creating/updating local branch + fetchCmd := exec.Command("git", "fetch", "origin", fmt.Sprintf("pull/%d/head", prNumber)) + fetchCmd.Dir = repoPath + log.Infof("Executing Git command: %s", fetchCmd.String()) + if output, err := fetchCmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", fetchCmd.String(), string(output), err) + return fmt.Errorf("failed to fetch PR #%d content: %w, output: %s", prNumber, err, string(output)) + } + + // Step 2: Reset current branch to the fetched content + resetCmd := exec.Command("git", "reset", "--hard", "FETCH_HEAD") + resetCmd.Dir = repoPath + log.Infof("Executing Git command: %s", resetCmd.String()) + if output, err := resetCmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", resetCmd.String(), string(output), err) + return fmt.Errorf("failed to reset PR branch to latest content: %w, output: %s", err, string(output)) + } + + log.Infof("Successfully updated PR #%d branch in-place", prNumber) + return nil + } + + fetchCmd := exec.Command("git", "fetch", "origin", fmt.Sprintf("pull/%d/head:%s", prNumber, prBranchName), "--force") + fetchCmd.Dir = repoPath + log.Infof("Executing Git command: %s", fetchCmd.String()) + if output, err := fetchCmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", fetchCmd.String(), string(output), err) + return fmt.Errorf("failed to fetch PR #%d: %w, output: %s", prNumber, err, string(output)) + } + + checkoutCmd := exec.Command("git", "checkout", prBranchName) + checkoutCmd.Dir = repoPath + log.Infof("Executing Git command: %s", checkoutCmd.String()) + if output, err := checkoutCmd.CombinedOutput(); err != nil { + log.Errorf("Git command failed: %s, output: %s, error: %v", checkoutCmd.String(), string(output), err) + return fmt.Errorf("failed to checkout PR branch %s: %w, output: %s", prBranchName, err, string(output)) + } + + log.Infof("Successfully fetched and checked out PR #%d content to branch: %s", prNumber, prBranchName) + return nil +} diff --git a/internal/workspace/manager.go b/internal/workspace/manager.go index 84186eb..aac07a3 100644 --- a/internal/workspace/manager.go +++ b/internal/workspace/manager.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "time" @@ -47,6 +48,9 @@ func NewManager(cfg *config.Config) *Manager { // Recover existing workspaces on startup m.recoverExistingWorkspaces() + // Start periodic cleanup + m.startPeriodicCleanup() + return m } @@ -218,6 +222,21 @@ func (m *Manager) CreateWorkspaceFromPR(pr *github.PullRequest, aiModel string) return nil } + // For fork PRs, fetch and checkout the PR content after cloning + var actualBranch = cloneBranch + if isForkPR { + log.Infof("Fork PR detected, fetching PR #%d content using GitHub PR refs", pr.GetNumber()) + + if err := m.gitService.FetchAndCheckoutPR(clonePath, pr.GetNumber()); err != nil { + log.Errorf("Failed to fetch fork PR content for PR #%d: %v", pr.GetNumber(), err) + // Don't fail completely, but log the error - the base branch clone still works + } else { + // Update the actual branch to the PR branch we checked out + actualBranch = fmt.Sprintf("pr-%d", pr.GetNumber()) + log.Infof("Successfully fetched fork PR content, workspace is now on branch: %s", actualBranch) + } + } + // Create session directory suffix := m.dirFormatter.ExtractSuffixFromPRDir(aiModel, repo, pr.GetNumber(), prDir) sessionPath, err := m.CreateSessionPath(filepath.Join(m.baseDir, org), aiModel, repo, pr.GetNumber(), suffix) @@ -235,7 +254,7 @@ func (m *Manager) CreateWorkspaceFromPR(pr *github.PullRequest, aiModel string) Path: clonePath, SessionPath: sessionPath, Repository: repoURL, - Branch: pr.GetHead().GetRef(), // Always use the actual PR head branch for workspace + Branch: actualBranch, // Use the actual branch after potential PR content fetch PullRequest: pr, CreatedAt: time.Now(), } @@ -256,6 +275,11 @@ func (m *Manager) GetOrCreateWorkspaceForPR(pr *github.PullRequest, aiModel stri if ws != nil { // Validate workspace for PR if m.validateWorkspaceForPR(ws, pr) { + // For PR workspaces, also check if content is stale and sync if needed + if err := m.syncPRContentIfStale(ws, pr); err != nil { + log.Errorf("Failed to sync PR content for workspace: %v", err) + // Continue with existing workspace even if sync fails + } return ws } // If validation fails, cleanup old workspace @@ -321,6 +345,46 @@ func (m *Manager) GetExpiredWorkspaces() []*models.Workspace { return m.repository.GetExpired(m.config.Workspace.CleanupAfter) } +// startPeriodicCleanup starts a goroutine for periodic cleanup of expired workspaces +func (m *Manager) startPeriodicCleanup() { + // Run cleanup every 24 hours + ticker := time.NewTicker(24 * time.Hour) + // Cleanup expired workspaces immediately + m.cleanupExpiredWorkspaces() + go func() { + log.Infof("Started periodic workspace cleanup") + defer ticker.Stop() + for range ticker.C { + // Cleanup expired workspaces periodically + m.cleanupExpiredWorkspaces() + } + }() +} + +// cleanupExpiredWorkspaces cleans up all expired workspaces +func (m *Manager) cleanupExpiredWorkspaces() { + expiredWorkspaces := m.GetExpiredWorkspaces() + if len(expiredWorkspaces) == 0 { + log.Info("No expired workspaces found for cleanup") + return + } + + log.Infof("Found %d expired workspaces, starting cleanup", len(expiredWorkspaces)) + cleanedCount := 0 + + for _, ws := range expiredWorkspaces { + log.Infof("Cleaning up expired workspace: %s (created at: %s)", ws.Path, ws.CreatedAt.Format(time.RFC3339)) + if m.CleanupWorkspace(ws) { + cleanedCount++ + log.Infof("Successfully cleaned up expired workspace: %s", ws.Path) + } else { + log.Errorf("Failed to cleanup expired workspace: %s", ws.Path) + } + } + + log.Infof("Expired workspace cleanup completed. Cleaned %d/%d workspaces", cleanedCount, len(expiredWorkspaces)) +} + // ExtractAIModelFromBranch extracts AI model information from branch name func (m *Manager) ExtractAIModelFromBranch(branchName string) string { // Check if it's a codeagent branch @@ -375,6 +439,7 @@ func (m *Manager) ExtractSuffixFromIssueDir(aiModel, repo string, issueNumber in func (m *Manager) cleanupPhysicalWorkspace(ws *models.Workspace) bool { cloneRemoved := false sessionRemoved := false + mcpConfigsRemoved := true // 默认为true,如果有MCP配置文件但清理失败则设为false // Remove cloned repository directory if ws.Path != "" { @@ -396,13 +461,19 @@ func (m *Manager) cleanupPhysicalWorkspace(ws *models.Workspace) bool { } } + // Clean up MCP configuration files + if err := m.cleanupMCPConfigs(ws); err != nil { + log.Errorf("Failed to cleanup MCP config files: %v", err) + mcpConfigsRemoved = false + } + // Clean up related Docker containers if err := m.containerService.CleanupWorkspaceContainers(ws); err != nil { log.Warnf("Failed to cleanup containers for workspace %s: %v", ws.Path, err) } - // Only return true if both clone and session are cleaned successfully - return cloneRemoved && sessionRemoved + // Return true if all cleanup operations succeeded + return cloneRemoved && sessionRemoved && mcpConfigsRemoved } // cleanupClonedRepository removes a cloned repository directory @@ -417,11 +488,91 @@ func (m *Manager) cleanupClonedRepository(clonePath string) error { if err := os.RemoveAll(clonePath); err != nil { return fmt.Errorf("failed to remove cloned repository directory: %w", err) } + return nil +} + +// cleanupMCPConfigs removes MCP configuration files associated with the workspace +func (m *Manager) cleanupMCPConfigs(ws *models.Workspace) error { + if ws == nil { + return nil + } + + removedCount := 0 + + // Remove the specific MCP config file if path is known + if ws.MCPConfigPath != "" { + if err := os.Remove(ws.MCPConfigPath); err != nil { + if !os.IsNotExist(err) { + log.Warnf("Failed to remove MCP config file %s: %v", ws.MCPConfigPath, err) + return err + } else { + log.Infof("MCP config file already removed: %s", ws.MCPConfigPath) + } + } else { + log.Infof("Successfully removed MCP config file: %s", ws.MCPConfigPath) + removedCount++ + } + } else { + // Fallback: search for MCP config files using pattern matching (for legacy workspaces) + var mcpConfigDirs []string + + if ws.SessionPath != "" { + // session目录的同级目录 + mcpConfigDirs = append(mcpConfigDirs, filepath.Dir(ws.SessionPath)) + } else if ws.Path != "" { + // 代码仓的同级目录 + mcpConfigDirs = append(mcpConfigDirs, filepath.Dir(ws.Path)) + } + + for _, dir := range mcpConfigDirs { + matches, err := filepath.Glob(filepath.Join(dir, "codeagent-mcp-*.json")) + if err != nil { + log.Warnf("Failed to search MCP config files in %s: %v", dir, err) + continue + } + + for _, mcpFile := range matches { + if err := os.Remove(mcpFile); err != nil { + log.Warnf("Failed to remove MCP config file %s: %v", mcpFile, err) + } else { + log.Infof("Successfully removed MCP config file: %s", mcpFile) + removedCount++ + } + } + } + } + + if removedCount > 0 { + log.Infof("Cleaned up %d MCP config files for workspace", removedCount) + } - log.Infof("Successfully removed cloned repository: %s", clonePath) return nil } +// findExistingMCPConfig tries to find an existing MCP config file in the specified directory +func (m *Manager) findExistingMCPConfig(searchDir string) string { + if searchDir == "" { + return "" + } + + // Search for codeagent-mcp-*.json pattern + matches, err := filepath.Glob(filepath.Join(searchDir, "codeagent-mcp-*.json")) + if err != nil { + log.Warnf("Failed to search for MCP config files in %s: %v", searchDir, err) + return "" + } + + // Return the first match if found + for _, match := range matches { + if _, err := os.Stat(match); err == nil { + log.Infof("Found existing MCP config file during recovery: %s", match) + return match + } + } + + return "" +} + // isForkRepositoryPR checks if a PR is from a fork repository func (m *Manager) isForkRepositoryPR(pr *github.PullRequest) bool { if pr == nil || pr.GetHead() == nil || pr.GetBase() == nil { @@ -456,7 +607,13 @@ func (m *Manager) validateWorkspaceForPR(ws *models.Workspace, pr *github.PullRe } // Check if workspace is on correct branch - expectedBranch := pr.GetHead().GetRef() + // Use the branch stored in workspace (which reflects the actual cloned branch) + // rather than always expecting the PR head branch + expectedBranch := ws.Branch + if expectedBranch == "" { + // Fallback to PR head branch if workspace branch is not set + expectedBranch = pr.GetHead().GetRef() + } return m.gitService.ValidateBranch(ws.Path, expectedBranch) } @@ -573,8 +730,9 @@ func (m *Manager) recoverExistingWorkspaces() { // recoverPRWorkspaceFromClone recovers a single PR workspace from clone func (m *Manager) recoverPRWorkspaceFromClone(org, repo, clonePath, remoteURL string, prNumber int, aiModel string, timestamp int64) error { - // Create corresponding session directory (same level as clone directory) - sessionPath := m.dirFormatter.CreateSessionPathWithTimestamp(m.baseDir, aiModel, repo, prNumber, timestamp) + // Create corresponding session directory using the same suffix as the clone directory + suffix := strconv.FormatInt(timestamp, 10) + sessionPath := m.dirFormatter.CreateSessionPath(filepath.Join(m.baseDir, org), aiModel, repo, prNumber, suffix) // Get current branch information currentBranch, err := m.gitService.GetCurrentBranch(clonePath) @@ -583,17 +741,21 @@ func (m *Manager) recoverPRWorkspaceFromClone(org, repo, clonePath, remoteURL st currentBranch = "" } + // Try to find existing MCP config file + mcpConfigPath := m.findExistingMCPConfig(filepath.Dir(sessionPath)) + // Recover workspace object ws := &models.Workspace{ - Org: org, - Repo: repo, - AIModel: aiModel, - Path: clonePath, - PRNumber: prNumber, - SessionPath: sessionPath, - Repository: remoteURL, - Branch: currentBranch, - CreatedAt: time.Unix(timestamp, 0), + Org: org, + Repo: repo, + AIModel: aiModel, + Path: clonePath, + PRNumber: prNumber, + SessionPath: sessionPath, + MCPConfigPath: mcpConfigPath, + Repository: remoteURL, + Branch: currentBranch, + CreatedAt: time.Unix(timestamp, 0), } // Store in repository @@ -604,3 +766,21 @@ func (m *Manager) recoverPRWorkspaceFromClone(org, repo, clonePath, remoteURL st log.Infof("Recovered PR workspace from clone: %v", ws) return nil } + +// syncPRContentIfStale force syncs PR content to ensure it's up to date +func (m *Manager) syncPRContentIfStale(ws *models.Workspace, pr *github.PullRequest) error { + // Only sync for PR workspaces that are on PR branches + if ws.PRNumber == 0 || !strings.HasPrefix(ws.Branch, "pr-") { + return nil + } + + log.Infof("Force syncing PR #%d content in workspace to ensure it's up to date: %s", pr.GetNumber(), ws.Path) + + // Force fetch and checkout PR content to handle any updates/force pushes + if err := m.gitService.FetchAndCheckoutPR(ws.Path, pr.GetNumber()); err != nil { + return fmt.Errorf("failed to force sync PR content: %w", err) + } + + log.Infof("Successfully synced PR #%d content in workspace", pr.GetNumber()) + return nil +} diff --git a/internal/workspace/manager_test.go b/internal/workspace/manager_test.go index 6d7a327..3a00662 100644 --- a/internal/workspace/manager_test.go +++ b/internal/workspace/manager_test.go @@ -798,6 +798,11 @@ func (m *mockGitService) CreateTrackingBranch(repoPath, branchName string) error return nil } +func (m *mockGitService) FetchAndCheckoutPR(repoPath string, prNumber int) error { + // Mock implementation for testing + return nil +} + // TestIssueWorkspaceReuse tests the Issue workspace reuse mechanism func TestIssueWorkspaceReuse(t *testing.T) { // Setup test environment diff --git a/internal/workspace/repo_manager.go b/internal/workspace/repo_manager.go deleted file mode 100644 index 0d0261d..0000000 --- a/internal/workspace/repo_manager.go +++ /dev/null @@ -1,4 +0,0 @@ -// This file was removed as part of the migration from git worktree to git clone approach. -// The RepoManager and related worktree functionality has been replaced with direct git clones. -// See issue #321 for details. -package workspace diff --git a/pkg/models/workspace.go b/pkg/models/workspace.go index d795aee..b7642a8 100644 --- a/pkg/models/workspace.go +++ b/pkg/models/workspace.go @@ -19,6 +19,8 @@ type Workspace struct { Path string `json:"path"` // session path in local file system SessionPath string `json:"session_path"` + // MCP configuration file path in local file system + MCPConfigPath string `json:"mcp_config_path,omitempty"` // processed .codeagent directory path with GitHub context applied ProcessedCodeAgentPath string `json:"processed_codeagent_path,omitempty"` // github repo url