1use std::fmt;
2use std::io;
3use std::path::PathBuf;
4use std::time::Duration;
5use thiserror::Error;
6pub use xchecker_lock::LockError;
7
8#[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
143pub trait UserFriendlyError {
145 fn user_message(&self) -> String;
147
148 fn context(&self) -> Option<String>;
150
151 fn suggestions(&self) -> Vec<String>;
153
154 fn category(&self) -> ErrorCategory;
156}
157
158#[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#[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#[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#[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#[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 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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
1575pub enum ValidationError {
1576 MetaSummaryDetected { pattern: String },
1578 TooShort { actual: usize, minimum: usize },
1580 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#[derive(Debug, thiserror::Error)]
1608pub enum LlmError {
1609 #[error("Transport error: {0}")]
1611 Transport(String),
1612
1613 #[error("Provider authentication error: {0}")]
1615 ProviderAuth(String),
1616
1617 #[error("Provider quota exceeded: {0}")]
1619 ProviderQuota(String),
1620
1621 #[error("Provider outage: {0}")]
1623 ProviderOutage(String),
1624
1625 #[error("Timeout after {duration:?}")]
1627 Timeout { duration: Duration },
1628
1629 #[error("Budget exceeded: attempted {attempted} calls, limit is {limit}")]
1631 BudgetExceeded { limit: u32, attempted: u32 },
1632
1633 #[error("Misconfiguration: {0}")]
1635 Misconfiguration(String),
1636
1637 #[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
1974impl XCheckerError {
1979 #[must_use]
2009 pub fn display_for_user(&self) -> String {
2010 self.display_for_user_with_redactor(xchecker_redaction::default_redactor())
2011 }
2012
2013 #[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 output.push_str(&format!("Error: {}\n", self.user_message()));
2024
2025 if let Some(ctx) = self.context() {
2027 output.push_str(&format!("\nContext: {}\n", ctx));
2028 }
2029
2030 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 redactor.redact_string(&output)
2042 }
2043
2044 #[must_use]
2073 pub fn to_exit_code(&self) -> crate::exit_codes::ExitCode {
2074 use crate::exit_codes::ExitCode;
2075
2076 match self {
2077 XCheckerError::Config(_) => ExitCode::CLI_ARGS,
2079
2080 XCheckerError::PacketOverflow { .. } => ExitCode::PACKET_OVERFLOW,
2082
2083 XCheckerError::SecretDetected { .. } => ExitCode::SECRET_DETECTED,
2085
2086 XCheckerError::ConcurrentExecution { .. } => ExitCode::LOCK_HELD,
2088 XCheckerError::Lock(_) => ExitCode::LOCK_HELD,
2089
2090 XCheckerError::Phase(phase_err) => {
2092 match phase_err {
2093 PhaseError::Timeout { .. } => ExitCode::PHASE_TIMEOUT,
2094 PhaseError::InvalidTransition { .. } => ExitCode::CLI_ARGS,
2096 PhaseError::DependencyNotSatisfied { .. } => ExitCode::CLI_ARGS,
2097 _ => ExitCode::INTERNAL,
2098 }
2099 }
2100
2101 XCheckerError::Claude(_) => ExitCode::CLAUDE_FAILURE,
2103 XCheckerError::Runner(_) => ExitCode::CLAUDE_FAILURE,
2104
2105 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 _ => ExitCode::INTERNAL,
2122 }
2123 }
2124}