crackerjack 0.39.11__py3-none-any.whl → 0.40.0__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.
- crackerjack/adapters/__init__.py +5 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +841 -0
- crackerjack/agents/claude_code_bridge.py +202 -1
- crackerjack/cli/facade.py +62 -0
- crackerjack/cli/options.py +14 -4
- crackerjack/dynamic_config.py +1 -1
- crackerjack/services/__init__.py +8 -21
- crackerjack/services/file_modifier.py +520 -0
- crackerjack/workflows/__init__.py +10 -0
- crackerjack/workflows/auto_fix.py +443 -0
- {crackerjack-0.39.11.dist-info → crackerjack-0.40.0.dist-info}/METADATA +60 -17
- {crackerjack-0.39.11.dist-info → crackerjack-0.40.0.dist-info}/RECORD +16 -11
- {crackerjack-0.39.11.dist-info → crackerjack-0.40.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.39.11.dist-info → crackerjack-0.40.0.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.39.11.dist-info → crackerjack-0.40.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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__(
|
crackerjack/cli/options.py
CHANGED
|
@@ -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
|
-
|
|
587
|
+
10,
|
|
587
588
|
"--max-iterations",
|
|
588
|
-
help="Maximum
|
|
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
|
|
710
|
-
"
|
|
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,
|
crackerjack/dynamic_config.py
CHANGED
|
@@ -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,
|
crackerjack/services/__init__.py
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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"]
|