crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,561 @@
1
+ import logging
2
+ import time
3
+ import typing as t
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ from crackerjack.code_cleaner import CodeCleaner
9
+ from crackerjack.models.protocols import (
10
+ FileSystemInterface,
11
+ GitInterface,
12
+ HookManager,
13
+ OptionsProtocol,
14
+ PublishManager,
15
+ TestManagerProtocol,
16
+ )
17
+ from crackerjack.services.config import ConfigurationService
18
+
19
+ from .session_coordinator import SessionCoordinator
20
+
21
+
22
+ class PhaseCoordinator:
23
+ def __init__(
24
+ self,
25
+ console: Console,
26
+ pkg_path: Path,
27
+ session: SessionCoordinator,
28
+ filesystem: FileSystemInterface,
29
+ git_service: GitInterface,
30
+ hook_manager: HookManager,
31
+ test_manager: TestManagerProtocol,
32
+ publish_manager: PublishManager,
33
+ ) -> None:
34
+ self.console = console
35
+ self.pkg_path = pkg_path
36
+ self.session = session
37
+
38
+ self.filesystem = filesystem
39
+ self.git_service = git_service
40
+ self.hook_manager = hook_manager
41
+ self.test_manager = test_manager
42
+ self.publish_manager = publish_manager
43
+
44
+ self.code_cleaner = CodeCleaner(console=console)
45
+ self.config_service = ConfigurationService(console=console, pkg_path=pkg_path)
46
+
47
+ self.logger = logging.getLogger("crackerjack.phases")
48
+
49
+ def run_cleaning_phase(self, options: OptionsProtocol) -> bool:
50
+ if not options.clean:
51
+ return True
52
+
53
+ self.session.track_task("cleaning", "Code cleaning")
54
+ try:
55
+ self._display_cleaning_header()
56
+ return self._execute_cleaning_process()
57
+ except Exception as e:
58
+ self.console.print(f"[red]❌[/red] Cleaning failed: {e}")
59
+ self.session.fail_task("cleaning", str(e))
60
+ return False
61
+
62
+ def _display_cleaning_header(self) -> None:
63
+ self.console.print("\n" + " - " * 80)
64
+ self.console.print(
65
+ "[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]",
66
+ )
67
+ self.console.print(" - " * 80 + "\n")
68
+ self.console.print("[yellow]🧹[/yellow] Starting code cleaning...")
69
+
70
+ def _execute_cleaning_process(self) -> bool:
71
+ python_files = list(self.pkg_path.rglob("*.py"))
72
+
73
+ if not python_files:
74
+ return self._handle_no_files_to_clean()
75
+
76
+ cleaned_files = self._clean_python_files(python_files)
77
+ self._report_cleaning_results(cleaned_files)
78
+ return True
79
+
80
+ def _handle_no_files_to_clean(self) -> bool:
81
+ self.console.print("[yellow]⚠️[/yellow] No Python files found to clean")
82
+ self.session.complete_task("cleaning", "No files to clean")
83
+ return True
84
+
85
+ def _clean_python_files(self, python_files: list[Path]) -> list[str]:
86
+ cleaned_files: list[str] = []
87
+ for file_path in python_files:
88
+ if self.code_cleaner.should_process_file(file_path):
89
+ if self.code_cleaner.clean_file(file_path):
90
+ cleaned_files.append(str(file_path))
91
+ return cleaned_files
92
+
93
+ def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
94
+ if cleaned_files:
95
+ self.console.print(f"[green]✅[/green] Cleaned {len(cleaned_files)} files")
96
+ self.session.complete_task(
97
+ "cleaning",
98
+ f"Cleaned {len(cleaned_files)} files",
99
+ )
100
+ else:
101
+ self.console.print("[green]✅[/green] No cleaning needed")
102
+ self.session.complete_task("cleaning", "No cleaning needed")
103
+
104
+ def run_configuration_phase(self, options: OptionsProtocol) -> bool:
105
+ if options.no_config_updates:
106
+ return True
107
+ self.session.track_task("configuration", "Configuration updates")
108
+ try:
109
+ success = True
110
+
111
+ # Check if we're running from the crackerjack project root
112
+ if self._is_crackerjack_project():
113
+ if not self._copy_config_files_to_package():
114
+ success = False
115
+
116
+ if not self.config_service.update_precommit_config(options):
117
+ success = False
118
+ if not self.config_service.update_pyproject_config(options):
119
+ success = False
120
+ self.session.complete_task(
121
+ "configuration",
122
+ "Configuration updated successfully"
123
+ if success
124
+ else "Some configuration updates failed",
125
+ )
126
+ return success
127
+ except Exception as e:
128
+ self.session.fail_task("configuration", str(e))
129
+ return False
130
+
131
+ def _is_crackerjack_project(self) -> bool:
132
+ """Check if we're running from the crackerjack project root."""
133
+ # Check for crackerjack-specific markers
134
+ pyproject_path = self.pkg_path / "pyproject.toml"
135
+ if not pyproject_path.exists():
136
+ return False
137
+
138
+ try:
139
+ import tomllib
140
+
141
+ with pyproject_path.open("rb") as f:
142
+ data = tomllib.load(f)
143
+
144
+ # Check if this is the crackerjack project
145
+ project_name = data.get("project", {}).get("name", "")
146
+ return project_name == "crackerjack"
147
+ except Exception:
148
+ return False
149
+
150
+ def _copy_config_files_to_package(self) -> bool:
151
+ """Copy configuration files from project root to package root."""
152
+ try:
153
+ # Files to copy from project root to package root
154
+ files_to_copy = [
155
+ "pyproject.toml",
156
+ ".pre-commit-config.yaml",
157
+ "CLAUDE.md",
158
+ "RULES.md",
159
+ ".gitignore",
160
+ "mcp.json",
161
+ "uv.lock",
162
+ ]
163
+
164
+ package_dir = self.pkg_path / "crackerjack"
165
+ if not package_dir.exists():
166
+ self.console.print(
167
+ "[yellow]⚠️[/yellow] Package directory not found: crackerjack/",
168
+ )
169
+ return False
170
+
171
+ copied_count = 0
172
+ for filename in files_to_copy:
173
+ src_path = self.pkg_path / filename
174
+ if src_path.exists():
175
+ dst_path = package_dir / filename
176
+ try:
177
+ import shutil
178
+
179
+ shutil.copy2(src_path, dst_path)
180
+ copied_count += 1
181
+ self.logger.debug(f"Copied {filename} to package directory")
182
+ except Exception as e:
183
+ self.console.print(
184
+ f"[yellow]⚠️[/yellow] Failed to copy {filename}: {e}",
185
+ )
186
+
187
+ if copied_count > 0:
188
+ self.console.print(
189
+ f"[green]✅[/green] Copied {copied_count} config files to package directory",
190
+ )
191
+
192
+ return True
193
+ except Exception as e:
194
+ self.console.print(
195
+ f"[red]❌[/red] Failed to copy config files to package: {e}",
196
+ )
197
+ return False
198
+
199
+ def run_hooks_phase(self, options: OptionsProtocol) -> bool:
200
+ if options.skip_hooks:
201
+ return True
202
+
203
+ temp_config = self.config_service.get_temp_config_path()
204
+ if temp_config:
205
+ self.hook_manager.set_config_path(temp_config)
206
+
207
+ if not self.run_fast_hooks_only(options):
208
+ return False
209
+
210
+ return self.run_comprehensive_hooks_only(options)
211
+
212
+ def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
213
+ if options.skip_hooks:
214
+ return True
215
+
216
+ return self._execute_hooks_with_retry(
217
+ "fast",
218
+ self.hook_manager.run_fast_hooks,
219
+ options,
220
+ )
221
+
222
+ def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
223
+ if options.skip_hooks:
224
+ return True
225
+
226
+ return self._execute_hooks_with_retry(
227
+ "comprehensive",
228
+ self.hook_manager.run_comprehensive_hooks,
229
+ options,
230
+ )
231
+
232
+ def run_testing_phase(self, options: OptionsProtocol) -> bool:
233
+ if not options.test:
234
+ return True
235
+ self.session.track_task("testing", "Test execution")
236
+ try:
237
+ self.console.print("\n" + "-" * 80)
238
+ self.console.print(
239
+ "[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
240
+ )
241
+ self.console.print("-" * 80 + "\n")
242
+ if not self.test_manager.validate_test_environment():
243
+ self.session.fail_task("testing", "Test environment validation failed")
244
+ return False
245
+ test_success = self.test_manager.run_tests(options)
246
+ if test_success:
247
+ coverage_info = self.test_manager.get_coverage()
248
+ self.session.complete_task(
249
+ "testing",
250
+ f"Tests passed, coverage: {coverage_info.get('total_coverage', 0):.1f}%",
251
+ )
252
+ else:
253
+ self.session.fail_task("testing", "Tests failed")
254
+
255
+ return test_success
256
+ except Exception as e:
257
+ self.console.print(f"Testing error: {e}")
258
+ self.session.fail_task("testing", str(e))
259
+ return False
260
+
261
+ def run_publishing_phase(self, options: OptionsProtocol) -> bool:
262
+ version_type = self._determine_version_type(options)
263
+ if not version_type:
264
+ return True
265
+
266
+ self.session.track_task("publishing", f"Publishing ({version_type})")
267
+ try:
268
+ return self._execute_publishing_workflow(options, version_type)
269
+ except Exception as e:
270
+ self.console.print(f"[red]❌[/red] Publishing failed: {e}")
271
+ self.session.fail_task("publishing", str(e))
272
+ return False
273
+
274
+ def _determine_version_type(self, options: OptionsProtocol) -> str | None:
275
+ if options.publish:
276
+ return options.publish
277
+ if options.all:
278
+ return options.all
279
+ if options.bump:
280
+ self._handle_version_bump_only(options.bump)
281
+ return None
282
+ return None
283
+
284
+ def _execute_publishing_workflow(
285
+ self,
286
+ options: OptionsProtocol,
287
+ version_type: str,
288
+ ) -> bool:
289
+ new_version = self.publish_manager.bump_version(version_type)
290
+
291
+ if not options.no_git_tags:
292
+ self.publish_manager.create_git_tag(new_version)
293
+
294
+ if self.publish_manager.publish_package():
295
+ self._handle_successful_publish(options, new_version)
296
+ return True
297
+ self.session.fail_task("publishing", "Package publishing failed")
298
+ return False
299
+
300
+ def _handle_successful_publish(
301
+ self,
302
+ options: OptionsProtocol,
303
+ new_version: str,
304
+ ) -> None:
305
+ self.console.print(f"[green]🚀[/green] Successfully published {new_version}!")
306
+
307
+ if options.cleanup_pypi:
308
+ self.publish_manager.cleanup_old_releases(options.keep_releases)
309
+
310
+ self.session.complete_task("publishing", f"Published {new_version}")
311
+
312
+ def run_commit_phase(self, options: OptionsProtocol) -> bool:
313
+ if not options.commit:
314
+ return True
315
+ self.session.track_task("commit", "Git commit and push")
316
+ try:
317
+ changed_files = self.git_service.get_changed_files()
318
+ if not changed_files:
319
+ return self._handle_no_changes_to_commit()
320
+ commit_message = self._get_commit_message(changed_files, options)
321
+ return self._execute_commit_and_push(changed_files, commit_message)
322
+ except Exception as e:
323
+ self.console.print(f"[red]❌[/red] Commit failed: {e}")
324
+ self.session.fail_task("commit", str(e))
325
+ return False
326
+
327
+ def _handle_no_changes_to_commit(self) -> bool:
328
+ self.console.print("[yellow]ℹ️[/yellow] No changes to commit")
329
+ self.session.complete_task("commit", "No changes to commit")
330
+ return True
331
+
332
+ def _execute_commit_and_push(
333
+ self,
334
+ changed_files: list[str],
335
+ commit_message: str,
336
+ ) -> bool:
337
+ if not self.git_service.add_files(changed_files):
338
+ self.session.fail_task("commit", "Failed to stage files")
339
+ return False
340
+
341
+ if not self.git_service.commit(commit_message):
342
+ self.session.fail_task("commit", "Commit failed")
343
+ return False
344
+
345
+ return self._handle_push_result(commit_message)
346
+
347
+ def _handle_push_result(self, commit_message: str) -> bool:
348
+ if self.git_service.push():
349
+ self.console.print(
350
+ f"[green]🎉[/green] Committed and pushed: {commit_message}",
351
+ )
352
+ self.session.complete_task(
353
+ "commit",
354
+ f"Committed and pushed: {commit_message}",
355
+ )
356
+ else:
357
+ self.console.print(
358
+ f"[yellow]⚠️[/yellow] Committed but push failed: {commit_message}",
359
+ )
360
+ self.session.complete_task(
361
+ "commit",
362
+ f"Committed (push failed): {commit_message}",
363
+ )
364
+ return True
365
+
366
+ def execute_hooks_with_retry(
367
+ self,
368
+ hook_type: str,
369
+ hook_runner: t.Callable[[], list[t.Any]],
370
+ options: OptionsProtocol,
371
+ ) -> bool:
372
+ return self._execute_hooks_with_retry(hook_type, hook_runner, options)
373
+
374
+ def _handle_version_bump_only(self, bump_type: str) -> bool:
375
+ self.session.track_task("version_bump", f"Version bump ({bump_type})")
376
+ try:
377
+ new_version = self.publish_manager.bump_version(bump_type)
378
+ self.console.print(f"[green]🎯[/green] Version bumped to {new_version}")
379
+ self.session.complete_task("version_bump", f"Bumped to {new_version}")
380
+ return True
381
+ except Exception as e:
382
+ self.console.print(f"[red]❌[/red] Version bump failed: {e}")
383
+ self.session.fail_task("version_bump", str(e))
384
+ return False
385
+
386
+ def _get_commit_message(
387
+ self,
388
+ changed_files: list[str],
389
+ options: OptionsProtocol,
390
+ ) -> str:
391
+ suggestions = self.git_service.get_commit_message_suggestions(changed_files)
392
+
393
+ if not suggestions:
394
+ return "Update project files"
395
+
396
+ if not options.interactive:
397
+ return suggestions[0]
398
+
399
+ return self._interactive_commit_message_selection(suggestions)
400
+
401
+ def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
402
+ self._display_commit_suggestions(suggestions)
403
+
404
+ try:
405
+ choice = self.console.input(
406
+ f"\nSelect message (1 - {len(suggestions)}) or enter custom: ",
407
+ )
408
+ return self._process_commit_choice(choice, suggestions)
409
+ except (KeyboardInterrupt, EOFError):
410
+ return suggestions[0]
411
+
412
+ def _display_commit_suggestions(self, suggestions: list[str]) -> None:
413
+ self.console.print("[cyan]📝[/cyan] Commit message suggestions: ")
414
+ for i, suggestion in enumerate(suggestions, 1):
415
+ self.console.print(f" {i}. {suggestion}")
416
+
417
+ def _process_commit_choice(self, choice: str, suggestions: list[str]) -> str:
418
+ if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
419
+ return suggestions[int(choice) - 1]
420
+ return choice or suggestions[0]
421
+
422
+ def _execute_hooks_with_retry(
423
+ self,
424
+ hook_type: str,
425
+ hook_runner: t.Callable[[], list[t.Any]],
426
+ options: OptionsProtocol,
427
+ ) -> bool:
428
+ self._initialize_hook_execution(hook_type)
429
+ max_retries = self._get_max_retries(hook_type)
430
+
431
+ for attempt in range(max_retries):
432
+ try:
433
+ results = hook_runner()
434
+ summary = self.hook_manager.get_hook_summary(results)
435
+
436
+ if self._has_hook_failures(summary):
437
+ if self._should_retry_hooks(
438
+ hook_type,
439
+ attempt,
440
+ max_retries,
441
+ results,
442
+ ):
443
+ continue
444
+
445
+ return self._handle_hook_failures(
446
+ hook_type,
447
+ options,
448
+ summary,
449
+ results,
450
+ attempt,
451
+ max_retries,
452
+ )
453
+ return self._handle_hook_success(hook_type, summary)
454
+
455
+ except Exception as e:
456
+ return self._handle_hook_exception(hook_type, e)
457
+
458
+ return False
459
+
460
+ def _initialize_hook_execution(self, hook_type: str) -> None:
461
+ self.logger.info(f"Starting {hook_type} hooks execution")
462
+ self.session.track_task(
463
+ f"{hook_type}_hooks",
464
+ f"{hook_type.title()} hooks execution",
465
+ )
466
+
467
+ def _get_max_retries(self, hook_type: str) -> int:
468
+ return 2 if hook_type == "fast" else 1
469
+
470
+ def _has_hook_failures(self, summary: dict[str, t.Any]) -> bool:
471
+ return summary["failed"] > 0 or summary["errors"] > 0
472
+
473
+ def _should_retry_hooks(
474
+ self,
475
+ hook_type: str,
476
+ attempt: int,
477
+ max_retries: int,
478
+ results: list[t.Any],
479
+ ) -> bool:
480
+ if hook_type == "fast" and attempt < max_retries - 1:
481
+ if self._should_retry_fast_hooks(results):
482
+ self.console.print(
483
+ "[yellow]🔄[/yellow] Fast hooks modified files, retrying all fast hooks...",
484
+ )
485
+ return True
486
+ return False
487
+
488
+ def _handle_hook_failures(
489
+ self,
490
+ hook_type: str,
491
+ options: OptionsProtocol,
492
+ summary: dict[str, t.Any],
493
+ results: list[t.Any],
494
+ attempt: int,
495
+ max_retries: int,
496
+ ) -> bool:
497
+ self.logger.warning(
498
+ f"{hook_type} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
499
+ )
500
+
501
+ self.console.print(
502
+ f"[red]❌[/red] {hook_type.title()} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
503
+ )
504
+ self.session.fail_task(
505
+ f"{hook_type}_hooks",
506
+ f"{summary['failed']} failed, {summary['errors']} errors",
507
+ )
508
+ return False
509
+
510
+ def _should_retry_fast_hooks(self, results: list[t.Any]) -> bool:
511
+ formatting_hooks = {
512
+ "ruff-format",
513
+ "ruff-check",
514
+ "trailing-whitespace",
515
+ "end-of-file-fixer",
516
+ }
517
+
518
+ for result in results:
519
+ hook_id = getattr(result, "hook_id", "") or getattr(result, "name", "")
520
+ if (
521
+ hook_id in formatting_hooks
522
+ and hasattr(result, "failed")
523
+ and result.failed
524
+ ):
525
+ output = getattr(result, "output", "") or getattr(result, "stdout", "")
526
+ if any(
527
+ phrase in output.lower()
528
+ for phrase in (
529
+ "files were modified",
530
+ "fixed",
531
+ "reformatted",
532
+ "fixing",
533
+ )
534
+ ):
535
+ return True
536
+ return False
537
+
538
+ def _apply_retry_backoff(self, attempt: int) -> None:
539
+ if attempt > 0:
540
+ backoff_delay = 2 ** (attempt - 1)
541
+ self.logger.debug(f"Applying exponential backoff: {backoff_delay}s")
542
+ self.console.print(f"[dim]Waiting {backoff_delay}s before retry...[/dim]")
543
+ time.sleep(backoff_delay)
544
+
545
+ def _handle_hook_success(self, hook_type: str, summary: dict[str, t.Any]) -> bool:
546
+ self.logger.info(
547
+ f"{hook_type} hooks passed: {summary['passed']} / {summary['total']}",
548
+ )
549
+ self.console.print(
550
+ f"[green]✅[/green] {hook_type.title()} hooks passed: {summary['passed']} / {summary['total']}",
551
+ )
552
+ self.session.complete_task(
553
+ f"{hook_type}_hooks",
554
+ f"{summary['passed']} / {summary['total']} passed",
555
+ )
556
+ return True
557
+
558
+ def _handle_hook_exception(self, hook_type: str, e: Exception) -> bool:
559
+ self.console.print(f"[red]❌[/red] {hook_type.title()} hooks error: {e}")
560
+ self.session.fail_task(f"{hook_type}_hooks", str(e))
561
+ return False