diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 3e06628..344f395 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1226,16 +1226,16 @@ func (c *CLI) initRepo(args []string) error { // Check if repository is already initialized st, err := state.Load(c.paths.StateFile) if err != nil { - return fmt.Errorf("failed to load state: %w", err) + return errors.StateLoadFailed(err) } if _, exists := st.GetRepo(repoName); exists { - return fmt.Errorf("repository '%s' is already initialized\nUse 'multiclaude repo rm %s' to remove it first, or choose a different name", repoName, repoName) + return errors.RepoAlreadyExists(repoName) } // Check if tmux session already exists (stale session from previous incomplete init) tmuxSession := sanitizeTmuxSessionName(repoName) if tmuxSession == "mc-" { - return fmt.Errorf("invalid tmux session name: repository name cannot be empty") + return errors.InvalidTmuxSessionName("repository name cannot be empty") } tmuxClient := tmux.NewClient() if exists, err := tmuxClient.HasSession(context.Background(), tmuxSession); err == nil && exists { @@ -1243,7 +1243,7 @@ func (c *CLI) initRepo(args []string) error { fmt.Printf("This may be from a previous incomplete initialization.\n") fmt.Printf("Auto-repairing: killing existing tmux session...\n") if err := tmuxClient.KillSession(context.Background(), tmuxSession); err != nil { - return fmt.Errorf("failed to clean up existing tmux session: %w\nPlease manually kill it with: tmux kill-session -t %s", err, tmuxSession) + return errors.TmuxSessionCleanupNeeded(tmuxSession, err) } fmt.Println("✓ Cleaned up stale tmux session") } @@ -1251,7 +1251,7 @@ func (c *CLI) initRepo(args []string) error { // Check if repository directory already exists repoPath := c.paths.RepoDir(repoName) if _, err := os.Stat(repoPath); err == nil { - return fmt.Errorf("directory already exists: %s\nRemove it manually or choose a different name", repoPath) + return errors.DirectoryAlreadyExists(repoPath) } // Clone repository @@ -1331,38 +1331,38 @@ func (c *CLI) initRepo(args []string) error { // Generate session IDs for agents supervisorSessionID, err := claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate supervisor session ID: %w", err) + return errors.SessionIDGenerationFailed("supervisor", err) } var mergeQueueSessionID, prShepherdSessionID string if mqEnabled { mergeQueueSessionID, err = claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate merge-queue session ID: %w", err) + return errors.SessionIDGenerationFailed("merge-queue", err) } } else if psEnabled { prShepherdSessionID, err = claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate pr-shepherd session ID: %w", err) + return errors.SessionIDGenerationFailed("pr-shepherd", err) } } // Write prompt files supervisorPromptFile, err := c.writePromptFile(repoPath, state.AgentTypeSupervisor, "supervisor") if err != nil { - return fmt.Errorf("failed to write supervisor prompt: %w", err) + return errors.PromptWriteFailed("supervisor", err) } var mergeQueuePromptFile, prShepherdPromptFile string if mqEnabled { mergeQueuePromptFile, err = c.writeMergeQueuePromptFile(repoPath, "merge-queue", mqConfig) if err != nil { - return fmt.Errorf("failed to write merge-queue prompt: %w", err) + return errors.PromptWriteFailed("merge-queue", err) } } else if psEnabled { prShepherdPromptFile, err = c.writePRShepherdPromptFile(repoPath, "pr-shepherd", psConfig, forkConfig) if err != nil { - return fmt.Errorf("failed to write pr-shepherd prompt: %w", err) + return errors.PromptWriteFailed("pr-shepherd", err) } } @@ -1377,13 +1377,13 @@ func (c *CLI) initRepo(args []string) error { // Resolve claude binary claudeBinary, err := c.getClaudeBinary() if err != nil { - return fmt.Errorf("failed to resolve claude binary: %w", err) + return errors.ClaudeNotFound(err) } fmt.Println("Starting Claude Code in supervisor window...") pid, err := c.startClaudeInTmux(claudeBinary, tmuxSession, "supervisor", repoPath, supervisorSessionID, supervisorPromptFile, repoName, "") if err != nil { - return fmt.Errorf("failed to start supervisor Claude: %w", err) + return errors.ClaudeStartFailed("supervisor", err) } supervisorPID = pid @@ -1397,7 +1397,7 @@ func (c *CLI) initRepo(args []string) error { fmt.Println("Starting Claude Code in merge-queue window...") pid, err = c.startClaudeInTmux(claudeBinary, tmuxSession, "merge-queue", repoPath, mergeQueueSessionID, mergeQueuePromptFile, repoName, "") if err != nil { - return fmt.Errorf("failed to start merge-queue Claude: %w", err) + return errors.ClaudeStartFailed("merge-queue", err) } mergeQueuePID = pid @@ -1409,7 +1409,7 @@ func (c *CLI) initRepo(args []string) error { fmt.Println("Starting Claude Code in pr-shepherd window...") pid, err = c.startClaudeInTmux(claudeBinary, tmuxSession, "pr-shepherd", repoPath, prShepherdSessionID, prShepherdPromptFile, repoName, "") if err != nil { - return fmt.Errorf("failed to start pr-shepherd Claude: %w", err) + return errors.ClaudeStartFailed("pr-shepherd", err) } prShepherdPID = pid @@ -1441,10 +1441,10 @@ func (c *CLI) initRepo(args []string) error { Args: addRepoArgs, }) if err != nil { - return fmt.Errorf("failed to register repository with daemon: %w", err) + return errors.AgentRegistrationFailed("repository", err) } if !resp.Success { - return fmt.Errorf("failed to register repository: %s", resp.Error) + return errors.AgentRegistrationFailed("repository", fmt.Errorf("%s", resp.Error)) } // Add supervisor agent @@ -1461,10 +1461,10 @@ func (c *CLI) initRepo(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register supervisor: %w", err) + return errors.AgentRegistrationFailed("supervisor", err) } if !resp.Success { - return fmt.Errorf("failed to register supervisor: %s", resp.Error) + return errors.AgentRegistrationFailed("supervisor", fmt.Errorf("%s", resp.Error)) } // Add merge-queue agent only if enabled (non-fork mode) @@ -1482,10 +1482,10 @@ func (c *CLI) initRepo(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register merge-queue: %w", err) + return errors.AgentRegistrationFailed("merge-queue", err) } if !resp.Success { - return fmt.Errorf("failed to register merge-queue: %s", resp.Error) + return errors.AgentRegistrationFailed("merge-queue", fmt.Errorf("%s", resp.Error)) } } @@ -1504,10 +1504,10 @@ func (c *CLI) initRepo(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register pr-shepherd: %w", err) + return errors.AgentRegistrationFailed("pr-shepherd", err) } if !resp.Success { - return fmt.Errorf("failed to register pr-shepherd: %s", resp.Error) + return errors.AgentRegistrationFailed("pr-shepherd", fmt.Errorf("%s", resp.Error)) } } @@ -1522,9 +1522,9 @@ func (c *CLI) initRepo(args []string) error { // Check if it's a conflict state that requires manual resolution hasConflict, suggestion, checkErr := wt.CheckWorkspaceBranchConflict() if checkErr == nil && hasConflict { - return fmt.Errorf("workspace branch conflict detected:\n%s", suggestion) + return errors.New(errors.CategoryConfig, fmt.Sprintf("workspace branch conflict detected:\n%s", suggestion)) } - return fmt.Errorf("failed to check workspace branch state: %w", err) + return errors.Wrap(errors.CategoryRuntime, "failed to check workspace branch state", err) } if migrated { fmt.Println("Migrated legacy 'workspace' branch to 'workspace/default'") @@ -1533,25 +1533,25 @@ func (c *CLI) initRepo(args []string) error { fmt.Printf("Creating default workspace worktree at: %s\n", workspacePath) if err := wt.CreateNewBranch(workspacePath, workspaceBranch, "HEAD"); err != nil { - return fmt.Errorf("failed to create default workspace worktree: %w", err) + return errors.WorktreeCreationFailed(err) } // Create default workspace tmux window (detached so it doesn't switch focus) cmd = exec.Command("tmux", "new-window", "-d", "-t", tmuxSession, "-n", "default", "-c", workspacePath) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create workspace window: %w", err) + return errors.TmuxOperationFailed("create window", err) } // Generate session ID for workspace workspaceSessionID, err := claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate workspace session ID: %w", err) + return errors.SessionIDGenerationFailed("workspace", err) } // Write prompt file for default workspace workspacePromptFile, err := c.writePromptFile(repoPath, state.AgentTypeWorkspace, "default") if err != nil { - return fmt.Errorf("failed to write default workspace prompt: %w", err) + return errors.PromptWriteFailed("workspace", err) } // Copy hooks configuration if it exists @@ -1565,13 +1565,13 @@ func (c *CLI) initRepo(args []string) error { // Resolve claude binary claudeBinary, err := c.getClaudeBinary() if err != nil { - return fmt.Errorf("failed to resolve claude binary: %w", err) + return errors.ClaudeNotFound(err) } fmt.Println("Starting Claude Code in default workspace window...") pid, err := c.startClaudeInTmux(claudeBinary, tmuxSession, "default", workspacePath, workspaceSessionID, workspacePromptFile, repoName, "") if err != nil { - return fmt.Errorf("failed to start default workspace Claude: %w", err) + return errors.ClaudeStartFailed("default workspace", err) } workspacePID = pid @@ -1595,10 +1595,10 @@ func (c *CLI) initRepo(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register default workspace: %w", err) + return errors.AgentRegistrationFailed("default workspace", err) } if !resp.Success { - return fmt.Errorf("failed to register default workspace: %s", resp.Error) + return errors.AgentRegistrationFailed("default workspace", fmt.Errorf("%s", resp.Error)) } fmt.Println() @@ -2224,7 +2224,7 @@ func (c *CLI) createWorker(args []string) error { // Generate session ID for worker workerSessionID, err := claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate worker session ID: %w", err) + return errors.SessionIDGenerationFailed("worker", err) } // Get fork config from daemon to include in worker prompt @@ -2255,7 +2255,7 @@ func (c *CLI) createWorker(args []string) error { } workerPromptFile, err := c.writeWorkerPromptFile(repoPath, workerName, workerConfig) if err != nil { - return fmt.Errorf("failed to write worker prompt: %w", err) + return errors.PromptWriteFailed("worker", err) } // Copy hooks configuration if it exists @@ -2269,14 +2269,14 @@ func (c *CLI) createWorker(args []string) error { // Resolve claude binary claudeBinary, err := c.getClaudeBinary() if err != nil { - return fmt.Errorf("failed to resolve claude binary: %w", err) + return errors.ClaudeNotFound(err) } fmt.Println("Starting Claude Code in worker window...") initialMessage := fmt.Sprintf("Task: %s", task) pid, err := c.startClaudeInTmux(claudeBinary, tmuxSession, workerName, wtPath, workerSessionID, workerPromptFile, repoName, initialMessage) if err != nil { - return fmt.Errorf("failed to start worker Claude: %w", err) + return errors.ClaudeStartFailed("worker", err) } workerPID = pid @@ -2301,10 +2301,10 @@ func (c *CLI) createWorker(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register worker: %w", err) + return errors.AgentRegistrationFailed("worker", err) } if !resp.Success { - return fmt.Errorf("failed to register worker: %s", resp.Error) + return errors.AgentRegistrationFailed("worker", fmt.Errorf("%s", resp.Error)) } fmt.Println() @@ -3297,7 +3297,7 @@ func (c *CLI) addWorkspace(args []string) error { agentType, _ := agentMap["type"].(string) name, _ := agentMap["name"].(string) if agentType == "workspace" && name == workspaceName { - return fmt.Errorf("workspace '%s' already exists in repo '%s'", workspaceName, repoName) + return errors.WorkspaceAlreadyExists(workspaceName, repoName) } } } @@ -3316,7 +3316,7 @@ func (c *CLI) addWorkspace(args []string) error { fmt.Printf("This may be from a previous incomplete workspace creation.\n") fmt.Printf("Auto-repairing: removing existing worktree...\n") if err := wt.Remove(wtPath, true); err != nil { - return fmt.Errorf("failed to clean up existing worktree: %w\nPlease manually remove it with: git worktree remove %s", err, wtPath) + return errors.WorktreeCleanupNeeded(wtPath, err) } fmt.Println("✓ Cleaned up stale worktree") } @@ -3336,7 +3336,7 @@ func (c *CLI) addWorkspace(args []string) error { fmt.Printf("This may be from a previous incomplete workspace creation.\n") fmt.Printf("Auto-repairing: killing existing tmux window...\n") if err := tmuxClient.KillWindow(context.Background(), tmuxSession, workspaceName); err != nil { - return fmt.Errorf("failed to clean up existing tmux window: %w\nPlease manually kill it with: tmux kill-window -t %s:%s", err, tmuxSession, workspaceName) + return errors.TmuxWindowCleanupNeeded(tmuxSession, workspaceName, err) } fmt.Println("✓ Cleaned up stale tmux window") } @@ -3351,13 +3351,13 @@ func (c *CLI) addWorkspace(args []string) error { // Generate session ID for workspace workspaceSessionID, err := claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate workspace session ID: %w", err) + return errors.SessionIDGenerationFailed("workspace", err) } // Write prompt file for workspace workspacePromptFile, err := c.writePromptFile(repoPath, state.AgentTypeWorkspace, workspaceName) if err != nil { - return fmt.Errorf("failed to write workspace prompt: %w", err) + return errors.PromptWriteFailed("workspace", err) } // Copy hooks configuration if it exists @@ -3371,13 +3371,13 @@ func (c *CLI) addWorkspace(args []string) error { // Resolve claude binary claudeBinary, err := c.getClaudeBinary() if err != nil { - return fmt.Errorf("failed to resolve claude binary: %w", err) + return errors.ClaudeNotFound(err) } fmt.Println("Starting Claude Code in workspace window...") pid, err := c.startClaudeInTmux(claudeBinary, tmuxSession, workspaceName, wtPath, workspaceSessionID, workspacePromptFile, repoName, "") if err != nil { - return fmt.Errorf("failed to start workspace Claude: %w", err) + return errors.ClaudeStartFailed("workspace", err) } workspacePID = pid @@ -3401,10 +3401,10 @@ func (c *CLI) addWorkspace(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register workspace: %w", err) + return errors.AgentRegistrationFailed("workspace", err) } if !resp.Success { - return fmt.Errorf("failed to register workspace: %s", resp.Error) + return errors.AgentRegistrationFailed("workspace", fmt.Errorf("%s", resp.Error)) } fmt.Println() @@ -3713,7 +3713,7 @@ func (c *CLI) connectWorkspace(args []string) error { // validateWorkspaceName validates that a workspace name follows branch name restrictions func validateWorkspaceName(name string) error { if name == "" { - return fmt.Errorf("workspace name cannot be empty") + return errors.InvalidWorkspaceName(name, "cannot be empty") } // Git branch name restrictions @@ -3724,25 +3724,25 @@ func validateWorkspaceName(name string) error { // - Cannot be "." or ".." if name == "." || name == ".." { - return fmt.Errorf("workspace name cannot be '.' or '..'") + return errors.InvalidWorkspaceName(name, "cannot be '.' or '..'") } if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "-") { - return fmt.Errorf("workspace name cannot start with '.' or '-'") + return errors.InvalidWorkspaceName(name, "cannot start with '.' or '-'") } if strings.HasSuffix(name, ".") || strings.HasSuffix(name, "/") { - return fmt.Errorf("workspace name cannot end with '.' or '/'") + return errors.InvalidWorkspaceName(name, "cannot end with '.' or '/'") } if strings.Contains(name, "..") { - return fmt.Errorf("workspace name cannot contain '..'") + return errors.InvalidWorkspaceName(name, "cannot contain '..'") } invalidChars := []string{"\\", "~", "^", ":", "?", "*", "[", "@", "{", "}", " ", "\t", "\n"} for _, char := range invalidChars { if strings.Contains(name, char) { - return fmt.Errorf("workspace name cannot contain '%s'", char) + return errors.InvalidWorkspaceName(name, fmt.Sprintf("cannot contain '%s'", char)) } } @@ -4066,7 +4066,7 @@ func (c *CLI) resolveRepo(flags map[string]string) (string, error) { } } - return "", fmt.Errorf("could not determine repository; use --repo flag or run 'multiclaude repo use '") + return "", errors.NoDefaultRepo() } // inferAgentContext infers the current agent and repo from working directory @@ -4386,19 +4386,19 @@ func (c *CLI) reviewPR(args []string) error { fmt.Printf("Creating tmux window: %s\n", reviewerName) cmd = exec.Command("tmux", "new-window", "-d", "-t", tmuxSession, "-n", reviewerName, "-c", wtPath) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create tmux window: %w", err) + return errors.TmuxOperationFailed("create window", err) } // Generate session ID for reviewer reviewerSessionID, err := claude.GenerateSessionID() if err != nil { - return fmt.Errorf("failed to generate reviewer session ID: %w", err) + return errors.SessionIDGenerationFailed("reviewer", err) } // Write prompt file for reviewer reviewerPromptFile, err := c.writePromptFile(repoPath, state.AgentTypeReview, reviewerName) if err != nil { - return fmt.Errorf("failed to write reviewer prompt: %w", err) + return errors.PromptWriteFailed("reviewer", err) } // Copy hooks configuration if it exists @@ -4412,14 +4412,14 @@ func (c *CLI) reviewPR(args []string) error { // Resolve claude binary claudeBinary, err := c.getClaudeBinary() if err != nil { - return fmt.Errorf("failed to resolve claude binary: %w", err) + return errors.ClaudeNotFound(err) } fmt.Println("Starting Claude Code in reviewer window...") initialMessage := fmt.Sprintf("Review PR #%s: https://github.com/%s/%s/pull/%s", prNumber, parts[1], parts[2], prNumber) pid, err := c.startClaudeInTmux(claudeBinary, tmuxSession, reviewerName, wtPath, reviewerSessionID, reviewerPromptFile, repoName, initialMessage) if err != nil { - return fmt.Errorf("failed to start reviewer Claude: %w", err) + return errors.ClaudeStartFailed("reviewer", err) } reviewerPID = pid @@ -4445,10 +4445,10 @@ func (c *CLI) reviewPR(args []string) error { }, }) if err != nil { - return fmt.Errorf("failed to register reviewer: %w", err) + return errors.AgentRegistrationFailed("reviewer", err) } if !resp.Success { - return fmt.Errorf("failed to register reviewer: %s", resp.Error) + return errors.AgentRegistrationFailed("reviewer", fmt.Errorf("%s", resp.Error)) } fmt.Println() @@ -4498,7 +4498,7 @@ func (c *CLI) viewLogs(args []string) error { } else if _, err := os.Stat(systemLogFile); err == nil { logFile = systemLogFile } else { - return fmt.Errorf("no log file found for agent %s in repo %s", agentName, repoName) + return errors.LogFileNotFound(agentName, repoName) } // Check for --follow flag @@ -4666,13 +4666,13 @@ func (c *CLI) cleanLogs(args []string) error { olderThan, ok := flags["older-than"] if !ok { - return fmt.Errorf("usage: multiclaude logs clean --older-than (e.g., 7d, 24h)") + return errors.InvalidUsage("usage: multiclaude logs clean --older-than (e.g., 7d, 24h)") } // Parse duration duration, err := parseDuration(olderThan) if err != nil { - return fmt.Errorf("invalid duration: %v", err) + return errors.InvalidDuration(olderThan) } cutoff := time.Now().Add(-duration) diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 6798ebd..0ec601f 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -391,3 +391,173 @@ func WorkspaceNotFound(name, repo string) *CLIError { Suggestion: fmt.Sprintf("multiclaude workspace list --repo %s", repo), } } + +// RepoAlreadyExists creates an error for when trying to init an already tracked repo +func RepoAlreadyExists(name string) *CLIError { + return &CLIError{ + Category: CategoryConfig, + Message: fmt.Sprintf("repository '%s' is already initialized", name), + Suggestion: fmt.Sprintf("multiclaude repo rm %s # to remove and re-init", name), + } +} + +// DirectoryAlreadyExists creates an error for when a directory already exists +func DirectoryAlreadyExists(path string) *CLIError { + return &CLIError{ + Category: CategoryConfig, + Message: fmt.Sprintf("directory already exists: %s", path), + Suggestion: "remove the directory manually or choose a different name", + } +} + +// WorkspaceAlreadyExists creates an error for when a workspace already exists +func WorkspaceAlreadyExists(name, repo string) *CLIError { + return &CLIError{ + Category: CategoryConfig, + Message: fmt.Sprintf("workspace '%s' already exists in repo '%s'", name, repo), + Suggestion: fmt.Sprintf("multiclaude workspace list --repo %s", repo), + } +} + +// InvalidWorkspaceName creates an error for invalid workspace names +func InvalidWorkspaceName(name, reason string) *CLIError { + return &CLIError{ + Category: CategoryUsage, + Message: fmt.Sprintf("invalid workspace name '%s': %s", name, reason), + Suggestion: "workspace names should be alphanumeric with hyphens or underscores (e.g., 'my-workspace')", + } +} + +// LogFileNotFound creates an error for when no log file exists for an agent +func LogFileNotFound(agent, repo string) *CLIError { + return &CLIError{ + Category: CategoryNotFound, + Message: fmt.Sprintf("no log file found for agent '%s' in repo '%s'", agent, repo), + Suggestion: "the agent may not have been started yet or logs may have been cleaned up", + } +} + +// InvalidDuration creates an error for invalid duration format +func InvalidDuration(value string) *CLIError { + return &CLIError{ + Category: CategoryUsage, + Message: fmt.Sprintf("invalid duration: %s", value), + Suggestion: "use format like '7d' (days), '24h' (hours), or '30m' (minutes)", + } +} + +// NoDefaultRepo creates an error for when no default repo is set and multiple exist +func NoDefaultRepo() *CLIError { + return &CLIError{ + Category: CategoryUsage, + Message: "could not determine which repository to use", + Suggestion: "use --repo flag, run 'multiclaude repo use ' to set a default, or run from within a tracked repository", + } +} + +// StateLoadFailed creates an error for when state cannot be loaded +func StateLoadFailed(cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: "failed to load multiclaude state", + Cause: cause, + Suggestion: "try 'multiclaude repair' to fix corrupted state", + } +} + +// SessionIDGenerationFailed creates an error for UUID generation failures +func SessionIDGenerationFailed(agentType string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: fmt.Sprintf("failed to generate session ID for %s", agentType), + Cause: cause, + Suggestion: "this is usually a transient error; try again", + } +} + +// PromptWriteFailed creates an error for prompt file write failures +func PromptWriteFailed(agentType string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: fmt.Sprintf("failed to write %s prompt file", agentType), + Cause: cause, + Suggestion: "check disk space and permissions in ~/.multiclaude/", + } +} + +// ClaudeStartFailed creates an error for Claude startup failures +func ClaudeStartFailed(agentType string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: fmt.Sprintf("failed to start %s Claude instance", agentType), + Cause: cause, + Suggestion: "check 'claude --version' works and tmux is running", + } +} + +// AgentRegistrationFailed creates an error for agent registration failures +func AgentRegistrationFailed(agentType string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: fmt.Sprintf("failed to register %s with daemon", agentType), + Cause: cause, + Suggestion: "multiclaude daemon status", + } +} + +// WorktreeCleanupNeeded creates an error when manual worktree cleanup is needed +func WorktreeCleanupNeeded(path string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: "failed to clean up existing worktree", + Cause: cause, + Suggestion: fmt.Sprintf("git worktree remove %s", path), + } +} + +// TmuxWindowCleanupNeeded creates an error when manual tmux cleanup is needed +func TmuxWindowCleanupNeeded(session, window string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: "failed to clean up existing tmux window", + Cause: cause, + Suggestion: fmt.Sprintf("tmux kill-window -t %s:%s", session, window), + } +} + +// TmuxSessionCleanupNeeded creates an error when manual tmux session cleanup is needed +func TmuxSessionCleanupNeeded(session string, cause error) *CLIError { + return &CLIError{ + Category: CategoryRuntime, + Message: "failed to clean up existing tmux session", + Cause: cause, + Suggestion: fmt.Sprintf("tmux kill-session -t %s", session), + } +} + +// InvalidTmuxSessionName creates an error for invalid tmux session names +func InvalidTmuxSessionName(reason string) *CLIError { + return &CLIError{ + Category: CategoryUsage, + Message: fmt.Sprintf("invalid tmux session name: %s", reason), + Suggestion: "repository name must not be empty and must be valid for tmux", + } +} + +// WorkerNotFound creates an error for when a worker is not found +func WorkerNotFound(name, repo string) *CLIError { + return &CLIError{ + Category: CategoryNotFound, + Message: fmt.Sprintf("worker '%s' not found in repo '%s'", name, repo), + Suggestion: fmt.Sprintf("multiclaude worker list --repo %s", repo), + } +} + +// AgentNoSessionID creates an error for agents without session IDs +func AgentNoSessionID(name string) *CLIError { + return &CLIError{ + Category: CategoryConfig, + Message: fmt.Sprintf("agent '%s' has no session ID", name), + Suggestion: "try removing and recreating the agent", + } +}