crackerjack 0.31.4__py3-none-any.whl → 0.31.7__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/__main__.py CHANGED
@@ -171,6 +171,7 @@ def main(
171
171
  coverage_status: bool = CLI_OPTIONS["coverage_status"],
172
172
  coverage_goal: float | None = CLI_OPTIONS["coverage_goal"],
173
173
  no_coverage_ratchet: bool = CLI_OPTIONS["no_coverage_ratchet"],
174
+ boost_coverage: bool = CLI_OPTIONS["boost_coverage"],
174
175
  ) -> None:
175
176
  options = create_options(
176
177
  commit,
@@ -207,6 +208,7 @@ def main(
207
208
  coverage_status,
208
209
  coverage_goal,
209
210
  no_coverage_ratchet,
211
+ boost_coverage,
210
212
  )
211
213
 
212
214
  if ai_debug:
@@ -26,6 +26,7 @@ class IssueType(Enum):
26
26
  PERFORMANCE = "performance"
27
27
  DOCUMENTATION = "documentation"
28
28
  TEST_ORGANIZATION = "test_organization"
29
+ COVERAGE_IMPROVEMENT = "coverage_improvement"
29
30
 
30
31
 
31
32
  @dataclass
@@ -23,6 +23,7 @@ class TestCreationAgent(SubAgent):
23
23
  IssueType.TEST_FAILURE,
24
24
  IssueType.DEPENDENCY,
25
25
  IssueType.TEST_ORGANIZATION,
26
+ IssueType.COVERAGE_IMPROVEMENT,
26
27
  }
27
28
 
28
29
  async def can_handle(self, issue: Issue) -> float:
@@ -31,6 +32,10 @@ class TestCreationAgent(SubAgent):
31
32
 
32
33
  message_lower = issue.message.lower()
33
34
 
35
+ # Handle coverage improvement requests with perfect confidence
36
+ if issue.type == IssueType.COVERAGE_IMPROVEMENT:
37
+ return 1.0
38
+
34
39
  # Handle test organization issues with high confidence
35
40
  if issue.type == IssueType.TEST_ORGANIZATION:
36
41
  return self._check_test_organization_confidence(message_lower)
@@ -48,6 +48,7 @@ class Options(BaseModel):
48
48
  keep_releases: int = 10
49
49
  track_progress: bool = False
50
50
  orchestrated: bool = False
51
+ boost_coverage: bool = True
51
52
  coverage: bool = False
52
53
  orchestration_strategy: str = "adaptive"
53
54
  orchestration_progress: str = "granular"
@@ -324,6 +325,11 @@ CLI_OPTIONS = {
324
325
  "--no-coverage-ratchet",
325
326
  help="Disable coverage ratchet system temporarily (for experiments).",
326
327
  ),
328
+ "boost_coverage": typer.Option(
329
+ True,
330
+ "--boost-coverage/--no-boost-coverage",
331
+ help="Automatically improve test coverage after successful workflow execution (default: True).",
332
+ ),
327
333
  }
328
334
 
329
335
 
@@ -362,6 +368,7 @@ def create_options(
362
368
  coverage_status: bool,
363
369
  coverage_goal: float | None,
364
370
  no_coverage_ratchet: bool,
371
+ boost_coverage: bool,
365
372
  ) -> Options:
366
373
  return Options(
367
374
  commit=commit,
@@ -398,4 +405,5 @@ def create_options(
398
405
  coverage_status=coverage_status,
399
406
  coverage_goal=coverage_goal,
400
407
  no_coverage_ratchet=no_coverage_ratchet,
408
+ boost_coverage=boost_coverage,
401
409
  )
@@ -501,12 +501,36 @@ class PhaseCoordinator:
501
501
  self.console.print(
502
502
  f"[red]❌[/red] {hook_type.title()} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
503
503
  )
504
+
505
+ # Collect detailed hook failure information for AI agent processing
506
+ detailed_error_msg = self._build_detailed_hook_error_message(results, summary)
507
+
504
508
  self.session.fail_task(
505
509
  f"{hook_type}_hooks",
506
- f"{summary['failed']} failed, {summary['errors']} errors",
510
+ detailed_error_msg,
507
511
  )
508
512
  return False
509
513
 
514
+ def _build_detailed_hook_error_message(
515
+ self, results: list[t.Any], summary: dict[str, t.Any]
516
+ ) -> str:
517
+ """Build detailed error message with specific hook failure information."""
518
+ error_parts = [f"{summary['failed']} failed, {summary['errors']} errors"]
519
+
520
+ # Extract specific hook failures
521
+ failed_hooks = []
522
+ for result in results:
523
+ if hasattr(result, "failed") and result.failed:
524
+ hook_name = getattr(result, "hook_id", "") or getattr(
525
+ result, "name", "unknown"
526
+ )
527
+ failed_hooks.append(hook_name.lower())
528
+
529
+ if failed_hooks:
530
+ error_parts.append(f"Failed hooks: {', '.join(failed_hooks)}")
531
+
532
+ return " | ".join(error_parts)
533
+
510
534
  def _should_retry_fast_hooks(self, results: list[t.Any]) -> bool:
511
535
  formatting_hooks = {
512
536
  "ruff-format",
@@ -259,7 +259,7 @@ class WorkflowPipeline:
259
259
 
260
260
  success = self.phases.run_comprehensive_hooks_only(options)
261
261
  if not success:
262
- self.session.fail_task("workflow", "Comprehensive hooks failed")
262
+ self.session.fail_task("comprehensive_hooks", "Comprehensive hooks failed")
263
263
  self._update_mcp_status("comprehensive", "failed")
264
264
  # In AI agent mode, continue to collect more failures
265
265
  # In non-AI mode, this will be handled by caller
@@ -345,7 +345,25 @@ class WorkflowPipeline:
345
345
  """Run AI agent fixing phase to analyze and fix collected failures."""
346
346
  self._update_mcp_status("ai_fixing", "running")
347
347
  self.logger.info("Starting AI agent fixing phase")
348
+ self._log_debug_phase_start()
348
349
 
350
+ try:
351
+ agent_coordinator = self._setup_agent_coordinator()
352
+ issues = await self._collect_issues_from_failures()
353
+
354
+ if not issues:
355
+ return self._handle_no_issues_found()
356
+
357
+ self.logger.info(f"AI agents will attempt to fix {len(issues)} issues")
358
+ fix_result = await agent_coordinator.handle_issues(issues)
359
+
360
+ return await self._process_fix_results(options, fix_result)
361
+
362
+ except Exception as e:
363
+ return self._handle_fixing_phase_error(e)
364
+
365
+ def _log_debug_phase_start(self) -> None:
366
+ """Log debug information for phase start."""
349
367
  if self._should_debug():
350
368
  self.debugger.log_workflow_phase(
351
369
  "ai_agent_fixing",
@@ -353,77 +371,150 @@ class WorkflowPipeline:
353
371
  details={"ai_agent": True},
354
372
  )
355
373
 
356
- try:
357
- # Create AI agent context
358
- agent_context = AgentContext(
359
- project_path=self.pkg_path,
360
- session_id=getattr(self.session, "session_id", None),
361
- )
374
+ def _setup_agent_coordinator(self) -> AgentCoordinator:
375
+ """Set up agent coordinator with proper context."""
376
+ from crackerjack.agents.coordinator import AgentCoordinator
362
377
 
363
- # Initialize agent coordinator
364
- agent_coordinator = AgentCoordinator(agent_context)
365
- agent_coordinator.initialize_agents()
378
+ agent_context = AgentContext(
379
+ project_path=self.pkg_path,
380
+ session_id=getattr(self.session, "session_id", None),
381
+ )
366
382
 
367
- # Collect issues from failures
368
- issues = await self._collect_issues_from_failures()
383
+ agent_coordinator = AgentCoordinator(agent_context)
384
+ agent_coordinator.initialize_agents()
385
+ return agent_coordinator
369
386
 
370
- if not issues:
371
- self.logger.info("No issues collected for AI agent fixing")
372
- self._update_mcp_status("ai_fixing", "completed")
373
- return True
387
+ def _handle_no_issues_found(self) -> bool:
388
+ """Handle case when no issues are collected."""
389
+ self.logger.info("No issues collected for AI agent fixing")
390
+ self._update_mcp_status("ai_fixing", "completed")
391
+ return True
374
392
 
375
- self.logger.info(f"AI agents will attempt to fix {len(issues)} issues")
393
+ async def _process_fix_results(
394
+ self, options: OptionsProtocol, fix_result: t.Any
395
+ ) -> bool:
396
+ """Process fix results and verify success."""
397
+ verification_success = await self._verify_fixes_applied(options, fix_result)
398
+ success = fix_result.success and verification_success
376
399
 
377
- # Let agents handle the issues
378
- fix_result = await agent_coordinator.handle_issues(issues)
400
+ if success:
401
+ self._handle_successful_fixes(fix_result)
402
+ else:
403
+ self._handle_failed_fixes(fix_result, verification_success)
379
404
 
380
- success = fix_result.success
381
- if success:
382
- self.logger.info("AI agents successfully fixed all issues")
383
- self._update_mcp_status("ai_fixing", "completed")
405
+ self._log_debug_phase_completion(success, fix_result)
406
+ return success
384
407
 
385
- # Log fix counts for debugging
386
- if self._should_debug():
387
- total_fixes = len(fix_result.fixes_applied)
388
- # Estimate test vs hook fixes based on original issue types
389
- test_fixes = len(
390
- [f for f in fix_result.fixes_applied if "test" in f.lower()],
391
- )
392
- hook_fixes = total_fixes - test_fixes
393
- self.debugger.log_test_fixes(test_fixes)
394
- self.debugger.log_hook_fixes(hook_fixes)
395
- else:
396
- self.logger.warning(
397
- f"AI agents could not fix all issues: {fix_result.remaining_issues}",
398
- )
399
- self._update_mcp_status("ai_fixing", "failed")
408
+ def _handle_successful_fixes(self, fix_result: t.Any) -> None:
409
+ """Handle successful fix results."""
410
+ self.logger.info(
411
+ "AI agents successfully fixed all issues and verification passed"
412
+ )
413
+ self._update_mcp_status("ai_fixing", "completed")
414
+ self._log_fix_counts_if_debugging(fix_result)
400
415
 
401
- if self._should_debug():
402
- self.debugger.log_workflow_phase(
403
- "ai_agent_fixing",
404
- "completed" if success else "failed",
405
- details={
406
- "confidence": fix_result.confidence,
407
- "fixes_applied": len(fix_result.fixes_applied),
408
- "remaining_issues": len(fix_result.remaining_issues),
409
- },
410
- )
416
+ def _handle_failed_fixes(
417
+ self, fix_result: t.Any, verification_success: bool
418
+ ) -> None:
419
+ """Handle failed fix results."""
420
+ if not verification_success:
421
+ self.logger.warning(
422
+ "AI agent fixes did not pass verification - issues still exist"
423
+ )
424
+ else:
425
+ self.logger.warning(
426
+ f"AI agents could not fix all issues: {fix_result.remaining_issues}",
427
+ )
428
+ self._update_mcp_status("ai_fixing", "failed")
411
429
 
412
- return success
430
+ def _log_fix_counts_if_debugging(self, fix_result: t.Any) -> None:
431
+ """Log fix counts for debugging if debug mode is enabled."""
432
+ if not self._should_debug():
433
+ return
413
434
 
414
- except Exception as e:
415
- self.logger.exception(f"AI agent fixing phase failed: {e}")
416
- self.session.fail_task("ai_fixing", f"AI agent fixing failed: {e}")
417
- self._update_mcp_status("ai_fixing", "failed")
435
+ total_fixes = len(fix_result.fixes_applied)
436
+ test_fixes = len(
437
+ [f for f in fix_result.fixes_applied if "test" in f.lower()],
438
+ )
439
+ hook_fixes = total_fixes - test_fixes
440
+ self.debugger.log_test_fixes(test_fixes)
441
+ self.debugger.log_hook_fixes(hook_fixes)
418
442
 
419
- if self._should_debug():
420
- self.debugger.log_workflow_phase(
421
- "ai_agent_fixing",
422
- "failed",
423
- details={"error": str(e)},
443
+ def _log_debug_phase_completion(self, success: bool, fix_result: t.Any) -> None:
444
+ """Log debug information for phase completion."""
445
+ if self._should_debug():
446
+ self.debugger.log_workflow_phase(
447
+ "ai_agent_fixing",
448
+ "completed" if success else "failed",
449
+ details={
450
+ "confidence": fix_result.confidence,
451
+ "fixes_applied": len(fix_result.fixes_applied),
452
+ "remaining_issues": len(fix_result.remaining_issues),
453
+ },
454
+ )
455
+
456
+ def _handle_fixing_phase_error(self, error: Exception) -> bool:
457
+ """Handle errors during the fixing phase."""
458
+ self.logger.exception(f"AI agent fixing phase failed: {error}")
459
+ self.session.fail_task("ai_fixing", f"AI agent fixing failed: {error}")
460
+ self._update_mcp_status("ai_fixing", "failed")
461
+
462
+ if self._should_debug():
463
+ self.debugger.log_workflow_phase(
464
+ "ai_agent_fixing",
465
+ "failed",
466
+ details={"error": str(error)},
467
+ )
468
+
469
+ return False
470
+
471
+ async def _verify_fixes_applied(
472
+ self, options: OptionsProtocol, fix_result: t.Any
473
+ ) -> bool:
474
+ """Verify that AI agent fixes actually resolved the issues by re-running checks."""
475
+ if not fix_result.fixes_applied:
476
+ return True # No fixes were applied, nothing to verify
477
+
478
+ self.logger.info("Verifying AI agent fixes by re-running quality checks")
479
+
480
+ # Re-run the phases that previously failed to verify fixes
481
+ verification_success = True
482
+
483
+ # Check if we need to re-run tests
484
+ if any("test" in fix.lower() for fix in fix_result.fixes_applied):
485
+ self.logger.info("Re-running tests to verify test fixes")
486
+ test_success = self.phases.run_testing_phase(options)
487
+ if not test_success:
488
+ self.logger.warning(
489
+ "Test verification failed - test fixes did not work"
490
+ )
491
+ verification_success = False
492
+
493
+ # Check if we need to re-run comprehensive hooks
494
+ hook_fixes = [
495
+ f
496
+ for f in fix_result.fixes_applied
497
+ if "hook" not in f.lower()
498
+ or "complexity" in f.lower()
499
+ or "type" in f.lower()
500
+ ]
501
+ if hook_fixes:
502
+ self.logger.info("Re-running comprehensive hooks to verify hook fixes")
503
+ hook_success = self.phases.run_comprehensive_hooks_only(options)
504
+ if not hook_success:
505
+ self.logger.warning(
506
+ "Hook verification failed - hook fixes did not work"
424
507
  )
508
+ verification_success = False
425
509
 
426
- return False
510
+ if verification_success:
511
+ self.logger.info("All AI agent fixes verified successfully")
512
+ else:
513
+ self.logger.error(
514
+ "Verification failed - some fixes did not resolve the issues"
515
+ )
516
+
517
+ return verification_success
427
518
 
428
519
  async def _collect_issues_from_failures(self) -> list[Issue]:
429
520
  """Collect issues from test and comprehensive hook failures."""
@@ -469,29 +560,124 @@ class WorkflowPipeline:
469
560
  issues: list[Issue] = []
470
561
  hook_count = 0
471
562
 
472
- if self.session.session_tracker:
473
- for task_id, task_data in self.session.session_tracker.tasks.items():
474
- if task_data.status == "failed" and task_id in (
475
- "fast_hooks",
476
- "comprehensive_hooks",
477
- ):
478
- hook_count += 1
479
- issue_type = (
480
- IssueType.FORMATTING
481
- if "fast" in task_id
482
- else IssueType.TYPE_ERROR
563
+ if not self.session.session_tracker:
564
+ return issues, hook_count
565
+
566
+ for task_id, task_data in self.session.session_tracker.tasks.items():
567
+ if self._is_failed_hook_task(task_data, task_id):
568
+ hook_count += 1
569
+ hook_issues = self._process_hook_failure(task_id, task_data)
570
+ issues.extend(hook_issues)
571
+
572
+ return issues, hook_count
573
+
574
+ def _is_failed_hook_task(self, task_data: t.Any, task_id: str) -> bool:
575
+ """Check if a task is a failed hook task."""
576
+ return task_data.status == "failed" and task_id in (
577
+ "fast_hooks",
578
+ "comprehensive_hooks",
579
+ )
580
+
581
+ def _process_hook_failure(self, task_id: str, task_data: t.Any) -> list[Issue]:
582
+ """Process a single hook failure and return corresponding issues."""
583
+ error_msg = getattr(task_data, "error_message", "Unknown error")
584
+ specific_issues = self._parse_hook_error_details(task_id, error_msg)
585
+
586
+ if specific_issues:
587
+ return specific_issues
588
+
589
+ return [self._create_generic_hook_issue(task_id, error_msg)]
590
+
591
+ def _create_generic_hook_issue(self, task_id: str, error_msg: str) -> Issue:
592
+ """Create a generic issue for unspecific hook failures."""
593
+ issue_type = IssueType.FORMATTING if "fast" in task_id else IssueType.TYPE_ERROR
594
+ return Issue(
595
+ id=f"hook_failure_{task_id}",
596
+ type=issue_type,
597
+ severity=Priority.MEDIUM,
598
+ message=error_msg,
599
+ stage=task_id.replace("_hooks", ""),
600
+ )
601
+
602
+ def _parse_hook_error_details(self, task_id: str, error_msg: str) -> list[Issue]:
603
+ """Parse specific hook failure details to create targeted issues."""
604
+ issues: list[Issue] = []
605
+
606
+ # For comprehensive hooks, parse specific tool failures
607
+ if task_id == "comprehensive_hooks":
608
+ # Check for complexipy failures (complexity violations)
609
+ if "complexipy" in error_msg.lower():
610
+ issues.append(
611
+ Issue(
612
+ id="complexipy_violation",
613
+ type=IssueType.COMPLEXITY,
614
+ severity=Priority.HIGH,
615
+ message="Code complexity violation detected by complexipy",
616
+ stage="comprehensive",
617
+ )
618
+ )
619
+
620
+ # Check for pyright failures (type errors)
621
+ if "pyright" in error_msg.lower():
622
+ issues.append(
623
+ Issue(
624
+ id="pyright_type_error",
625
+ type=IssueType.TYPE_ERROR,
626
+ severity=Priority.HIGH,
627
+ message="Type checking errors detected by pyright",
628
+ stage="comprehensive",
629
+ )
630
+ )
631
+
632
+ # Check for bandit failures (security issues)
633
+ if "bandit" in error_msg.lower():
634
+ issues.append(
635
+ Issue(
636
+ id="bandit_security_issue",
637
+ type=IssueType.SECURITY,
638
+ severity=Priority.HIGH,
639
+ message="Security vulnerabilities detected by bandit",
640
+ stage="comprehensive",
483
641
  )
484
- error_msg = getattr(task_data, "error_message", "Unknown error")
485
- issue = Issue(
486
- id=f"hook_failure_{task_id}",
487
- type=issue_type,
642
+ )
643
+
644
+ # Check for refurb failures (code quality issues)
645
+ if "refurb" in error_msg.lower():
646
+ issues.append(
647
+ Issue(
648
+ id="refurb_quality_issue",
649
+ type=IssueType.PERFORMANCE, # Use PERFORMANCE as closest match for refurb issues
488
650
  severity=Priority.MEDIUM,
489
- message=error_msg,
490
- stage=task_id.replace("_hooks", ""),
651
+ message="Code quality issues detected by refurb",
652
+ stage="comprehensive",
491
653
  )
492
- issues.append(issue)
654
+ )
493
655
 
494
- return issues, hook_count
656
+ # Check for vulture failures (dead code)
657
+ if "vulture" in error_msg.lower():
658
+ issues.append(
659
+ Issue(
660
+ id="vulture_dead_code",
661
+ type=IssueType.DEAD_CODE,
662
+ severity=Priority.MEDIUM,
663
+ message="Dead code detected by vulture",
664
+ stage="comprehensive",
665
+ )
666
+ )
667
+
668
+ elif task_id == "fast_hooks":
669
+ # Fast hooks are typically formatting issues
670
+ issues.append(
671
+ Issue(
672
+ id="fast_hooks_formatting",
673
+ type=IssueType.FORMATTING,
674
+ severity=Priority.LOW,
675
+ message="Code formatting issues detected",
676
+ stage="fast",
677
+ )
678
+ )
679
+
680
+ return issues
495
681
 
496
682
  def _log_failure_counts_if_debugging(
497
683
  self, test_count: int, hook_count: int
@@ -123,7 +123,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
123
123
  "id": "uv-lock",
124
124
  "name": None,
125
125
  "repo": "https://github.com/astral-sh/uv-pre-commit",
126
- "rev": "0.8.13",
126
+ "rev": "0.8.14",
127
127
  "tier": 1,
128
128
  "time_estimate": 0.5,
129
129
  "stages": None,
@@ -195,7 +195,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
195
195
  "id": "ruff-check",
196
196
  "name": None,
197
197
  "repo": "https://github.com/astral-sh/ruff-pre-commit",
198
- "rev": "v0.12.10",
198
+ "rev": "v0.12.11",
199
199
  "tier": 2,
200
200
  "time_estimate": 1.5,
201
201
  "stages": None,
@@ -212,7 +212,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
212
212
  "id": "ruff-format",
213
213
  "name": None,
214
214
  "repo": "https://github.com/astral-sh/ruff-pre-commit",
215
- "rev": "v0.12.10",
215
+ "rev": "v0.12.11",
216
216
  "tier": 2,
217
217
  "time_estimate": 1.0,
218
218
  "stages": None,
@@ -233,7 +233,29 @@ class PublishManagerImpl:
233
233
  self.console.print("[yellow]🔍[/yellow] Would build package")
234
234
  return True
235
235
 
236
+ def _clean_dist_directory(self) -> None:
237
+ """Clean dist directory to ensure only current version artifacts are uploaded."""
238
+ dist_dir = self.pkg_path / "dist"
239
+ if not dist_dir.exists():
240
+ return
241
+
242
+ try:
243
+ import shutil
244
+
245
+ # Remove entire dist directory and recreate it
246
+ shutil.rmtree(dist_dir)
247
+ dist_dir.mkdir(exist_ok=True)
248
+ self.console.print("[cyan]🧹[/cyan] Cleaned dist directory for fresh build")
249
+ except Exception as e:
250
+ self.console.print(
251
+ f"[yellow]⚠️[/yellow] Warning: Could not clean dist directory: {e}"
252
+ )
253
+ # Continue with build anyway - uv publish will fail with clear error
254
+
236
255
  def _execute_build(self) -> bool:
256
+ # Clean dist directory before building to avoid uploading multiple versions
257
+ self._clean_dist_directory()
258
+
237
259
  result = self._run_command(["uv", "build"])
238
260
 
239
261
  if result.returncode != 0:
@@ -234,11 +234,14 @@ class TestExecutor:
234
234
 
235
235
  def _handle_session_events(self, line: str, progress: TestProgress) -> bool:
236
236
  """Handle pytest session events."""
237
- if "session starts" in line:
238
- progress.update(collection_status="Session starting...")
237
+ if "session starts" in line and progress.collection_status != "Session started":
238
+ progress.update(collection_status="Session started")
239
239
  return True
240
- elif "test session starts" in line:
241
- progress.update(collection_status="Starting test collection...")
240
+ elif (
241
+ "test session starts" in line
242
+ and progress.collection_status != "Test collection started"
243
+ ):
244
+ progress.update(collection_status="Test collection started")
242
245
  return True
243
246
  return False
244
247
 
@@ -308,7 +311,12 @@ class TestExecutor:
308
311
 
309
312
  def _should_refresh_display(self, progress: TestProgress) -> bool:
310
313
  """Determine if display should be refreshed."""
311
- return True # Simplified - let Rich handle refresh rate
314
+ # Only refresh on significant changes to reduce spam
315
+ return (
316
+ progress.is_complete
317
+ or progress.total_tests > 0
318
+ or len(progress.current_test) > 0
319
+ )
312
320
 
313
321
  def _mark_test_as_stuck(self, progress: TestProgress, test_name: str) -> None:
314
322
  """Mark a test as potentially stuck."""