Skip to main content

xchecker_utils/
error.rs

1use std::fmt;
2use std::io;
3use std::path::PathBuf;
4use std::time::Duration;
5use thiserror::Error;
6pub use xchecker_lock::LockError;
7
8/// Library-level error type with rich context and user-friendly reporting.
9///
10/// `XCheckerError` is the primary error type returned by xchecker library operations.
11/// It provides:
12/// - Detailed error information for programmatic handling
13/// - User-friendly messages with context and suggestions
14/// - Mapping to CLI exit codes for consistent error reporting
15///
16/// # Error Categories
17///
18/// Errors are organized into categories for better handling:
19///
20/// | Category | Description |
21/// |----------|-------------|
22/// | `Config` | Configuration file or CLI argument errors |
23/// | `Phase` | Phase execution failures |
24/// | `Claude` | Claude CLI integration errors |
25/// | `Runner` | Process execution errors |
26/// | `SecretDetected` | Security: secrets found in content |
27/// | `PacketOverflow` | Resource: packet size exceeded |
28/// | `Lock` | Concurrency: lock already held |
29///
30/// # Exit Code Mapping
31///
32/// Use [`to_exit_code()`](Self::to_exit_code) to map errors to CLI exit codes:
33///
34/// | Exit Code | Error Type |
35/// |-----------|------------|
36/// | 2 | Configuration/CLI argument errors |
37/// | 7 | Packet overflow |
38/// | 8 | Secret detected |
39/// | 9 | Lock held |
40/// | 10 | Phase timeout |
41/// | 70 | Claude CLI failure |
42/// | 1 | Other errors |
43///
44/// # User-Friendly Messages
45///
46/// Use [`display_for_user()`](Self::display_for_user) to get formatted error messages
47/// suitable for end users, including context and actionable suggestions.
48///
49/// # Example
50///
51/// ```rust
52/// use xchecker_utils::error::XCheckerError;
53/// use xchecker_utils::exit_codes::ExitCode;
54///
55/// fn handle_error(err: XCheckerError) {
56///     // Get user-friendly message
57///     eprintln!("{}", err.display_for_user());
58///     
59///     // Map to exit code for CLI
60///     let code = err.to_exit_code();
61///     std::process::exit(code.as_i32());
62/// }
63/// ```
64///
65/// # Library vs CLI Usage
66///
67/// - **Library consumers**: Handle `XCheckerError` directly, use `to_exit_code()` if needed
68/// - **CLI**: Maps errors to exit codes and displays user-friendly messages
69///
70/// Library code returns `XCheckerError` and does NOT call `std::process::exit()`.
71#[derive(Error, Debug)]
72pub enum XCheckerError {
73    #[error("Configuration error: {0}")]
74    Config(#[from] ConfigError),
75
76    #[error("Phase execution error: {0}")]
77    Phase(#[from] PhaseError),
78
79    #[error("Claude CLI error: {0}")]
80    Claude(#[from] ClaudeError),
81
82    #[error("Runner error: {0}")]
83    Runner(#[from] RunnerError),
84
85    #[error("IO error: {0}")]
86    Io(#[from] std::io::Error),
87
88    #[error("Secret detected: {pattern} in {location}")]
89    SecretDetected { pattern: String, location: String },
90
91    #[error(
92        "Packet overflow: {used_bytes} bytes, {used_lines} lines > limits {limit_bytes} bytes, {limit_lines} lines"
93    )]
94    PacketOverflow {
95        used_bytes: usize,
96        used_lines: usize,
97        limit_bytes: usize,
98        limit_lines: usize,
99    },
100
101    #[error("Concurrent execution detected for spec {id}")]
102    ConcurrentExecution { id: String },
103
104    #[error("Packet preview too large: {size} bytes")]
105    PacketPreviewTooLarge { size: usize },
106
107    #[error("Canonicalization failed in {phase}: {reason}")]
108    CanonicalizationFailed { phase: String, reason: String },
109
110    #[error("Receipt write failed at {path}: {reason}")]
111    ReceiptWriteFailed { path: String, reason: String },
112
113    #[error("Model resolution error: alias '{alias}' -> '{resolved}': {reason}")]
114    ModelResolutionError {
115        alias: String,
116        resolved: String,
117        reason: String,
118    },
119
120    #[error("Source resolution error: {0}")]
121    Source(#[from] SourceError),
122
123    #[error("Fixup error: {0}")]
124    Fixup(#[from] FixupError),
125
126    #[error("Spec ID validation error: {0}")]
127    SpecId(#[from] SpecIdError),
128
129    #[error("File lock error: {0}")]
130    Lock(#[from] LockError),
131
132    #[error("LLM backend error: {0}")]
133    Llm(#[from] LlmError),
134
135    #[error("Validation failed for phase {phase}: {issue_count} issue(s)")]
136    ValidationFailed {
137        phase: String,
138        issues: Vec<ValidationError>,
139        issue_count: usize,
140    },
141}
142
143/// Trait for providing user-friendly error reporting with context and suggestions
144pub trait UserFriendlyError {
145    /// Get a user-friendly error message
146    fn user_message(&self) -> String;
147
148    /// Get contextual information about the error
149    fn context(&self) -> Option<String>;
150
151    /// Get suggested actions to resolve the error
152    fn suggestions(&self) -> Vec<String>;
153
154    /// Get the error category for grouping similar errors
155    fn category(&self) -> ErrorCategory;
156}
157
158/// Categories of errors for better organization and handling
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum ErrorCategory {
161    Configuration,
162    PhaseExecution,
163    ClaudeIntegration,
164    FileSystem,
165    Security,
166    ResourceLimits,
167    Concurrency,
168    Validation,
169}
170
171impl fmt::Display for ErrorCategory {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            Self::Configuration => write!(f, "Configuration"),
175            Self::PhaseExecution => write!(f, "Phase Execution"),
176            Self::ClaudeIntegration => write!(f, "Claude Integration"),
177            Self::FileSystem => write!(f, "File System"),
178            Self::Security => write!(f, "Security"),
179            Self::ResourceLimits => write!(f, "Resource Limits"),
180            Self::Concurrency => write!(f, "Concurrency"),
181            Self::Validation => write!(f, "Validation"),
182        }
183    }
184}
185
186/// Configuration-related errors
187#[derive(Error, Debug)]
188pub enum ConfigError {
189    #[error("Invalid configuration file: {0}")]
190    InvalidFile(String),
191
192    #[error("Missing required configuration: {0}")]
193    MissingRequired(String),
194
195    #[error("Invalid configuration value for {key}: {value}")]
196    InvalidValue { key: String, value: String },
197
198    #[error("Configuration file not found at {path}")]
199    NotFound { path: String },
200
201    #[error("Configuration discovery failed: {reason}")]
202    DiscoveryFailed { reason: String },
203
204    #[error("Configuration validation failed: {error_count} errors")]
205    ValidationFailed {
206        errors: Vec<String>,
207        error_count: usize,
208    },
209
210    #[error("Unsupported configuration version: {version}")]
211    UnsupportedVersion { version: String },
212}
213
214impl UserFriendlyError for ConfigError {
215    fn user_message(&self) -> String {
216        match self {
217            Self::InvalidFile(reason) => {
218                format!("Configuration file has invalid format: {reason}")
219            }
220            Self::MissingRequired(key) => {
221                format!("Required configuration '{key}' is missing")
222            }
223            Self::InvalidValue { key, value } => {
224                format!("Configuration '{key}' has invalid value: {value}")
225            }
226            Self::NotFound { path } => {
227                format!("Configuration file not found: {path}")
228            }
229            Self::DiscoveryFailed { reason } => {
230                format!("Failed to discover configuration: {reason}")
231            }
232            Self::ValidationFailed {
233                errors,
234                error_count: _,
235            } => {
236                format!(
237                    "Configuration validation failed with {} errors: {}",
238                    errors.len(),
239                    errors.join(", ")
240                )
241            }
242            Self::UnsupportedVersion { version } => {
243                format!(
244                    "Configuration version '{version}' is not supported by this version of xchecker"
245                )
246            }
247        }
248    }
249
250    fn context(&self) -> Option<String> {
251        match self {
252            Self::InvalidFile(_) => {
253                Some("Configuration files must be valid TOML format with [defaults] and [selectors] sections.".to_string())
254            }
255            Self::MissingRequired(_) => {
256                Some("Some configuration values are required for xchecker to function properly.".to_string())
257            }
258            Self::InvalidValue { key, value: _ } => {
259                Some(format!("The '{key}' configuration option has specific format requirements."))
260            }
261            Self::NotFound { path: _ } => {
262                Some("xchecker searches for .xchecker/config.toml starting from the current directory upward.".to_string())
263            }
264            Self::DiscoveryFailed { reason: _ } => {
265                Some("Configuration discovery involves searching the directory tree for .xchecker/config.toml files.".to_string())
266            }
267            Self::ValidationFailed { errors: _, error_count: _ } => {
268                Some("Configuration validation ensures all required sections and values are properly formatted.".to_string())
269            }
270            Self::UnsupportedVersion { version: _ } => {
271                Some("Configuration file format versions ensure compatibility between xchecker versions.".to_string())
272            }
273        }
274    }
275
276    fn suggestions(&self) -> Vec<String> {
277        match self {
278            Self::InvalidFile(_) => vec![
279                "Check the TOML syntax using a TOML validator".to_string(),
280                "Ensure the file has proper [defaults] and [selectors] sections".to_string(),
281                "Compare with the example configuration in the documentation".to_string(),
282            ],
283            Self::MissingRequired(key) => vec![
284                format!(
285                    "Add '{}' to the [defaults] section of .xchecker/config.toml",
286                    key
287                ),
288                "Check the documentation for required configuration options".to_string(),
289                "Use CLI flags as a temporary workaround".to_string(),
290            ],
291            Self::InvalidValue { key, value: _ } => match key.as_str() {
292                "model" => vec![
293                    "Use a valid Claude model name (e.g., 'haiku', 'sonnet', 'opus')".to_string(),
294                    "Check available models with 'claude models'".to_string(),
295                ],
296                "packet_max_bytes" | "packet_max_lines" => vec![
297                    "Use a positive integer value".to_string(),
298                    "Consider reasonable limits (e.g., 65536 bytes, 1200 lines)".to_string(),
299                ],
300                "source" => vec![
301                    "Use 'gh', 'fs', or 'stdin' as the source type".to_string(),
302                    "For GitHub: --source gh --gh owner/repo".to_string(),
303                    "For filesystem: --source fs --repo /path/to/repo".to_string(),
304                    "For stdin: --source stdin (default)".to_string(),
305                ],
306                "gh" => vec![
307                    "Use format 'owner/repo' for GitHub repositories".to_string(),
308                    "Example: --gh anthropic/claude-cli".to_string(),
309                    "Ensure the repository exists and is accessible".to_string(),
310                ],
311                "repo" => vec![
312                    "Provide a valid filesystem path".to_string(),
313                    "Ensure the directory exists and is readable".to_string(),
314                    "Use absolute or relative paths".to_string(),
315                ],
316                _ => vec![
317                    "Check the documentation for valid values for this option".to_string(),
318                    "Remove the option to use the default value".to_string(),
319                ],
320            },
321            Self::NotFound { path: _ } => vec![
322                "Create .xchecker/config.toml in your project root".to_string(),
323                "Use CLI flags instead of a configuration file".to_string(),
324                "Check that you're running xchecker from the correct directory".to_string(),
325            ],
326            Self::DiscoveryFailed { reason: _ } => vec![
327                "Check file permissions in the current directory and parent directories"
328                    .to_string(),
329                "Ensure you have read access to the directory tree".to_string(),
330                "Try running from a different directory with proper permissions".to_string(),
331                "Use --config <path> to specify configuration file explicitly".to_string(),
332            ],
333            Self::ValidationFailed {
334                errors: _,
335                error_count: _,
336            } => vec![
337                "Review the configuration file syntax and structure".to_string(),
338                "Check that all required sections ([defaults], [selectors]) are present"
339                    .to_string(),
340                "Validate TOML syntax using an online TOML validator".to_string(),
341                "Compare with the example configuration in documentation".to_string(),
342            ],
343            Self::UnsupportedVersion { version: _ } => vec![
344                "Update xchecker to the latest version".to_string(),
345                "Check the documentation for supported configuration versions".to_string(),
346                "Migrate configuration to the current format".to_string(),
347            ],
348        }
349    }
350
351    fn category(&self) -> ErrorCategory {
352        ErrorCategory::Configuration
353    }
354}
355
356/// Phase execution errors
357#[derive(Error, Debug)]
358pub enum PhaseError {
359    #[error("Phase {phase} failed with exit code {code}")]
360    ExecutionFailed { phase: String, code: i32 },
361
362    #[error("Phase {phase} dependency not satisfied: missing {dependency}")]
363    DependencyNotSatisfied { phase: String, dependency: String },
364
365    #[error("Invalid phase transition from {from} to {to}")]
366    InvalidTransition { from: String, to: String },
367
368    #[error("Phase {phase} output validation failed: {reason}")]
369    OutputValidationFailed { phase: String, reason: String },
370
371    #[error("Phase {phase} packet creation failed: {reason}")]
372    PacketCreationFailed { phase: String, reason: String },
373
374    #[error("Phase {phase} context creation failed: {reason}")]
375    ContextCreationFailed { phase: String, reason: String },
376
377    #[error("Phase {phase} timed out after {timeout_seconds} seconds")]
378    Timeout { phase: String, timeout_seconds: u64 },
379
380    #[error("Phase {phase} was interrupted by user")]
381    Interrupted { phase: String },
382
383    #[error("Phase {phase} resource limit exceeded: {resource} ({limit})")]
384    ResourceLimitExceeded {
385        phase: String,
386        resource: String,
387        limit: String,
388    },
389
390    #[error("Phase {phase} failed with stderr: {stderr_tail}")]
391    ExecutionFailedWithStderr {
392        phase: String,
393        code: i32,
394        stderr_tail: String,
395    },
396
397    #[error("Phase {phase} produced partial output due to failure")]
398    PartialOutputSaved { phase: String, partial_path: String },
399}
400
401impl UserFriendlyError for PhaseError {
402    fn user_message(&self) -> String {
403        match self {
404            Self::ExecutionFailed { phase, code } => {
405                format!("The {phase} phase failed to complete successfully (exit code: {code})")
406            }
407            Self::DependencyNotSatisfied { phase, dependency } => {
408                format!(
409                    "Cannot run {phase} phase: {dependency} phase must complete successfully first"
410                )
411            }
412            Self::InvalidTransition { from, to } => {
413                format!("Cannot transition from {from} phase to {to} phase")
414            }
415            Self::OutputValidationFailed { phase, reason } => {
416                format!("The {phase} phase produced invalid output: {reason}")
417            }
418            Self::PacketCreationFailed { phase, reason } => {
419                format!("Failed to create input packet for {phase} phase: {reason}")
420            }
421            Self::ContextCreationFailed { phase, reason } => {
422                format!("Failed to create execution context for {phase} phase: {reason}")
423            }
424            Self::Timeout {
425                phase,
426                timeout_seconds,
427            } => {
428                format!("The {phase} phase timed out after {timeout_seconds} seconds")
429            }
430            Self::Interrupted { phase } => {
431                format!("The {phase} phase was interrupted")
432            }
433            Self::ResourceLimitExceeded {
434                phase,
435                resource,
436                limit,
437            } => {
438                format!("The {phase} phase exceeded {resource} limit: {limit}")
439            }
440            Self::ExecutionFailedWithStderr {
441                phase,
442                code,
443                stderr_tail,
444            } => {
445                format!(
446                    "The {phase} phase failed (exit code: {code}) with error output: {stderr_tail}"
447                )
448            }
449            Self::PartialOutputSaved {
450                phase,
451                partial_path,
452            } => {
453                format!("The {phase} phase failed and partial output was saved to: {partial_path}")
454            }
455        }
456    }
457
458    fn context(&self) -> Option<String> {
459        match self {
460            Self::ExecutionFailed { phase, code: _ } => {
461                Some(format!("The {phase} phase encountered an error during execution. Partial outputs have been saved for debugging."))
462            }
463            Self::DependencyNotSatisfied { phase: _, dependency: _ } => {
464                Some("xchecker phases have dependencies that must be satisfied before execution.".to_string())
465            }
466            Self::InvalidTransition { from: _, to: _ } => {
467                Some("Phase transitions must follow the defined workflow order.".to_string())
468            }
469            Self::OutputValidationFailed { phase: _, reason: _ } => {
470                Some("Phase outputs are validated to ensure they meet the expected format and structure.".to_string())
471            }
472            Self::PacketCreationFailed { phase: _, reason: _ } => {
473                Some("Input packets contain the context and files needed for Claude to generate phase outputs.".to_string())
474            }
475            Self::ContextCreationFailed { phase: _, reason: _ } => {
476                Some("Execution context includes configuration, artifacts, and environment needed for phase execution.".to_string())
477            }
478            Self::Timeout { phase: _, timeout_seconds: _ } => {
479                Some("Phase execution has configurable timeouts to prevent hanging operations.".to_string())
480            }
481            Self::Interrupted { phase: _ } => {
482                Some("Phase execution can be interrupted by user signals (Ctrl+C) or system events.".to_string())
483            }
484            Self::ResourceLimitExceeded { phase: _, resource: _, limit: _ } => {
485                Some("Resource limits prevent excessive memory, disk, or network usage during phase execution.".to_string())
486            }
487            Self::ExecutionFailedWithStderr { phase: _, code: _, stderr_tail: _ } => {
488                Some("Phase execution failed and stderr output has been captured for debugging.".to_string())
489            }
490            Self::PartialOutputSaved { phase: _, partial_path: _ } => {
491                Some("Partial outputs are saved when phases fail to help with debugging and recovery.".to_string())
492            }
493        }
494    }
495
496    fn suggestions(&self) -> Vec<String> {
497        match self {
498            Self::ExecutionFailed { phase, code: _ } => {
499                let mut suggestions = vec![
500                    format!(
501                        "Check the partial output in .xchecker/specs/<id>/artifacts/*-{}.partial.md",
502                        phase.to_lowercase()
503                    ),
504                    "Review the receipt file for detailed error information".to_string(),
505                    "Check the stderr output in the receipt for Claude CLI errors".to_string(),
506                ];
507
508                match phase.as_str() {
509                    "REQUIREMENTS" => {
510                        suggestions.push(
511                            "Ensure your problem statement is clear and well-defined".to_string(),
512                        );
513                        suggestions.push("Try simplifying the requirements scope".to_string());
514                    }
515                    "DESIGN" => {
516                        suggestions.push("Review the requirements for completeness".to_string());
517                        suggestions
518                            .push("Check if the design complexity is appropriate".to_string());
519                    }
520                    "TASKS" => {
521                        suggestions.push("Verify the design document is complete".to_string());
522                        suggestions.push("Consider breaking down complex tasks".to_string());
523                    }
524                    _ => {}
525                }
526
527                suggestions
528            }
529            Self::DependencyNotSatisfied {
530                phase: _,
531                dependency,
532            } => vec![
533                format!("Run the {} phase first: xchecker spec <id>", dependency),
534                format!("Check the status of {}: xchecker status <id>", dependency),
535                "Ensure the dependency phase completed successfully (exit code 0)".to_string(),
536            ],
537            Self::InvalidTransition { from: _, to: _ } => vec![
538                "Follow the standard phase order: Requirements → Design → Tasks → Review → Fixup"
539                    .to_string(),
540                "Use 'xchecker resume <id> --phase <name>' to restart from a specific phase"
541                    .to_string(),
542                "Check 'xchecker status <id>' to see the current phase state".to_string(),
543            ],
544            Self::OutputValidationFailed {
545                phase: _,
546                reason: _,
547            } => vec![
548                "Check the phase output format matches the expected structure".to_string(),
549                "Verify that required sections are present in the output".to_string(),
550                "Review the canonicalization requirements for the output type".to_string(),
551            ],
552            Self::PacketCreationFailed {
553                phase: _,
554                reason: _,
555            } => vec![
556                "Check that all required input files are accessible".to_string(),
557                "Verify packet size limits are not exceeded".to_string(),
558                "Ensure no secrets are detected in the input files".to_string(),
559                "Review include/exclude patterns in configuration".to_string(),
560            ],
561            Self::ContextCreationFailed {
562                phase: _,
563                reason: _,
564            } => vec![
565                "Check that the spec directory is accessible and writable".to_string(),
566                "Verify configuration values are valid".to_string(),
567                "Ensure previous phase artifacts are available if required".to_string(),
568                "Check file permissions in the working directory".to_string(),
569            ],
570            Self::Timeout {
571                phase,
572                timeout_seconds: _,
573            } => vec![
574                format!("Increase timeout for {} phase in configuration", phase),
575                "Check your internet connection if using Claude API".to_string(),
576                "Try running with --verbose to see where it's hanging".to_string(),
577                "Consider breaking down complex requests into smaller parts".to_string(),
578            ],
579            Self::Interrupted { phase: _ } => vec![
580                "Resume the phase with: xchecker resume <id> --phase <name>".to_string(),
581                "Check partial outputs in .xchecker/specs/<id>/artifacts/".to_string(),
582                "Use --dry-run to test without making Claude calls".to_string(),
583            ],
584            Self::ResourceLimitExceeded {
585                phase: _,
586                resource,
587                limit: _,
588            } => match resource.as_str() {
589                "memory" => vec![
590                    "Reduce packet size limits in configuration".to_string(),
591                    "Use more restrictive file include patterns".to_string(),
592                    "Process files in smaller batches".to_string(),
593                ],
594                "disk" => vec![
595                    "Free up disk space".to_string(),
596                    "Clean old spec artifacts with: xchecker clean <id>".to_string(),
597                    "Check available disk space".to_string(),
598                ],
599                "network" => vec![
600                    "Check your internet connection".to_string(),
601                    "Verify Claude API rate limits".to_string(),
602                    "Try again later if rate limited".to_string(),
603                ],
604                _ => vec![
605                    "Check system resources and limits".to_string(),
606                    "Review configuration for resource settings".to_string(),
607                ],
608            },
609            Self::ExecutionFailedWithStderr {
610                phase,
611                code: _,
612                stderr_tail: _,
613            } => vec![
614                format!(
615                    "Check the stderr output captured in the receipt for detailed error information"
616                ),
617                format!(
618                    "Review partial outputs in .xchecker/specs/<id>/artifacts/*-{}.partial.md",
619                    phase.to_lowercase()
620                ),
621                "Try running with --dry-run to test configuration without Claude calls".to_string(),
622                "Check Claude CLI authentication and connectivity".to_string(),
623            ],
624            Self::PartialOutputSaved {
625                phase: _,
626                partial_path,
627            } => vec![
628                format!("Review the partial output at: {}", partial_path),
629                "Check the receipt file for detailed error information and stderr output"
630                    .to_string(),
631                "Use the partial output to understand where the phase failed".to_string(),
632                "Try resuming the phase after addressing any issues".to_string(),
633            ],
634        }
635    }
636
637    fn category(&self) -> ErrorCategory {
638        ErrorCategory::PhaseExecution
639    }
640}
641
642/// Source resolution errors (R6.4)
643#[derive(Error, Debug)]
644pub enum SourceError {
645    #[error("GitHub repository not found: {owner}/{repo}")]
646    GitHubRepoNotFound { owner: String, repo: String },
647
648    #[error("GitHub issue not found: {owner}/{repo}#{issue}")]
649    GitHubIssueNotFound {
650        owner: String,
651        repo: String,
652        issue: String,
653    },
654
655    #[error("GitHub authentication failed: {reason}")]
656    GitHubAuthFailed { reason: String },
657
658    #[error("GitHub API error: {status} - {message}")]
659    GitHubApiError { status: u16, message: String },
660
661    #[error("Filesystem path not found: {path}")]
662    FileSystemNotFound { path: String },
663
664    #[error("Filesystem access denied: {path}")]
665    FileSystemAccessDenied { path: String },
666
667    #[error("Filesystem path is not a directory: {path}")]
668    FileSystemNotDirectory { path: String },
669
670    #[error("Stdin read failed: {reason}")]
671    StdinReadFailed { reason: String },
672
673    #[error("Empty input provided")]
674    EmptyInput,
675
676    #[error("Invalid source format: {reason}")]
677    InvalidFormat { reason: String },
678}
679
680impl UserFriendlyError for SourceError {
681    fn user_message(&self) -> String {
682        match self {
683            Self::GitHubRepoNotFound { owner, repo } => {
684                format!("GitHub repository '{owner}/{repo}' could not be found or accessed")
685            }
686            Self::GitHubIssueNotFound { owner, repo, issue } => {
687                format!("Issue #{issue} not found in repository '{owner}/{repo}'")
688            }
689            Self::GitHubAuthFailed { reason } => {
690                format!("GitHub authentication failed: {reason}")
691            }
692            Self::GitHubApiError { status, message } => {
693                format!("GitHub API returned error {status}: {message}")
694            }
695            Self::FileSystemNotFound { path } => {
696                format!("Path '{path}' does not exist")
697            }
698            Self::FileSystemAccessDenied { path } => {
699                format!("Access denied to path '{path}'")
700            }
701            Self::FileSystemNotDirectory { path } => {
702                format!("Path '{path}' is not a directory")
703            }
704            Self::StdinReadFailed { reason } => {
705                format!("Failed to read from standard input: {reason}")
706            }
707            Self::EmptyInput => {
708                "No input provided - please provide a problem statement".to_string()
709            }
710            Self::InvalidFormat { reason } => {
711                format!("Input format is invalid: {reason}")
712            }
713        }
714    }
715
716    fn context(&self) -> Option<String> {
717        match self {
718            Self::GitHubRepoNotFound { .. } => {
719                Some("GitHub source resolution requires access to public repositories or proper authentication for private ones.".to_string())
720            }
721            Self::GitHubIssueNotFound { .. } => {
722                Some("GitHub issues are resolved by their number within the specified repository.".to_string())
723            }
724            Self::GitHubAuthFailed { .. } => {
725                Some("GitHub authentication is required for private repositories and API rate limiting.".to_string())
726            }
727            Self::GitHubApiError { .. } => {
728                Some("GitHub API errors can be temporary or indicate rate limiting, authentication, or permission issues.".to_string())
729            }
730            Self::FileSystemNotFound { .. } => {
731                Some("Filesystem source resolution requires the specified path to exist and be accessible.".to_string())
732            }
733            Self::FileSystemAccessDenied { .. } => {
734                Some("File system permissions must allow read access to the specified directory.".to_string())
735            }
736            Self::FileSystemNotDirectory { .. } => {
737                Some("Filesystem source resolution expects a directory containing project files.".to_string())
738            }
739            Self::StdinReadFailed { .. } => {
740                Some("Standard input is used when no other source is specified or when --source stdin is used.".to_string())
741            }
742            Self::EmptyInput => {
743                Some("xchecker requires a problem statement to generate specifications from.".to_string())
744            }
745            Self::InvalidFormat { .. } => {
746                Some("Input should be a clear problem statement describing what you want to build.".to_string())
747            }
748        }
749    }
750
751    fn suggestions(&self) -> Vec<String> {
752        match self {
753            Self::GitHubRepoNotFound { owner, repo } => vec![
754                format!(
755                    "Verify the repository name: https://github.com/{}/{}",
756                    owner, repo
757                ),
758                "Check that the repository is public or you have access".to_string(),
759                "Ensure your GitHub authentication is working".to_string(),
760                "Try using the full repository URL format".to_string(),
761            ],
762            Self::GitHubIssueNotFound { owner, repo, issue } => vec![
763                format!(
764                    "Check if issue #{} exists: https://github.com/{}/{}/issues/{}",
765                    issue, owner, repo, issue
766                ),
767                "Verify the issue number is correct".to_string(),
768                "Ensure the issue is not closed or private".to_string(),
769                "Try using a different issue number".to_string(),
770            ],
771            Self::GitHubAuthFailed { .. } => vec![
772                "Set up GitHub authentication: gh auth login".to_string(),
773                "Check your GitHub token permissions".to_string(),
774                "Verify your GitHub CLI is properly configured".to_string(),
775                "Try accessing a public repository first".to_string(),
776            ],
777            Self::GitHubApiError { status, .. } => match *status {
778                401 => vec![
779                    "Authentication required - run 'gh auth login'".to_string(),
780                    "Check your GitHub token is valid and not expired".to_string(),
781                ],
782                403 => vec![
783                    "Rate limit exceeded - wait a few minutes and try again".to_string(),
784                    "Authenticate to get higher rate limits".to_string(),
785                ],
786                404 => vec![
787                    "Repository or resource not found - check the URL".to_string(),
788                    "Verify you have access to the repository".to_string(),
789                ],
790                _ => vec![
791                    "This may be a temporary GitHub API issue - try again later".to_string(),
792                    "Check GitHub status: https://www.githubstatus.com/".to_string(),
793                ],
794            },
795            Self::FileSystemNotFound { path } => vec![
796                format!("Create the directory: mkdir -p '{}'", path),
797                "Check the path spelling and case sensitivity".to_string(),
798                "Use an absolute path to avoid confusion".to_string(),
799                "Verify you're in the correct working directory".to_string(),
800            ],
801            Self::FileSystemAccessDenied { path } => vec![
802                format!("Check permissions: ls -la '{}'", path),
803                "Ensure you have read access to the directory".to_string(),
804                "Try running from a directory you own".to_string(),
805                "Use sudo if appropriate (be careful with permissions)".to_string(),
806            ],
807            Self::FileSystemNotDirectory { path } => vec![
808                format!("Use the parent directory of '{}'", path),
809                "Specify a directory path, not a file path".to_string(),
810                "Check that the path points to a directory".to_string(),
811            ],
812            Self::StdinReadFailed { .. } => vec![
813                "Provide input via pipe: echo 'problem statement' | xchecker spec <id>".to_string(),
814                "Use a different source: --source fs --repo /path/to/project".to_string(),
815                "Check that stdin is not closed or redirected incorrectly".to_string(),
816            ],
817            Self::EmptyInput => vec![
818                "Provide a problem statement via stdin".to_string(),
819                "Use --source fs --repo <path> to read from filesystem".to_string(),
820                "Use --source gh --gh owner/repo to read from GitHub issue".to_string(),
821                "Example: echo 'Build a web API for user management' | xchecker spec my-api"
822                    .to_string(),
823            ],
824            Self::InvalidFormat { .. } => vec![
825                "Provide a clear, single-line problem statement".to_string(),
826                "Describe what you want to build in plain English".to_string(),
827                "Example: 'Build a REST API for managing user accounts'".to_string(),
828                "Avoid complex formatting or multiple unrelated requests".to_string(),
829            ],
830        }
831    }
832
833    fn category(&self) -> ErrorCategory {
834        ErrorCategory::Configuration
835    }
836}
837
838/// Claude CLI integration errors
839#[derive(Error, Debug)]
840pub enum ClaudeError {
841    #[error("Claude CLI not found or not executable")]
842    NotFound,
843
844    #[error("Claude CLI version incompatible: {version}")]
845    IncompatibleVersion { version: String },
846
847    #[error("Claude CLI execution failed: {stderr}")]
848    ExecutionFailed { stderr: String },
849
850    #[error("Failed to parse Claude CLI output: {reason}")]
851    ParseError { reason: String },
852
853    #[error("Model '{model}' not available")]
854    ModelNotAvailable { model: String },
855
856    #[error("Authentication failed: {reason}")]
857    AuthenticationFailed { reason: String },
858}
859
860impl UserFriendlyError for ClaudeError {
861    fn user_message(&self) -> String {
862        match self {
863            Self::NotFound => "Claude CLI is not installed or not found in PATH".to_string(),
864            Self::IncompatibleVersion { version } => {
865                format!("Claude CLI version {version} is not compatible with xchecker")
866            }
867            Self::ExecutionFailed { stderr } => {
868                format!("Claude CLI execution failed: {stderr}")
869            }
870            Self::ParseError { reason } => {
871                format!("Could not understand Claude CLI response: {reason}")
872            }
873            Self::ModelNotAvailable { model } => {
874                format!("The model '{model}' is not available or accessible")
875            }
876            Self::AuthenticationFailed { reason } => {
877                format!("Claude authentication failed: {reason}")
878            }
879        }
880    }
881
882    fn context(&self) -> Option<String> {
883        match self {
884            Self::NotFound => Some(
885                "xchecker requires the Claude CLI to be installed and available in your PATH."
886                    .to_string(),
887            ),
888            Self::IncompatibleVersion { version: _ } => Some(
889                "xchecker is tested with specific versions of the Claude CLI for compatibility."
890                    .to_string(),
891            ),
892            Self::ExecutionFailed { stderr: _ } => {
893                Some("The Claude CLI encountered an error during execution.".to_string())
894            }
895            Self::ParseError { reason: _ } => Some(
896                "xchecker expects Claude CLI output in a specific format (stream-json or text)."
897                    .to_string(),
898            ),
899            Self::ModelNotAvailable { model: _ } => Some(
900                "Model availability depends on your Claude subscription and API access."
901                    .to_string(),
902            ),
903            Self::AuthenticationFailed { reason: _ } => {
904                Some("Claude CLI requires proper authentication to access the API.".to_string())
905            }
906        }
907    }
908
909    fn suggestions(&self) -> Vec<String> {
910        match self {
911            Self::NotFound => vec![
912                "Install the Claude CLI: https://claude.ai/cli".to_string(),
913                "Ensure 'claude' is in your PATH".to_string(),
914                "Test with 'claude --version' to verify installation".to_string(),
915            ],
916            Self::IncompatibleVersion { version: _ } => vec![
917                "Update Claude CLI to the latest version".to_string(),
918                "Check xchecker documentation for supported Claude CLI versions".to_string(),
919                "Use 'claude --version' to check your current version".to_string(),
920            ],
921            Self::ExecutionFailed { stderr: _ } => vec![
922                "Check your internet connection".to_string(),
923                "Verify Claude CLI authentication with 'claude auth status'".to_string(),
924                "Try running the command manually to debug the issue".to_string(),
925                "Check if you've exceeded API rate limits".to_string(),
926            ],
927            Self::ParseError { reason: _ } => vec![
928                "This may be a temporary issue - try running the command again".to_string(),
929                "Check if Claude CLI output format has changed".to_string(),
930                "Report this issue if it persists".to_string(),
931            ],
932            Self::ModelNotAvailable { model } => {
933                let mut suggestions = vec![
934                    "Check available models with 'claude models'".to_string(),
935                    "Verify your Claude subscription includes access to this model".to_string(),
936                ];
937
938                // Provide specific suggestions based on the model alias
939                if model.contains("sonnet") || model == "sonnet" {
940                    suggestions.push("Try '--model sonnet' for the Sonnet model".to_string());
941                } else if model.contains("haiku") || model == "haiku" {
942                    suggestions.push("Try '--model haiku' for the Haiku model".to_string());
943                } else if model.contains("opus") || model == "opus" {
944                    suggestions.push("Try '--model opus' for the Opus model".to_string());
945                } else {
946                    suggestions.push(
947                        "Try using a common alias like 'sonnet', 'haiku', or 'opus'".to_string(),
948                    );
949                }
950
951                suggestions.push(
952                    "Check the Claude CLI documentation for supported model names".to_string(),
953                );
954                suggestions
955            }
956            Self::AuthenticationFailed { reason: _ } => vec![
957                "Run 'claude auth login' to authenticate".to_string(),
958                "Check your API key is valid and not expired".to_string(),
959                "Verify your Claude account has API access".to_string(),
960                "Try logging out and back in: 'claude auth logout && claude auth login'"
961                    .to_string(),
962            ],
963        }
964    }
965
966    fn category(&self) -> ErrorCategory {
967        ErrorCategory::ClaudeIntegration
968    }
969}
970
971/// Runner execution errors for cross-platform Claude CLI execution
972#[derive(Error, Debug)]
973pub enum RunnerError {
974    #[error("Runner detection failed: {reason}")]
975    DetectionFailed { reason: String },
976
977    #[error("WSL not available: {reason}")]
978    WslNotAvailable { reason: String },
979
980    #[error("WSL execution failed: {reason}")]
981    WslExecutionFailed { reason: String },
982
983    #[error("Native execution failed: {reason}")]
984    NativeExecutionFailed { reason: String },
985
986    #[error("Runner configuration invalid: {reason}")]
987    ConfigurationInvalid { reason: String },
988
989    #[error("Claude CLI not found in runner environment: {runner}")]
990    ClaudeNotFoundInRunner { runner: String },
991
992    #[error("Execution timed out after {timeout_seconds} seconds")]
993    Timeout { timeout_seconds: u64 },
994}
995
996impl UserFriendlyError for RunnerError {
997    fn user_message(&self) -> String {
998        match self {
999            Self::DetectionFailed { reason } => {
1000                format!("Could not detect the best way to run Claude CLI: {reason}")
1001            }
1002            Self::WslNotAvailable { reason } => {
1003                format!("WSL is not available: {reason}")
1004            }
1005            Self::WslExecutionFailed { reason } => {
1006                format!("Failed to run Claude CLI in WSL: {reason}")
1007            }
1008            Self::NativeExecutionFailed { reason } => {
1009                format!("Failed to run Claude CLI natively: {reason}")
1010            }
1011            Self::ConfigurationInvalid { reason } => {
1012                format!("Runner configuration is invalid: {reason}")
1013            }
1014            Self::ClaudeNotFoundInRunner { runner } => {
1015                format!("Claude CLI not found in {runner} environment")
1016            }
1017            Self::Timeout { timeout_seconds } => {
1018                format!("Claude CLI execution timed out after {timeout_seconds} seconds")
1019            }
1020        }
1021    }
1022
1023    fn context(&self) -> Option<String> {
1024        match self {
1025            Self::DetectionFailed { .. } => {
1026                Some("xchecker automatically detects the best way to run Claude CLI on your system.".to_string())
1027            }
1028            Self::WslNotAvailable { .. } => {
1029                Some("WSL (Windows Subsystem for Linux) is required for running Claude CLI on Windows when not natively available.".to_string())
1030            }
1031            Self::WslExecutionFailed { .. } => {
1032                Some("WSL execution allows running Claude CLI in a Linux environment on Windows.".to_string())
1033            }
1034            Self::NativeExecutionFailed { .. } => {
1035                Some("Native execution runs Claude CLI directly on the host system.".to_string())
1036            }
1037            Self::ConfigurationInvalid { .. } => {
1038                Some("Runner configuration controls how xchecker executes Claude CLI across different platforms.".to_string())
1039            }
1040            Self::ClaudeNotFoundInRunner { .. } => {
1041                Some("Claude CLI must be installed and accessible in the specified runner environment.".to_string())
1042            }
1043            Self::Timeout { .. } => {
1044                Some("Phase execution has configurable timeouts to prevent hanging operations.".to_string())
1045            }
1046        }
1047    }
1048
1049    fn suggestions(&self) -> Vec<String> {
1050        match self {
1051            Self::DetectionFailed { .. } => vec![
1052                "Try specifying runner mode explicitly: --runner native or --runner wsl"
1053                    .to_string(),
1054                "Ensure Claude CLI is installed and accessible".to_string(),
1055                "Check that 'claude --version' works in your environment".to_string(),
1056            ],
1057            Self::WslNotAvailable { .. } => vec![
1058                "Install WSL: wsl --install".to_string(),
1059                "Use native runner mode if Claude CLI is available on Windows".to_string(),
1060                "Check WSL status: wsl --status".to_string(),
1061            ],
1062            Self::WslExecutionFailed { .. } => vec![
1063                "Check that WSL is running: wsl --status".to_string(),
1064                "Verify Claude CLI is installed in WSL: wsl -e claude --version".to_string(),
1065                "Try restarting WSL: wsl --shutdown && wsl".to_string(),
1066                "Use native runner mode as alternative".to_string(),
1067            ],
1068            Self::NativeExecutionFailed { .. } => vec![
1069                "Install Claude CLI for your platform".to_string(),
1070                "Ensure 'claude' is in your PATH".to_string(),
1071                "Try WSL runner mode on Windows: --runner wsl".to_string(),
1072            ],
1073            Self::ConfigurationInvalid { .. } => vec![
1074                "Check runner configuration in .xchecker/config.toml".to_string(),
1075                "Valid runner modes: auto, native, wsl".to_string(),
1076                "Remove invalid configuration to use defaults".to_string(),
1077            ],
1078            Self::ClaudeNotFoundInRunner { runner } => match runner.as_str() {
1079                "wsl" => vec![
1080                    "Install Claude CLI in WSL: wsl -e pip install claude-cli".to_string(),
1081                    "Check WSL PATH: wsl -e echo $PATH".to_string(),
1082                    "Specify claude_path in configuration if installed in non-standard location"
1083                        .to_string(),
1084                ],
1085                "native" => vec![
1086                    "Install Claude CLI for your platform".to_string(),
1087                    "Add Claude CLI to your PATH".to_string(),
1088                    "Test with: claude --version".to_string(),
1089                ],
1090                _ => vec![
1091                    "Install Claude CLI in the specified runner environment".to_string(),
1092                    "Check that Claude CLI is accessible and executable".to_string(),
1093                ],
1094            },
1095            Self::Timeout { timeout_seconds: _ } => vec![
1096                "Increase timeout in configuration or via --phase-timeout flag".to_string(),
1097                "Check your internet connection if using Claude API".to_string(),
1098                "Try running with --verbose to see where it's hanging".to_string(),
1099                "Consider breaking down complex requests into smaller parts".to_string(),
1100            ],
1101        }
1102    }
1103
1104    fn category(&self) -> ErrorCategory {
1105        ErrorCategory::ClaudeIntegration
1106    }
1107}
1108
1109/// Errors that can occur during fixup detection and parsing
1110#[derive(Error, Debug)]
1111pub enum FixupError {
1112    #[error("No fixup markers found in review output")]
1113    NoFixupMarkersFound,
1114
1115    #[error("Invalid diff format in block {block_index}: {reason}")]
1116    InvalidDiffFormat { block_index: usize, reason: String },
1117
1118    #[error("Git apply validation failed for {target_file}: {reason}")]
1119    GitApplyValidationFailed { target_file: String, reason: String },
1120
1121    #[error("Git apply execution failed for {target_file}: {reason}")]
1122    GitApplyExecutionFailed { target_file: String, reason: String },
1123
1124    #[error("Target file not found: {path}")]
1125    TargetFileNotFound { path: String },
1126
1127    #[error("Failed to create temporary copy of {file}: {reason}")]
1128    TempCopyFailed { file: String, reason: String },
1129
1130    #[error("Diff parsing failed: {reason}")]
1131    DiffParsingFailed { reason: String },
1132
1133    #[error("No valid diff blocks found")]
1134    NoValidDiffBlocks,
1135
1136    #[error("Absolute path not allowed: {0}")]
1137    AbsolutePath(PathBuf),
1138
1139    #[error("Parent directory escape not allowed: {0}")]
1140    ParentDirEscape(PathBuf),
1141
1142    #[error("Path resolves outside repo root: {0}")]
1143    OutsideRepo(PathBuf),
1144
1145    #[error("Path canonicalization failed: {0}")]
1146    CanonicalizationError(String),
1147
1148    #[error("Symlink not allowed (use --allow-links to permit): {0}")]
1149    SymlinkNotAllowed(PathBuf),
1150
1151    #[error("Hardlink not allowed (use --allow-links to permit): {0}")]
1152    HardlinkNotAllowed(PathBuf),
1153
1154    #[error(
1155        "Could not find matching context for hunk at line {expected_line} in {file} (searched ±{search_window} lines)"
1156    )]
1157    FuzzyMatchFailed {
1158        file: String,
1159        expected_line: usize,
1160        search_window: usize,
1161    },
1162}
1163
1164impl UserFriendlyError for FixupError {
1165    fn user_message(&self) -> String {
1166        match self {
1167            Self::NoFixupMarkersFound => {
1168                "No fixup changes were found in the review output".to_string()
1169            }
1170            Self::InvalidDiffFormat {
1171                block_index,
1172                reason,
1173            } => {
1174                format!("Diff block {block_index} has invalid format: {reason}")
1175            }
1176            Self::GitApplyValidationFailed {
1177                target_file,
1178                reason,
1179            } => {
1180                format!("Cannot apply changes to '{target_file}': {reason}")
1181            }
1182            Self::GitApplyExecutionFailed {
1183                target_file,
1184                reason,
1185            } => {
1186                format!("Failed to apply changes to '{target_file}': {reason}")
1187            }
1188            Self::TargetFileNotFound { path } => {
1189                format!("Target file '{path}' does not exist")
1190            }
1191            Self::TempCopyFailed { file, reason } => {
1192                format!("Could not create temporary copy of '{file}': {reason}")
1193            }
1194            Self::DiffParsingFailed { reason } => {
1195                format!("Could not parse diff content: {reason}")
1196            }
1197            Self::NoValidDiffBlocks => "No valid diff blocks found in the fixup plan".to_string(),
1198            Self::AbsolutePath(path) => {
1199                format!("Absolute paths are not allowed: {}", path.display())
1200            }
1201            Self::ParentDirEscape(path) => {
1202                format!(
1203                    "Path attempts to escape parent directory: {}",
1204                    path.display()
1205                )
1206            }
1207            Self::OutsideRepo(path) => {
1208                format!("Path resolves outside repository root: {}", path.display())
1209            }
1210            Self::CanonicalizationError(reason) => {
1211                format!("Could not resolve file path: {reason}")
1212            }
1213            Self::SymlinkNotAllowed(path) => {
1214                format!(
1215                    "Symlinks are not allowed: {} (use --allow-links to permit)",
1216                    path.display()
1217                )
1218            }
1219            Self::HardlinkNotAllowed(path) => {
1220                format!(
1221                    "Hardlinks are not allowed: {} (use --allow-links to permit)",
1222                    path.display()
1223                )
1224            }
1225            Self::FuzzyMatchFailed {
1226                file,
1227                expected_line,
1228                search_window,
1229            } => {
1230                format!(
1231                    "Could not find matching context for diff hunk at line {} in '{}' (searched ±{} lines)",
1232                    expected_line, file, search_window
1233                )
1234            }
1235        }
1236    }
1237
1238    fn context(&self) -> Option<String> {
1239        match self {
1240            Self::NoFixupMarkersFound => {
1241                Some("The review phase should produce a 'FIXUP PLAN:' section with unified diffs for files that need changes.".to_string())
1242            }
1243            Self::InvalidDiffFormat { .. } => {
1244                Some("Fixup diffs must follow the unified diff format with proper headers and hunks.".to_string())
1245            }
1246            Self::GitApplyValidationFailed { .. } | Self::GitApplyExecutionFailed { .. } => {
1247                Some("Git apply is used to safely apply diff patches to files with validation.".to_string())
1248            }
1249            Self::TargetFileNotFound { .. } => {
1250                Some("Fixup targets must exist in the repository before changes can be applied.".to_string())
1251            }
1252            Self::TempCopyFailed { .. } => {
1253                Some("Temporary copies are created to safely test changes before applying them.".to_string())
1254            }
1255            Self::DiffParsingFailed { .. } | Self::NoValidDiffBlocks => {
1256                Some("Fixup plans contain unified diff blocks that describe file changes.".to_string())
1257            }
1258            Self::AbsolutePath(_) | Self::ParentDirEscape(_) | Self::OutsideRepo(_) => {
1259                Some("Fixup paths are validated to prevent directory traversal and ensure changes stay within the repository.".to_string())
1260            }
1261            Self::CanonicalizationError(_) => {
1262                Some("Path canonicalization resolves symlinks and relative paths to absolute paths for validation.".to_string())
1263            }
1264            Self::SymlinkNotAllowed(_) | Self::HardlinkNotAllowed(_) => {
1265                Some("Symlinks and hardlinks are blocked by default for security. Use --allow-links to permit them.".to_string())
1266            }
1267            Self::FuzzyMatchFailed { .. } => {
1268                Some("The diff hunk's context lines couldn't be matched to the file, which may indicate the file has changed since the diff was generated.".to_string())
1269            }
1270        }
1271    }
1272
1273    fn suggestions(&self) -> Vec<String> {
1274        match self {
1275            Self::NoFixupMarkersFound => vec![
1276                "Check the review output in .xchecker/specs/<id>/artifacts/review.md".to_string(),
1277                "Ensure the review phase completed successfully".to_string(),
1278                "The review phase may not have identified any changes needed".to_string(),
1279                "Try running the review phase again if it failed".to_string(),
1280            ],
1281            Self::InvalidDiffFormat {
1282                block_index,
1283                reason,
1284            } => vec![
1285                format!("Review diff block {} in the review output", block_index),
1286                "Ensure the diff follows unified diff format (--- and +++ headers)".to_string(),
1287                "Check that hunk headers use @@ format".to_string(),
1288                format!("Specific issue: {}", reason),
1289            ],
1290            Self::GitApplyValidationFailed {
1291                target_file,
1292                reason,
1293            } => vec![
1294                format!("Check the current state of '{}'", target_file),
1295                "The file may have been modified since the review phase".to_string(),
1296                "Try running the review phase again to generate fresh diffs".to_string(),
1297                format!("Git apply error: {}", reason),
1298                "Use --dry-run to preview changes without applying them".to_string(),
1299            ],
1300            Self::GitApplyExecutionFailed {
1301                target_file,
1302                reason,
1303            } => vec![
1304                format!("Check file permissions for '{}'", target_file),
1305                "Ensure the file is writable".to_string(),
1306                "Check available disk space".to_string(),
1307                format!("Git apply error: {}", reason),
1308                "Try running with --verbose for more details".to_string(),
1309            ],
1310            Self::TargetFileNotFound { path } => vec![
1311                format!("Verify that '{}' exists in the repository", path),
1312                "The file may have been deleted or moved since the review phase".to_string(),
1313                "Check the file path is correct and relative to the repository root".to_string(),
1314                "Run the review phase again to generate fresh fixup plans".to_string(),
1315            ],
1316            Self::TempCopyFailed { file, reason } => vec![
1317                "Check available disk space for temporary files".to_string(),
1318                "Ensure you have write permissions in the temp directory".to_string(),
1319                format!("File: {}", file),
1320                format!("Reason: {}", reason),
1321            ],
1322            Self::DiffParsingFailed { reason } => vec![
1323                "Check the review output format".to_string(),
1324                "Ensure the FIXUP PLAN section contains valid unified diffs".to_string(),
1325                format!("Parsing error: {}", reason),
1326                "Try running the review phase again".to_string(),
1327            ],
1328            Self::NoValidDiffBlocks => vec![
1329                "Check the review output for FIXUP PLAN section".to_string(),
1330                "Ensure diff blocks follow unified diff format".to_string(),
1331                "The review phase may not have generated any valid changes".to_string(),
1332                "Try running the review phase again".to_string(),
1333            ],
1334            Self::AbsolutePath(path) => vec![
1335                format!("Use relative paths instead of absolute: {}", path.display()),
1336                "Fixup paths must be relative to the repository root".to_string(),
1337                "Remove leading '/' or drive letters from paths".to_string(),
1338            ],
1339            Self::ParentDirEscape(path) => vec![
1340                format!("Remove '..' components from path: {}", path.display()),
1341                "Fixup paths must not escape the repository directory".to_string(),
1342                "Use paths relative to the repository root".to_string(),
1343            ],
1344            Self::OutsideRepo(path) => vec![
1345                format!("Path resolves outside repository: {}", path.display()),
1346                "Ensure all fixup targets are within the repository".to_string(),
1347                "Check for symlinks that point outside the repository".to_string(),
1348                "Use --allow-links if you need to modify symlinked files".to_string(),
1349            ],
1350            Self::CanonicalizationError(reason) => vec![
1351                "Check that the file path exists and is accessible".to_string(),
1352                "Verify file permissions allow reading the path".to_string(),
1353                format!("Error: {}", reason),
1354            ],
1355            Self::SymlinkNotAllowed(path) => vec![
1356                format!("Symlink detected: {}", path.display()),
1357                "Use --allow-links flag to permit symlink modifications".to_string(),
1358                "Consider modifying the symlink target directly instead".to_string(),
1359                "Symlinks are blocked by default for security".to_string(),
1360            ],
1361            Self::HardlinkNotAllowed(path) => vec![
1362                format!("Hardlink detected: {}", path.display()),
1363                "Use --allow-links flag to permit hardlink modifications".to_string(),
1364                "Consider modifying one of the linked files directly".to_string(),
1365                "Hardlinks are blocked by default for security".to_string(),
1366            ],
1367            Self::FuzzyMatchFailed { file, .. } => vec![
1368                format!(
1369                    "The file '{}' may have changed since the review phase",
1370                    file
1371                ),
1372                "Run the review phase again to generate fresh diffs".to_string(),
1373                "Check if the file has been modified by another process".to_string(),
1374                "Use 'xchecker resume <id> --phase review' to regenerate fixups".to_string(),
1375            ],
1376        }
1377    }
1378
1379    fn category(&self) -> ErrorCategory {
1380        match self {
1381            Self::NoFixupMarkersFound | Self::NoValidDiffBlocks => ErrorCategory::Validation,
1382            Self::InvalidDiffFormat { .. } | Self::DiffParsingFailed { .. } => {
1383                ErrorCategory::Validation
1384            }
1385            Self::AbsolutePath(_) | Self::ParentDirEscape(_) | Self::OutsideRepo(_) => {
1386                ErrorCategory::Security
1387            }
1388            Self::SymlinkNotAllowed(_) | Self::HardlinkNotAllowed(_) => ErrorCategory::Security,
1389            Self::TargetFileNotFound { .. } | Self::TempCopyFailed { .. } => {
1390                ErrorCategory::FileSystem
1391            }
1392            Self::CanonicalizationError(_) => ErrorCategory::FileSystem,
1393            Self::GitApplyValidationFailed { .. } | Self::GitApplyExecutionFailed { .. } => {
1394                ErrorCategory::PhaseExecution
1395            }
1396            Self::FuzzyMatchFailed { .. } => ErrorCategory::PhaseExecution,
1397        }
1398    }
1399}
1400
1401impl UserFriendlyError for LockError {
1402    fn user_message(&self) -> String {
1403        match self {
1404            Self::ConcurrentExecution {
1405                spec_id,
1406                pid,
1407                created_ago,
1408            } => {
1409                format!(
1410                    "Another xchecker process is already running for spec '{spec_id}' (PID {pid}, started {created_ago})"
1411                )
1412            }
1413            Self::StaleLock {
1414                spec_id,
1415                pid,
1416                age_secs,
1417            } => {
1418                format!("Stale lock detected for spec '{spec_id}' (PID {pid}, age {age_secs}s)")
1419            }
1420            Self::CorruptedLock { reason } => {
1421                format!("Lock file is corrupted or invalid: {reason}")
1422            }
1423            Self::AcquisitionFailed { reason } => {
1424                format!("Failed to acquire exclusive lock: {reason}")
1425            }
1426            Self::ReleaseFailed { reason } => {
1427                format!("Failed to release lock: {reason}")
1428            }
1429            Self::Io(e) => {
1430                format!("File system error during lock operation: {e}")
1431            }
1432        }
1433    }
1434
1435    fn context(&self) -> Option<String> {
1436        match self {
1437            Self::ConcurrentExecution { .. } => {
1438                Some("xchecker uses advisory file locks to prevent concurrent execution on the same spec. This ensures data integrity and prevents conflicts.".to_string())
1439            }
1440            Self::StaleLock { .. } => {
1441                Some("Stale locks can occur when xchecker processes are terminated unexpectedly. The lock system prevents accidental conflicts.".to_string())
1442            }
1443            Self::CorruptedLock { .. } => {
1444                Some("Lock files contain process information in JSON format. Corruption can occur due to disk issues or interrupted writes.".to_string())
1445            }
1446            Self::AcquisitionFailed { .. } => {
1447                Some("Lock acquisition ensures exclusive access to spec directories during operations that modify state.".to_string())
1448            }
1449            Self::ReleaseFailed { .. } => {
1450                Some("Lock release cleans up the lock file when operations complete. Failure to release may leave stale locks.".to_string())
1451            }
1452            Self::Io(_) => {
1453                Some("File system operations are required for lock management. Check permissions and disk space.".to_string())
1454            }
1455        }
1456    }
1457
1458    fn suggestions(&self) -> Vec<String> {
1459        match self {
1460            Self::ConcurrentExecution { spec_id, pid, .. } => vec![
1461                format!("Wait for the other process (PID {}) to complete", pid),
1462                "Check if the process is still running with: ps {} (Unix) or tasklist /FI \"PID eq {}\" (Windows)".to_string(),
1463                "If the process is stuck, terminate it and try again".to_string(),
1464                format!("Use --force to override if you're certain no other process is running on spec '{}'", spec_id),
1465            ],
1466            Self::StaleLock { spec_id, pid, .. } => vec![
1467                format!("Use --force to override the stale lock for spec '{}'", spec_id),
1468                format!("Verify that process {} is no longer running", pid),
1469                "Check system logs for any crashed xchecker processes".to_string(),
1470                "Consider cleaning up old spec directories if they're no longer needed".to_string(),
1471            ],
1472            Self::CorruptedLock { .. } => vec![
1473                "Remove the corrupted lock file manually: rm .xchecker/specs/<spec_id>/.lock".to_string(),
1474                "Check disk space and file system integrity".to_string(),
1475                "Ensure proper shutdown of xchecker processes to prevent corruption".to_string(),
1476            ],
1477            Self::AcquisitionFailed { .. } => vec![
1478                "Check file permissions in the .xchecker directory".to_string(),
1479                "Ensure sufficient disk space for lock file creation".to_string(),
1480                "Verify that the parent directory is writable".to_string(),
1481                "Try running from a different directory with proper permissions".to_string(),
1482            ],
1483            Self::ReleaseFailed { .. } => vec![
1484                "Check file permissions for the lock file".to_string(),
1485                "Ensure the lock file exists and is writable".to_string(),
1486                "The lock will be automatically cleaned up when the process exits".to_string(),
1487            ],
1488            Self::Io(e) => {
1489                match e.kind() {
1490                    io::ErrorKind::PermissionDenied => vec![
1491                        "Check file and directory permissions".to_string(),
1492                        "Ensure you have write access to the .xchecker directory".to_string(),
1493                        "Try running with appropriate privileges".to_string(),
1494                    ],
1495                    io::ErrorKind::NotFound => vec![
1496                        "Ensure the .xchecker directory exists".to_string(),
1497                        "Check that the spec directory path is correct".to_string(),
1498                    ],
1499                    io::ErrorKind::AlreadyExists => vec![
1500                        "Another process may have created the lock file simultaneously".to_string(),
1501                        "Wait a moment and try again".to_string(),
1502                    ],
1503                    _ => vec![
1504                        "Check disk space and file system health".to_string(),
1505                        "Verify file system permissions".to_string(),
1506                        "Try the operation again".to_string(),
1507                    ]
1508                }
1509            }
1510        }
1511    }
1512
1513    fn category(&self) -> ErrorCategory {
1514        match self {
1515            Self::ConcurrentExecution { .. } | Self::StaleLock { .. } => ErrorCategory::Concurrency,
1516            Self::CorruptedLock { .. } => ErrorCategory::Validation,
1517            Self::AcquisitionFailed { .. } | Self::ReleaseFailed { .. } => {
1518                ErrorCategory::FileSystem
1519            }
1520            Self::Io(_) => ErrorCategory::FileSystem,
1521        }
1522    }
1523}
1524
1525/// Error type for spec ID validation failures
1526#[derive(Debug, thiserror::Error)]
1527pub enum SpecIdError {
1528    #[error("Spec ID is empty after sanitization")]
1529    Empty,
1530
1531    #[error("Spec ID contains only invalid characters")]
1532    OnlyInvalidCharacters,
1533}
1534
1535impl UserFriendlyError for SpecIdError {
1536    fn user_message(&self) -> String {
1537        match self {
1538            Self::Empty => "The spec ID is empty or contains no valid characters".to_string(),
1539            Self::OnlyInvalidCharacters => {
1540                "The spec ID contains only invalid characters (no alphanumeric, dots, or dashes)"
1541                    .to_string()
1542            }
1543        }
1544    }
1545
1546    fn context(&self) -> Option<String> {
1547        Some("Spec IDs are used as directory names and must contain valid filesystem characters. Only ASCII alphanumeric characters, dots (.), dashes (-), and underscores (_) are allowed. Invalid characters are automatically replaced with underscores.".to_string())
1548    }
1549
1550    fn suggestions(&self) -> Vec<String> {
1551        match self {
1552            Self::Empty => vec![
1553                "Provide a non-empty spec ID".to_string(),
1554                "Use alphanumeric characters, dots, dashes, or underscores".to_string(),
1555                "Example: my-api-spec, user-auth-v2, payment-system".to_string(),
1556                "Avoid using only special characters or whitespace".to_string(),
1557            ],
1558            Self::OnlyInvalidCharacters => vec![
1559                "Include at least one alphanumeric character, dot, or dash".to_string(),
1560                "Valid characters: A-Z, a-z, 0-9, . (dot), - (dash), _ (underscore)".to_string(),
1561                "Example: my-spec, api-v2, user_auth".to_string(),
1562                "Avoid using only special characters like !@#$%^&*()".to_string(),
1563                "Unicode characters will be replaced with underscores".to_string(),
1564            ],
1565        }
1566    }
1567
1568    fn category(&self) -> ErrorCategory {
1569        ErrorCategory::Validation
1570    }
1571}
1572
1573/// Validation error types
1574#[derive(Debug, Clone, PartialEq, Eq)]
1575pub enum ValidationError {
1576    /// Response starts with meta-commentary instead of document content
1577    MetaSummaryDetected { pattern: String },
1578    /// Response is too short for the phase type
1579    TooShort { actual: usize, minimum: usize },
1580    /// Required section header is missing
1581    MissingSectionHeader { header: String },
1582}
1583
1584impl std::fmt::Display for ValidationError {
1585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1586        match self {
1587            Self::MetaSummaryDetected { pattern } => {
1588                write!(f, "Response contains meta-summary pattern: '{}'", pattern)
1589            }
1590            Self::TooShort { actual, minimum } => {
1591                write!(
1592                    f,
1593                    "Response too short: {} lines (minimum: {} lines)",
1594                    actual, minimum
1595                )
1596            }
1597            Self::MissingSectionHeader { header } => {
1598                write!(f, "Missing required section: '{}'", header)
1599            }
1600        }
1601    }
1602}
1603
1604impl std::error::Error for ValidationError {}
1605
1606/// Errors that can occur during LLM backend operations
1607#[derive(Debug, thiserror::Error)]
1608pub enum LlmError {
1609    /// Transport-level failure (process spawn, HTTP connectivity)
1610    #[error("Transport error: {0}")]
1611    Transport(String),
1612
1613    /// Provider authentication failure (401, 403, missing API key)
1614    #[error("Provider authentication error: {0}")]
1615    ProviderAuth(String),
1616
1617    /// Provider quota/rate limit exceeded (429)
1618    #[error("Provider quota exceeded: {0}")]
1619    ProviderQuota(String),
1620
1621    /// Provider service outage (5xx errors)
1622    #[error("Provider outage: {0}")]
1623    ProviderOutage(String),
1624
1625    /// Invocation timed out
1626    #[error("Timeout after {duration:?}")]
1627    Timeout { duration: Duration },
1628
1629    /// Budget limit exceeded
1630    #[error("Budget exceeded: attempted {attempted} calls, limit is {limit}")]
1631    BudgetExceeded { limit: u32, attempted: u32 },
1632
1633    /// Configuration error
1634    #[error("Misconfiguration: {0}")]
1635    Misconfiguration(String),
1636
1637    /// Unsupported feature or provider
1638    #[error("Unsupported: {0}")]
1639    Unsupported(String),
1640}
1641
1642impl UserFriendlyError for LlmError {
1643    fn user_message(&self) -> String {
1644        match self {
1645            Self::Transport(msg) => format!("LLM transport error: {msg}"),
1646            Self::ProviderAuth(msg) => format!("LLM provider authentication failed: {msg}"),
1647            Self::ProviderQuota(msg) => format!("LLM provider quota exceeded: {msg}"),
1648            Self::ProviderOutage(msg) => format!("LLM provider service outage: {msg}"),
1649            Self::Timeout { duration } => {
1650                format!("LLM invocation timed out after {:?}", duration)
1651            }
1652            Self::BudgetExceeded { limit, attempted } => {
1653                format!(
1654                    "LLM budget exceeded: attempted {} calls, limit is {}",
1655                    attempted, limit
1656                )
1657            }
1658            Self::Misconfiguration(msg) => format!("LLM configuration error: {msg}"),
1659            Self::Unsupported(msg) => format!("LLM feature not supported: {msg}"),
1660        }
1661    }
1662
1663    fn context(&self) -> Option<String> {
1664        match self {
1665            Self::Transport(_) => Some(
1666                "Transport errors occur when the LLM backend cannot be reached or spawned."
1667                    .to_string(),
1668            ),
1669            Self::ProviderAuth(_) => Some(
1670                "Authentication errors indicate missing or invalid API keys or credentials."
1671                    .to_string(),
1672            ),
1673            Self::ProviderQuota(_) => Some(
1674                "Quota errors occur when rate limits or usage limits are exceeded.".to_string(),
1675            ),
1676            Self::ProviderOutage(_) => {
1677                Some("Provider outages are temporary service disruptions.".to_string())
1678            }
1679            Self::Timeout { .. } => Some(
1680                "Timeouts occur when LLM invocations take longer than the configured limit."
1681                    .to_string(),
1682            ),
1683            Self::BudgetExceeded { .. } => {
1684                Some("Budget limits prevent excessive LLM API calls and costs.".to_string())
1685            }
1686            Self::Misconfiguration(_) => Some(
1687                "Configuration errors indicate missing or invalid LLM provider settings."
1688                    .to_string(),
1689            ),
1690            Self::Unsupported(_) => Some(
1691                "Some LLM features are not yet supported in this version of xchecker.".to_string(),
1692            ),
1693        }
1694    }
1695
1696    fn suggestions(&self) -> Vec<String> {
1697        match self {
1698            Self::Transport(_) => vec![
1699                "Check that the LLM provider binary is installed and in PATH".to_string(),
1700                "Verify network connectivity for HTTP providers".to_string(),
1701                "Try running with --verbose to see detailed error information".to_string(),
1702            ],
1703            Self::ProviderAuth(_) => vec![
1704                "Check that the required API key environment variable is set".to_string(),
1705                "Verify the API key is valid and not expired".to_string(),
1706                "For CLI providers, ensure authentication is configured (e.g., 'claude auth login')".to_string(),
1707            ],
1708            Self::ProviderQuota(_) => vec![
1709                "Wait a few minutes and try again".to_string(),
1710                "Check your provider's rate limits and usage dashboard".to_string(),
1711                "Consider using a fallback provider if configured".to_string(),
1712            ],
1713            Self::ProviderOutage(_) => vec![
1714                "Wait a few minutes and try again".to_string(),
1715                "Check the provider's status page for known issues".to_string(),
1716                "Consider using a fallback provider if configured".to_string(),
1717            ],
1718            Self::Timeout { .. } => vec![
1719                "Increase the timeout in configuration or via CLI flags".to_string(),
1720                "Check your internet connection".to_string(),
1721                "Try breaking down complex requests into smaller parts".to_string(),
1722            ],
1723            Self::BudgetExceeded { .. } => vec![
1724                "Increase the budget limit via environment variable (e.g., XCHECKER_OPENROUTER_BUDGET)".to_string(),
1725                "Review which phases are consuming budget".to_string(),
1726                "Consider using a different provider with lower costs".to_string(),
1727            ],
1728            Self::Misconfiguration(_) => vec![
1729                "Check the LLM provider configuration in .xchecker/config.toml".to_string(),
1730                "Ensure required configuration keys are present".to_string(),
1731                "Review the documentation for provider-specific configuration".to_string(),
1732            ],
1733            Self::Unsupported(_) => vec![
1734                "Check the documentation for supported features in this version".to_string(),
1735                "Consider upgrading to a newer version of xchecker".to_string(),
1736                "Use an alternative approach if available".to_string(),
1737            ],
1738        }
1739    }
1740
1741    fn category(&self) -> ErrorCategory {
1742        match self {
1743            Self::Transport(_) => ErrorCategory::ClaudeIntegration,
1744            Self::ProviderAuth(_) => ErrorCategory::Configuration,
1745            Self::ProviderQuota(_) => ErrorCategory::ResourceLimits,
1746            Self::ProviderOutage(_) => ErrorCategory::ClaudeIntegration,
1747            Self::Timeout { .. } => ErrorCategory::PhaseExecution,
1748            Self::BudgetExceeded { .. } => ErrorCategory::ResourceLimits,
1749            Self::Misconfiguration(_) => ErrorCategory::Configuration,
1750            Self::Unsupported(_) => ErrorCategory::Configuration,
1751        }
1752    }
1753}
1754
1755impl UserFriendlyError for XCheckerError {
1756    fn user_message(&self) -> String {
1757        match self {
1758            Self::Config(config_err) => config_err.user_message(),
1759            Self::Phase(phase_err) => phase_err.user_message(),
1760            Self::Claude(claude_err) => claude_err.user_message(),
1761            Self::Runner(runner_err) => runner_err.user_message(),
1762            Self::Llm(llm_err) => llm_err.user_message(),
1763            Self::Io(io_err) => {
1764                format!("File system operation failed: {io_err}")
1765            }
1766            Self::SecretDetected {
1767                pattern: _,
1768                location,
1769            } => {
1770                format!("Security issue: Detected potential secret in {location}")
1771            }
1772            Self::PacketOverflow {
1773                used_bytes,
1774                used_lines,
1775                limit_bytes,
1776                limit_lines,
1777            } => {
1778                format!(
1779                    "Packet size exceeded limits: {used_bytes} bytes/{used_lines} lines used, {limit_bytes} bytes/{limit_lines} lines allowed"
1780                )
1781            }
1782            Self::ConcurrentExecution { id } => {
1783                format!("Another xchecker process is already working on spec '{id}'")
1784            }
1785            Self::PacketPreviewTooLarge { size } => {
1786                format!("Packet preview is too large: {size} bytes")
1787            }
1788            Self::CanonicalizationFailed { phase, reason } => {
1789                format!("Failed to normalize output from {phase} phase: {reason}")
1790            }
1791            Self::ReceiptWriteFailed { path, reason } => {
1792                format!("Failed to save execution record to {path}: {reason}")
1793            }
1794            Self::ModelResolutionError {
1795                alias,
1796                resolved: _,
1797                reason,
1798            } => {
1799                format!("Could not resolve model '{alias}': {reason}")
1800            }
1801            Self::Source(source_err) => source_err.user_message(),
1802            Self::Fixup(fixup_err) => fixup_err.user_message(),
1803            Self::SpecId(spec_id_err) => spec_id_err.user_message(),
1804            Self::Lock(lock_err) => lock_err.user_message(),
1805            Self::ValidationFailed {
1806                phase,
1807                issues,
1808                issue_count: _,
1809            } => {
1810                let issue_list: Vec<String> = issues.iter().map(|i| i.to_string()).collect();
1811                format!(
1812                    "Validation failed for {} phase: {}",
1813                    phase,
1814                    issue_list.join("; ")
1815                )
1816            }
1817        }
1818    }
1819
1820    fn context(&self) -> Option<String> {
1821        match self {
1822            Self::Config(config_err) => config_err.context(),
1823            Self::Phase(phase_err) => phase_err.context(),
1824            Self::Claude(claude_err) => claude_err.context(),
1825            Self::Runner(runner_err) => runner_err.context(),
1826            Self::Llm(llm_err) => llm_err.context(),
1827            Self::Io(_) => Some("This usually indicates a permissions issue or disk space problem.".to_string()),
1828            Self::SecretDetected { pattern, location: _ } => {
1829                Some(format!("The pattern '{pattern}' matches common secret formats. This prevents accidental exposure of sensitive data."))
1830            }
1831            Self::PacketOverflow { used_bytes: _, used_lines: _, limit_bytes: _, limit_lines: _ } => {
1832                Some("Packet size limits prevent excessive token usage and ensure Claude API calls remain efficient.".to_string())
1833            }
1834            Self::ConcurrentExecution { id: _ } => {
1835                Some("xchecker uses file locking to prevent data corruption from simultaneous executions.".to_string())
1836            }
1837            Self::PacketPreviewTooLarge { size: _ } => {
1838                Some("Packet previews are limited to prevent excessive disk usage.".to_string())
1839            }
1840            Self::CanonicalizationFailed { phase: _, reason: _ } => {
1841                Some("Canonicalization ensures deterministic output hashing for reproducible results.".to_string())
1842            }
1843            Self::ReceiptWriteFailed { path: _, reason: _ } => {
1844                Some("Receipts provide audit trails and enable resumption of failed executions.".to_string())
1845            }
1846            Self::ModelResolutionError { alias: _, resolved: _, reason: _ } => {
1847                Some("Model resolution maps short aliases to full model names for Claude API calls.".to_string())
1848            }
1849            Self::Source(source_err) => source_err.context(),
1850            Self::Fixup(fixup_err) => fixup_err.context(),
1851            Self::SpecId(spec_id_err) => spec_id_err.context(),
1852            Self::Lock(lock_err) => lock_err.context(),
1853            Self::ValidationFailed { .. } => {
1854                Some("Strict validation is enabled. LLM output must meet quality requirements: no meta-summaries, minimum length, and required sections.".to_string())
1855            }
1856        }
1857    }
1858
1859    fn suggestions(&self) -> Vec<String> {
1860        match self {
1861            Self::Config(config_err) => config_err.suggestions(),
1862            Self::Phase(phase_err) => phase_err.suggestions(),
1863            Self::Claude(claude_err) => claude_err.suggestions(),
1864            Self::Runner(runner_err) => runner_err.suggestions(),
1865            Self::Llm(llm_err) => llm_err.suggestions(),
1866            Self::Io(_) => vec![
1867                "Check file permissions in the current directory".to_string(),
1868                "Ensure sufficient disk space is available".to_string(),
1869                "Verify the directory is writable".to_string(),
1870            ],
1871            Self::SecretDetected {
1872                pattern: _,
1873                location: _,
1874            } => vec![
1875                "Use --ignore-secret-pattern <regex> to suppress this specific pattern".to_string(),
1876                "Remove or redact the sensitive data from the file".to_string(),
1877                "Add the file to .gitignore if it contains test data".to_string(),
1878            ],
1879            Self::PacketOverflow {
1880                used_bytes: _,
1881                used_lines: _,
1882                limit_bytes,
1883                limit_lines,
1884            } => vec![
1885                format!(
1886                    "Increase packet_max_bytes in config (current limit: {})",
1887                    limit_bytes
1888                ),
1889                format!(
1890                    "Increase packet_max_lines in config (current limit: {})",
1891                    limit_lines
1892                ),
1893                "Use more specific include/exclude patterns to reduce content".to_string(),
1894                "Split large files into smaller, more focused pieces".to_string(),
1895            ],
1896            Self::ConcurrentExecution { id } => vec![
1897                format!(
1898                    "Wait for the other process to complete or use 'xchecker status {}' to check progress",
1899                    id
1900                ),
1901                "Use --force flag to override the lock (use with caution)".to_string(),
1902                "Check if a previous process crashed and left a stale lock".to_string(),
1903            ],
1904            Self::PacketPreviewTooLarge { size: _ } => vec![
1905                "Reduce the packet size limits in configuration".to_string(),
1906                "Use more restrictive include patterns".to_string(),
1907            ],
1908            Self::CanonicalizationFailed {
1909                phase: _,
1910                reason: _,
1911            } => vec![
1912                "Check that the output format matches expected structure".to_string(),
1913                "Verify YAML syntax if the error involves YAML canonicalization".to_string(),
1914                "Review the phase output for formatting issues".to_string(),
1915            ],
1916            Self::ReceiptWriteFailed { path: _, reason: _ } => vec![
1917                "Check write permissions for the .xchecker directory".to_string(),
1918                "Ensure sufficient disk space is available".to_string(),
1919                "Verify the parent directory exists and is writable".to_string(),
1920            ],
1921            Self::ModelResolutionError {
1922                alias: _,
1923                resolved: _,
1924                reason: _,
1925            } => vec![
1926                "Check that the Claude CLI is properly installed and authenticated".to_string(),
1927                "Verify the model name is correct and available".to_string(),
1928                "Try using the full model name instead of an alias".to_string(),
1929            ],
1930            Self::Source(source_err) => source_err.suggestions(),
1931            Self::Fixup(fixup_err) => fixup_err.suggestions(),
1932            Self::SpecId(spec_id_err) => spec_id_err.suggestions(),
1933            Self::Lock(lock_err) => lock_err.suggestions(),
1934            Self::ValidationFailed { phase, .. } => vec![
1935                format!(
1936                    "Set strict_validation = false in config to log warnings instead of failing"
1937                ),
1938                format!(
1939                    "Review the {} phase prompt to ensure it produces compliant output",
1940                    phase
1941                ),
1942                "Check if the LLM response starts with meta-commentary instead of content"
1943                    .to_string(),
1944                "Ensure the response meets minimum length requirements".to_string(),
1945                "Verify required section headers are present in the output".to_string(),
1946            ],
1947        }
1948    }
1949
1950    fn category(&self) -> ErrorCategory {
1951        match self {
1952            Self::Config(_) => ErrorCategory::Configuration,
1953            Self::Phase(_) => ErrorCategory::PhaseExecution,
1954            Self::Claude(_) => ErrorCategory::ClaudeIntegration,
1955            Self::Runner(_) => ErrorCategory::ClaudeIntegration,
1956            Self::Llm(llm_err) => llm_err.category(),
1957            Self::Io(_) => ErrorCategory::FileSystem,
1958            Self::SecretDetected { .. } => ErrorCategory::Security,
1959            Self::PacketOverflow { .. } => ErrorCategory::ResourceLimits,
1960            Self::ConcurrentExecution { .. } => ErrorCategory::Concurrency,
1961            Self::PacketPreviewTooLarge { .. } => ErrorCategory::ResourceLimits,
1962            Self::CanonicalizationFailed { .. } => ErrorCategory::Validation,
1963            Self::ReceiptWriteFailed { .. } => ErrorCategory::FileSystem,
1964            Self::ModelResolutionError { .. } => ErrorCategory::ClaudeIntegration,
1965            Self::Source(_) => ErrorCategory::Configuration,
1966            Self::Fixup(fixup_err) => fixup_err.category(),
1967            Self::SpecId(_) => ErrorCategory::Validation,
1968            Self::Lock(lock_err) => lock_err.category(),
1969            Self::ValidationFailed { .. } => ErrorCategory::Validation,
1970        }
1971    }
1972}
1973
1974// ============================================================================
1975// XCheckerError methods for exit code mapping
1976// ============================================================================
1977
1978impl XCheckerError {
1979    /// Get a user-friendly error message with context and actionable suggestions.
1980    ///
1981    /// This method combines the error message, context, and suggestions into
1982    /// a single formatted string suitable for display to end users. The format is:
1983    ///
1984    /// ```text
1985    /// Error: <user message>
1986    ///
1987    /// Context: <context if available>
1988    ///
1989    /// Suggestions:
1990    ///   • <suggestion 1>
1991    ///   • <suggestion 2>
1992    ///   ...
1993    /// ```
1994    ///
1995    /// # Example
1996    ///
1997    /// ```rust
1998    /// use xchecker_utils::error::XCheckerError;
1999    ///
2000    /// let err = XCheckerError::SecretDetected {
2001    ///     pattern: "ghp_".to_string(),
2002    ///     location: "test.txt".to_string(),
2003    /// };
2004    /// let message = err.display_for_user();
2005    /// assert!(message.contains("Security issue"));
2006    /// assert!(message.contains("Suggestions:"));
2007    /// ```
2008    #[must_use]
2009    pub fn display_for_user(&self) -> String {
2010        self.display_for_user_with_redactor(xchecker_redaction::default_redactor())
2011    }
2012
2013    /// Get a user-friendly error message with context and actionable suggestions,
2014    /// applying a caller-provided redactor as a final safety net (FR-SEC-19).
2015    #[must_use]
2016    pub fn display_for_user_with_redactor(
2017        &self,
2018        redactor: &xchecker_redaction::SecretRedactor,
2019    ) -> String {
2020        let mut output = String::new();
2021
2022        // Add the main error message
2023        output.push_str(&format!("Error: {}\n", self.user_message()));
2024
2025        // Add context if available
2026        if let Some(ctx) = self.context() {
2027            output.push_str(&format!("\nContext: {}\n", ctx));
2028        }
2029
2030        // Add suggestions if any
2031        let suggestions = self.suggestions();
2032        if !suggestions.is_empty() {
2033            output.push_str("\nSuggestions:\n");
2034            for suggestion in suggestions {
2035                output.push_str(&format!("  • {}\n", suggestion));
2036            }
2037        }
2038
2039        // Apply redaction to ensure no secrets leak in user-facing error messages.
2040        // This is the final safety net before output reaches the user (FR-SEC-19).
2041        redactor.redact_string(&output)
2042    }
2043
2044    /// Map this error to the appropriate CLI exit code.
2045    ///
2046    /// This is the single source of truth for both CLI exit codes and receipt
2047    /// `exit_code` fields. The mapping follows the documented exit code table:
2048    ///
2049    /// | Exit Code | Name | Description |
2050    /// |-----------|------|-------------|
2051    /// | 0 | SUCCESS | Completed successfully |
2052    /// | 1 | INTERNAL | General failure |
2053    /// | 2 | CLI_ARGS | Invalid CLI arguments |
2054    /// | 7 | PACKET_OVERFLOW | Packet size exceeded |
2055    /// | 8 | SECRET_DETECTED | Secret found in content |
2056    /// | 9 | LOCK_HELD | Lock already held |
2057    /// | 10 | PHASE_TIMEOUT | Phase timed out |
2058    /// | 70 | CLAUDE_FAILURE | Claude CLI failed |
2059    ///
2060    /// # Example
2061    ///
2062    /// ```rust
2063    /// use xchecker_utils::error::XCheckerError;
2064    /// use xchecker_utils::exit_codes::ExitCode;
2065    ///
2066    /// let err = XCheckerError::SecretDetected {
2067    ///     pattern: "ghp_".to_string(),
2068    ///     location: "test.txt".to_string(),
2069    /// };
2070    /// assert_eq!(err.to_exit_code(), ExitCode::SECRET_DETECTED);
2071    /// ```
2072    #[must_use]
2073    pub fn to_exit_code(&self) -> crate::exit_codes::ExitCode {
2074        use crate::exit_codes::ExitCode;
2075
2076        match self {
2077            // Configuration errors map to CLI_ARGS
2078            XCheckerError::Config(_) => ExitCode::CLI_ARGS,
2079
2080            // Packet overflow before Claude invocation
2081            XCheckerError::PacketOverflow { .. } => ExitCode::PACKET_OVERFLOW,
2082
2083            // Secret detection (redaction hard stop)
2084            XCheckerError::SecretDetected { .. } => ExitCode::SECRET_DETECTED,
2085
2086            // Concurrent execution / lock held
2087            XCheckerError::ConcurrentExecution { .. } => ExitCode::LOCK_HELD,
2088            XCheckerError::Lock(_) => ExitCode::LOCK_HELD,
2089
2090            // Phase errors
2091            XCheckerError::Phase(phase_err) => {
2092                match phase_err {
2093                    PhaseError::Timeout { .. } => ExitCode::PHASE_TIMEOUT,
2094                    // Invalid transitions are CLI argument errors (FR-ORC-001, FR-ORC-002)
2095                    PhaseError::InvalidTransition { .. } => ExitCode::CLI_ARGS,
2096                    PhaseError::DependencyNotSatisfied { .. } => ExitCode::CLI_ARGS,
2097                    _ => ExitCode::INTERNAL,
2098                }
2099            }
2100
2101            // Claude CLI failures
2102            XCheckerError::Claude(_) => ExitCode::CLAUDE_FAILURE,
2103            XCheckerError::Runner(_) => ExitCode::CLAUDE_FAILURE,
2104
2105            // LLM backend errors
2106            XCheckerError::Llm(llm_err) => {
2107                use crate::error::LlmError;
2108                match llm_err {
2109                    LlmError::ProviderAuth(_) => ExitCode::CLAUDE_FAILURE,
2110                    LlmError::ProviderQuota(_) => ExitCode::CLAUDE_FAILURE,
2111                    LlmError::ProviderOutage(_) => ExitCode::CLAUDE_FAILURE,
2112                    LlmError::Timeout { .. } => ExitCode::PHASE_TIMEOUT,
2113                    LlmError::Misconfiguration(_) => ExitCode::CLI_ARGS,
2114                    LlmError::Unsupported(_) => ExitCode::CLI_ARGS,
2115                    LlmError::Transport(_) => ExitCode::CLAUDE_FAILURE,
2116                    LlmError::BudgetExceeded { .. } => ExitCode::CLAUDE_FAILURE,
2117                }
2118            }
2119
2120            // All other errors default to exit code 1 (INTERNAL)
2121            _ => ExitCode::INTERNAL,
2122        }
2123    }
2124}