autom8-cli 0.3.0

CLI automation tool for orchestrating Claude-powered development
Documentation
use std::path::PathBuf;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Autom8Error {
    #[error("Spec file not found: {0}\n\nThe spec file does not exist at the specified path.\n\nTo fix this:\n  1. Check that the path is correct\n  2. Run 'autom8 init' to create the project structure\n  3. Create a spec file or use 'autom8' to generate one interactively")]
    SpecNotFound(PathBuf),

    #[error("Invalid spec format: {0}\n\nThe spec file exists but cannot be parsed.\n\nTo fix this:\n  1. Ensure the file is valid JSON or Markdown\n  2. Check for syntax errors (missing commas, brackets, etc.)\n  3. See CLAUDE.md for spec format requirements")]
    InvalidSpec(String),

    #[error("No incomplete stories found in spec\n\nAll user stories in the spec have passes: true.\n\nTo continue:\n  1. Add new user stories to the spec, or\n  2. Set passes: false on stories you want to re-implement")]
    NoIncompleteStories,

    #[error("Claude process failed: {0}")]
    ClaudeError(String),

    #[error("Claude process timed out after {0} seconds")]
    ClaudeTimeout(u64),

    #[error("State file error: {0}")]
    StateError(String),

    #[error("No active run to resume\n\nNo incomplete session was found for this project.\n\nTo start a new run:\n  1. Run 'autom8 spec.json' to start from a spec file, or\n  2. Run 'autom8' to create a new spec interactively, or\n  3. Use 'autom8 status --all' to check all sessions")]
    NoActiveRun,

    #[error("Run already in progress: {0}\n\nAnother session is actively running for this project.\n\nTo resolve this:\n  1. Wait for the current run to complete, or\n  2. Use --worktree to run in a separate worktree, or\n  3. Use 'autom8 status --all' to see all sessions")]
    RunInProgress(String),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Git error: {0}")]
    GitError(String),

    #[error("Spec markdown file not found: {0}")]
    SpecMarkdownNotFound(PathBuf),

    #[error("Spec file is empty")]
    EmptySpec,

    #[error("Spec generation failed: {0}")]
    SpecGenerationFailed(String),

    #[error("Invalid generated spec: {0}")]
    InvalidGeneratedSpec(String),

    #[error("Configuration error: {0}")]
    Config(String),

    #[error("Review failed after 3 iterations. Please manually review autom8_review.md for remaining issues.")]
    MaxReviewIterationsReached,

    #[error("No incomplete specs found in spec/\n\nNo spec files with incomplete user stories were found.\n\nTo start a new run:\n  1. Run 'autom8' to create a new spec interactively, or\n  2. Add a spec file to ~/.config/autom8/<project>/spec/, or\n  3. Set passes: false on stories you want to re-implement")]
    NoSpecsToResume,

    #[error("Shell completion error: {0}")]
    ShellCompletion(String),

    #[error("Worktree error: {0}")]
    WorktreeError(String),

    #[error("Branch conflict: branch '{branch}' is already in use by session '{session_id}' at {worktree_path}.\n\nThe branch is checked out in another worktree session.\n\nTo resolve this:\n  1. Wait for the other session to complete, or\n  2. Use a different branch name in your spec, or\n  3. Resume the existing session: autom8 resume --session {session_id}, or\n  4. Clean up the conflicting session: autom8 clean --session {session_id}")]
    BranchConflict {
        branch: String,
        session_id: String,
        worktree_path: std::path::PathBuf,
    },

    #[error("Signal handler error: {0}")]
    SignalHandler(String),

    #[error("Interrupted by user")]
    Interrupted,

    #[error("GUI error: {0}")]
    GuiError(String),

    #[error("Not in a git repository\n\nThe improve command must be run from within a git repository.\n\nTo fix this:\n  1. Navigate to your project directory: cd /path/to/your/project\n  2. Ensure the directory is a git repository (contains .git folder)\n  3. If not initialized, run: git init")]
    NotInGitRepo,

    #[error("Claude CLI not found\n\nThe 'claude' command could not be found in your PATH.\n\nTo fix this:\n  1. Install Claude Code: https://claude.ai/code\n  2. Ensure 'claude' is in your PATH\n  3. Try running 'claude --version' to verify installation")]
    ClaudeNotFound,

    #[error("Failed to spawn Claude: {0}\n\nCould not start the Claude CLI process.\n\nTo fix this:\n  1. Check that 'claude' is installed and working\n  2. Ensure you have permissions to run the command\n  3. Try running 'claude' manually to diagnose the issue")]
    ClaudeSpawnError(String),
}

pub type Result<T> = std::result::Result<T, Autom8Error>;

#[cfg(test)]
mod tests {
    use super::*;

    // ========================================================================
    // Error message format tests (US-012)
    // ========================================================================

    #[test]
    fn test_us012_branch_conflict_error_includes_what_happened() {
        let err = Autom8Error::BranchConflict {
            branch: "feature/test".to_string(),
            session_id: "abc123".to_string(),
            worktree_path: PathBuf::from("/path/to/worktree"),
        };
        let msg = err.to_string();

        // What happened
        assert!(
            msg.contains("Branch conflict"),
            "Error should describe what happened"
        );
        assert!(
            msg.contains("feature/test"),
            "Error should include branch name"
        );
    }

    #[test]
    fn test_us012_branch_conflict_error_includes_why() {
        let err = Autom8Error::BranchConflict {
            branch: "feature/test".to_string(),
            session_id: "abc123".to_string(),
            worktree_path: PathBuf::from("/path/to/worktree"),
        };
        let msg = err.to_string();

        // Why it happened
        assert!(
            msg.contains("already in use") || msg.contains("checked out"),
            "Error should explain why"
        );
    }

    #[test]
    fn test_us012_branch_conflict_error_includes_how_to_fix() {
        let err = Autom8Error::BranchConflict {
            branch: "feature/test".to_string(),
            session_id: "abc123".to_string(),
            worktree_path: PathBuf::from("/path/to/worktree"),
        };
        let msg = err.to_string();

        // How to fix - multiple options
        assert!(
            msg.contains("To resolve"),
            "Error should include resolution steps"
        );
        assert!(
            msg.contains("autom8 resume"),
            "Error should suggest resume command"
        );
        assert!(
            msg.contains("autom8 clean"),
            "Error should suggest clean command"
        );
        assert!(
            msg.contains("abc123"),
            "Error should include session ID for commands"
        );
    }

    #[test]
    fn test_us012_spec_not_found_error_includes_fix() {
        let err = Autom8Error::SpecNotFound(PathBuf::from("/missing/spec.json"));
        let msg = err.to_string();

        assert!(
            msg.contains("not found"),
            "Error should describe what happened"
        );
        assert!(msg.contains("To fix"), "Error should include fix steps");
        assert!(
            msg.contains("autom8 init") || msg.contains("init"),
            "Error should suggest init command"
        );
    }

    #[test]
    fn test_us012_no_active_run_error_includes_fix() {
        let err = Autom8Error::NoActiveRun;
        let msg = err.to_string();

        assert!(
            msg.contains("No active run"),
            "Error should describe what happened"
        );
        assert!(
            msg.contains("To start") || msg.contains("To fix"),
            "Error should include fix steps"
        );
        assert!(
            msg.contains("autom8 status"),
            "Error should suggest status command"
        );
    }

    #[test]
    fn test_us012_run_in_progress_error_includes_fix() {
        let err = Autom8Error::RunInProgress("session123".to_string());
        let msg = err.to_string();

        assert!(
            msg.contains("in progress"),
            "Error should describe what happened"
        );
        assert!(msg.contains("To resolve"), "Error should include fix steps");
        assert!(
            msg.contains("--worktree"),
            "Error should suggest worktree option"
        );
    }

    #[test]
    fn test_us012_no_incomplete_stories_error_includes_fix() {
        let err = Autom8Error::NoIncompleteStories;
        let msg = err.to_string();

        assert!(
            msg.contains("No incomplete stories"),
            "Error should describe what happened"
        );
        assert!(
            msg.contains("To continue"),
            "Error should include fix steps"
        );
        assert!(
            msg.contains("passes: false"),
            "Error should suggest how to re-run stories"
        );
    }

    #[test]
    fn test_us012_no_specs_to_resume_error_includes_fix() {
        let err = Autom8Error::NoSpecsToResume;
        let msg = err.to_string();

        assert!(
            msg.contains("No incomplete specs"),
            "Error should describe what happened"
        );
        assert!(msg.contains("To start"), "Error should include fix steps");
    }

    #[test]
    fn test_us012_invalid_spec_error_includes_fix() {
        let err = Autom8Error::InvalidSpec("missing field 'id'".to_string());
        let msg = err.to_string();

        assert!(
            msg.contains("Invalid spec format"),
            "Error should describe what happened"
        );
        assert!(msg.contains("To fix"), "Error should include fix steps");
        assert!(
            msg.contains("JSON") || msg.contains("syntax"),
            "Error should mention format"
        );
    }

    // ========================================================================
    // US-009: NotInGitRepo error tests (improve command edge case)
    // ========================================================================

    #[test]
    fn test_us009_not_in_git_repo_error_includes_what_happened() {
        let err = Autom8Error::NotInGitRepo;
        let msg = err.to_string();

        assert!(
            msg.contains("Not in a git repository"),
            "Error should describe what happened"
        );
    }

    #[test]
    fn test_us009_not_in_git_repo_error_includes_why() {
        let err = Autom8Error::NotInGitRepo;
        let msg = err.to_string();

        assert!(
            msg.contains("must be run from within a git repository"),
            "Error should explain why"
        );
    }

    #[test]
    fn test_us009_not_in_git_repo_error_includes_fix() {
        let err = Autom8Error::NotInGitRepo;
        let msg = err.to_string();

        assert!(msg.contains("To fix"), "Error should include fix steps");
        assert!(
            msg.contains("cd") || msg.contains("Navigate"),
            "Error should suggest changing directory"
        );
        assert!(
            msg.contains(".git") || msg.contains("git init"),
            "Error should mention git initialization"
        );
    }
}