crackerjack 0.39.11__py3-none-any.whl → 0.40.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

@@ -8,10 +8,22 @@ agents to consult with expert external agents for complex scenarios.
8
8
 
9
9
  import logging
10
10
  import typing as t
11
+ from contextlib import suppress
11
12
  from pathlib import Path
12
13
 
14
+ from crackerjack.services.file_modifier import SafeFileModifier
15
+
13
16
  from .base import AgentContext, FixResult, Issue, IssueType
14
17
 
18
+ # Conditional import - ClaudeCodeFixer may not be available
19
+ _claude_ai_available = False
20
+ ClaudeCodeFixer: type[t.Any] | None = None
21
+
22
+ with suppress(ImportError):
23
+ from crackerjack.adapters.ai.claude import ClaudeCodeFixer # type: ignore[no-redef]
24
+
25
+ _claude_ai_available = True
26
+
15
27
  # Mapping of internal issue types to Claude Code external agents
16
28
  CLAUDE_CODE_AGENT_MAPPING = {
17
29
  IssueType.COMPLEXITY: ["refactoring-specialist", "crackerjack-architect"],
@@ -31,7 +43,7 @@ EXTERNAL_CONSULTATION_THRESHOLD = 0.8
31
43
 
32
44
 
33
45
  class ClaudeCodeBridge:
34
- """Bridge for consulting Claude Code external agents."""
46
+ """Bridge for consulting Claude Code external agents with real AI integration."""
35
47
 
36
48
  def __init__(self, context: AgentContext) -> None:
37
49
  self.context = context
@@ -39,6 +51,16 @@ class ClaudeCodeBridge:
39
51
  self._agent_path = Path.home() / ".claude" / "agents"
40
52
  self._consultation_cache: dict[str, dict[str, t.Any]] = {}
41
53
 
54
+ # Real AI integration components (if available)
55
+ self.ai_fixer: t.Any | None = None # ClaudeCodeFixer instance or None
56
+ self.file_modifier = SafeFileModifier()
57
+ self._ai_available = _claude_ai_available
58
+
59
+ if not self._ai_available:
60
+ self.logger.warning(
61
+ "Claude AI adapter not available - AI-powered fixes disabled"
62
+ )
63
+
42
64
  def should_consult_external_agent(
43
65
  self, issue: Issue, internal_confidence: float
44
66
  ) -> bool:
@@ -285,6 +307,185 @@ class ClaudeCodeBridge:
285
307
  "validation_steps": ["validate_domain_requirements"],
286
308
  }
287
309
 
310
+ async def _ensure_ai_fixer(self) -> t.Any:
311
+ """Lazy initialization of AI fixer adapter.
312
+
313
+ Returns:
314
+ Initialized ClaudeCodeFixer instance
315
+
316
+ Raises:
317
+ RuntimeError: If Claude AI is not available or initialization fails
318
+ """
319
+ if not self._ai_available:
320
+ raise RuntimeError(
321
+ "Claude AI adapter not available - install ACB with 'uv add acb[ai]'"
322
+ )
323
+
324
+ if self.ai_fixer is None:
325
+ if ClaudeCodeFixer is None:
326
+ raise RuntimeError("ClaudeCodeFixer import failed")
327
+
328
+ self.ai_fixer = ClaudeCodeFixer()
329
+ await self.ai_fixer.init()
330
+ self.logger.debug("Claude AI fixer initialized")
331
+
332
+ return self.ai_fixer
333
+
334
+ async def consult_on_issue(
335
+ self,
336
+ issue: Issue,
337
+ dry_run: bool = False,
338
+ ) -> FixResult:
339
+ """Consult with Claude AI to fix an issue using real AI integration.
340
+
341
+ This method replaces the simulation-based approach with real AI-powered
342
+ code fixing. It:
343
+ 1. Calls the ClaudeCodeFixer adapter to generate a fix
344
+ 2. Validates the AI response (confidence, success)
345
+ 3. Uses SafeFileModifier to safely apply changes
346
+ 4. Handles errors gracefully
347
+ 5. Returns a proper FixResult
348
+
349
+ Args:
350
+ issue: Issue to fix
351
+ dry_run: If True, only generate fix without applying
352
+
353
+ Returns:
354
+ FixResult with fix details and success status
355
+ """
356
+ try:
357
+ # Initialize AI fixer if needed
358
+ fixer = await self._ensure_ai_fixer()
359
+
360
+ # Extract issue details for AI context
361
+ file_path = str(issue.file_path) if issue.file_path else "unknown"
362
+ issue_description = issue.message # Issue uses 'message' not 'description'
363
+ code_snippet = "\n".join(issue.details) if issue.details else ""
364
+ fix_type = issue.type.value
365
+
366
+ self.logger.info(
367
+ f"Consulting Claude AI for {fix_type} issue in {file_path}"
368
+ )
369
+
370
+ # Call AI fixer to generate code fix
371
+ ai_result = await fixer.fix_code_issue(
372
+ file_path=file_path,
373
+ issue_description=issue_description,
374
+ code_context=code_snippet,
375
+ fix_type=fix_type,
376
+ )
377
+
378
+ # Check if AI fix was successful
379
+ if not ai_result.get("success"):
380
+ error_msg = ai_result.get("error", "Unknown AI error")
381
+ self.logger.error(f"AI fix failed: {error_msg}")
382
+
383
+ return FixResult(
384
+ success=False,
385
+ confidence=0.0,
386
+ fixes_applied=[],
387
+ remaining_issues=[issue.id],
388
+ recommendations=[f"AI fix failed: {error_msg}"],
389
+ files_modified=[],
390
+ )
391
+
392
+ # Extract AI response fields
393
+ fixed_code = str(ai_result.get("fixed_code", ""))
394
+ explanation = str(ai_result.get("explanation", "No explanation"))
395
+ confidence = float(ai_result.get("confidence", 0.0))
396
+ changes_made = ai_result.get("changes_made", [])
397
+ potential_side_effects = ai_result.get("potential_side_effects", [])
398
+
399
+ # Validate confidence threshold
400
+ min_confidence = 0.7 # Match AI fixer's default
401
+ if confidence < min_confidence:
402
+ self.logger.warning(
403
+ f"AI confidence {confidence:.2f} below threshold {min_confidence}"
404
+ )
405
+
406
+ return FixResult(
407
+ success=False,
408
+ confidence=confidence,
409
+ fixes_applied=[],
410
+ remaining_issues=[issue.id],
411
+ recommendations=[
412
+ f"AI fix confidence {confidence:.2f} too low (threshold: {min_confidence})",
413
+ explanation,
414
+ ],
415
+ files_modified=[],
416
+ )
417
+
418
+ # Apply fix using SafeFileModifier
419
+ if not dry_run and fixed_code:
420
+ modify_result = await self.file_modifier.apply_fix(
421
+ file_path=file_path,
422
+ fixed_content=fixed_code,
423
+ dry_run=dry_run,
424
+ create_backup=True,
425
+ )
426
+
427
+ if not modify_result.get("success"):
428
+ error_msg = modify_result.get("error", "Unknown modification error")
429
+ self.logger.error(f"File modification failed: {error_msg}")
430
+
431
+ return FixResult(
432
+ success=False,
433
+ confidence=confidence,
434
+ fixes_applied=[],
435
+ remaining_issues=[issue.id],
436
+ recommendations=[
437
+ f"File modification failed: {error_msg}",
438
+ explanation,
439
+ ],
440
+ files_modified=[],
441
+ )
442
+
443
+ # Success - fix applied
444
+ self.logger.info(
445
+ f"Successfully applied AI fix to {file_path} (confidence: {confidence:.2f})"
446
+ )
447
+
448
+ return FixResult(
449
+ success=True,
450
+ confidence=confidence,
451
+ fixes_applied=[f"Fixed {fix_type} in {file_path}"],
452
+ remaining_issues=[],
453
+ recommendations=[
454
+ explanation,
455
+ *[f"Change: {change}" for change in changes_made],
456
+ *[
457
+ f"Potential side effect: {effect}"
458
+ for effect in potential_side_effects
459
+ ],
460
+ ],
461
+ files_modified=[file_path],
462
+ )
463
+ else:
464
+ # Dry-run mode or no code - just report recommendation
465
+ return FixResult(
466
+ success=True,
467
+ confidence=confidence,
468
+ fixes_applied=[],
469
+ remaining_issues=[issue.id],
470
+ recommendations=[
471
+ f"AI suggests (dry-run): {explanation}",
472
+ *[f"Change: {change}" for change in changes_made],
473
+ ],
474
+ files_modified=[],
475
+ )
476
+
477
+ except Exception as e:
478
+ self.logger.exception(f"Unexpected error in consult_on_issue: {e}")
479
+
480
+ return FixResult(
481
+ success=False,
482
+ confidence=0.0,
483
+ fixes_applied=[],
484
+ remaining_issues=[issue.id],
485
+ recommendations=[f"Unexpected error: {e}"],
486
+ files_modified=[],
487
+ )
488
+
288
489
  def create_enhanced_fix_result(
289
490
  self, base_result: FixResult, consultations: list[dict[str, t.Any]]
290
491
  ) -> FixResult:
crackerjack/cli/facade.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import shlex
2
3
  from pathlib import Path
3
4
 
4
5
  from rich.console import Console
@@ -6,6 +7,67 @@ from rich.console import Console
6
7
  from crackerjack.core.workflow_orchestrator import WorkflowOrchestrator
7
8
  from crackerjack.models.protocols import OptionsProtocol
8
9
 
10
+ # Valid semantic commands for crackerjack operations
11
+ VALID_COMMANDS = {"test", "lint", "check", "format", "security", "complexity", "all"}
12
+
13
+
14
+ def validate_command(
15
+ command: str | None, args: str | None = None
16
+ ) -> tuple[str, list[str]]:
17
+ """Validate command and detect common misuse patterns.
18
+
19
+ Args:
20
+ command: Semantic command name (test, lint, check, etc.)
21
+ args: Additional arguments as a string
22
+
23
+ Returns:
24
+ Tuple of (validated_command, cleaned_args_list)
25
+
26
+ Raises:
27
+ ValueError: If command is invalid or misused
28
+
29
+ Examples:
30
+ >>> validate_command("test", "")
31
+ ("test", [])
32
+ >>> validate_command("check", "--verbose")
33
+ ("check", ["--verbose"])
34
+ >>> validate_command("--ai-fix", "-t")
35
+ Traceback (most recent call last):
36
+ ...
37
+ ValueError: Invalid command: '--ai-fix'...
38
+ """
39
+ # CRITICAL: Check for None command first
40
+ if command is None:
41
+ raise ValueError("Command cannot be None")
42
+
43
+ # Detect if user put flags in command parameter
44
+ if command.startswith("--") or command.startswith("-"):
45
+ raise ValueError(
46
+ f"Invalid command: {command!r}\n"
47
+ f"Commands should be semantic (e.g., 'test', 'lint', 'check')\n"
48
+ f"Use ai_agent_mode=True parameter for auto-fix, not --ai-fix in command"
49
+ )
50
+
51
+ # Validate against known semantic commands
52
+ if command not in VALID_COMMANDS:
53
+ raise ValueError(
54
+ f"Unknown command: {command!r}\n"
55
+ f"Valid commands: {', '.join(sorted(VALID_COMMANDS))}"
56
+ )
57
+
58
+ # Parse args and detect --ai-fix misuse
59
+ # Handle None gracefully by converting to empty string
60
+ args_str = args if args is not None else ""
61
+ # Use shlex.split for proper shell argument parsing (handles quotes)
62
+ parsed_args = shlex.split(args_str) if args_str else []
63
+ if "--ai-fix" in parsed_args:
64
+ raise ValueError(
65
+ "Do not pass --ai-fix in args parameter\n"
66
+ "Use ai_agent_mode=True parameter instead"
67
+ )
68
+
69
+ return command, parsed_args
70
+
9
71
 
10
72
  class CrackerjackCLIFacade:
11
73
  def __init__(
@@ -149,6 +149,7 @@ class Options(BaseModel):
149
149
  strip_code: bool | None = None # Replaces clean
150
150
  run_tests: bool = False # Replaces test
151
151
  ai_fix: bool | None = None # Replaces ai_agent
152
+ dry_run: bool = False # Preview fixes without applying
152
153
  full_release: str | None = None # Replaces all
153
154
  show_progress: bool | None = None # Replaces track_progress
154
155
  advanced_monitor: bool | None = None # Replaces enhanced_monitor
@@ -583,9 +584,9 @@ CLI_OPTIONS = {
583
584
  help="Port for unified dashboard server (default: 8675).",
584
585
  ),
585
586
  "max_iterations": typer.Option(
586
- 5,
587
+ 10,
587
588
  "--max-iterations",
588
- help="Maximum number of iterations for AI agent auto-fixing workflows (default: 5).",
589
+ help="Maximum auto-fix iterations (default: 10).",
589
590
  ),
590
591
  "ai_debug": typer.Option(
591
592
  False,
@@ -706,10 +707,17 @@ CLI_OPTIONS = {
706
707
  None,
707
708
  "--ai-fix",
708
709
  help=(
709
- "Enable AI-powered automatic fixing of code quality issues "
710
- "and test failures."
710
+ "Enable AI-powered auto-fixing. "
711
+ "Iteratively fixes code issues using Claude AI. "
712
+ "Requires ANTHROPIC_API_KEY environment variable. "
713
+ "Max 10 iterations, stops when all hooks pass."
711
714
  ),
712
715
  ),
716
+ "dry_run": typer.Option(
717
+ False,
718
+ "--dry-run",
719
+ help="Preview fixes without modifying files (implies --ai-fix).",
720
+ ),
713
721
  "full_release": typer.Option(
714
722
  None,
715
723
  "-a",
@@ -1045,6 +1053,7 @@ def create_options(
1045
1053
  strip_code: bool | None = None,
1046
1054
  run_tests: bool = False,
1047
1055
  ai_fix: bool | None = None,
1056
+ dry_run: bool = False,
1048
1057
  full_release: str | None = None,
1049
1058
  show_progress: bool | None = None,
1050
1059
  advanced_monitor: bool | None = None,
@@ -1142,6 +1151,7 @@ def create_options(
1142
1151
  strip_code=strip_code,
1143
1152
  run_tests=run_tests,
1144
1153
  ai_fix=ai_fix,
1154
+ dry_run=dry_run,
1145
1155
  full_release=full_release,
1146
1156
  show_progress=show_progress,
1147
1157
  advanced_monitor=advanced_monitor,
@@ -174,7 +174,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
174
174
  "stages": None,
175
175
  "args": None,
176
176
  "files": None,
177
- "exclude": r"uv\.lock|pyproject\.toml|tests/.*|docs/.*|.*\.md",
177
+ "exclude": r"uv\.lock|pyproject\.toml|tests/.*|docs/.*|\.claude/.*|.*\.md",
178
178
  "additional_dependencies": None,
179
179
  "types_or": None,
180
180
  "language": None,
@@ -1,22 +1,9 @@
1
- from .cache import CacheEntry, CacheStats, CrackerjackCache, FileCache, InMemoryCache
2
- from .config import ConfigurationService
3
- from .file_hasher import FileHasher, SmartFileWatcher
4
- from .filesystem import FileSystemService
5
- from .git import GitService
6
- from .initialization import InitializationService
7
- from .security import SecurityService
1
+ """Services for crackerjack.
8
2
 
9
- __all__ = [
10
- "CacheEntry",
11
- "CacheStats",
12
- "ConfigurationService",
13
- "CrackerjackCache",
14
- "FileCache",
15
- "FileHasher",
16
- "FileSystemService",
17
- "GitService",
18
- "InMemoryCache",
19
- "InitializationService",
20
- "SecurityService",
21
- "SmartFileWatcher",
22
- ]
3
+ This package contains service classes that provide business logic
4
+ and operations that are used across multiple components.
5
+ """
6
+
7
+ from crackerjack.services.file_modifier import SafeFileModifier
8
+
9
+ __all__ = ["SafeFileModifier"]