crackerjack 0.37.9__py3-none-any.whl → 0.45.2__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.
Files changed (425) hide show
  1. crackerjack/README.md +19 -0
  2. crackerjack/__init__.py +30 -1
  3. crackerjack/__main__.py +342 -1263
  4. crackerjack/adapters/README.md +18 -0
  5. crackerjack/adapters/__init__.py +27 -5
  6. crackerjack/adapters/_output_paths.py +167 -0
  7. crackerjack/adapters/_qa_adapter_base.py +309 -0
  8. crackerjack/adapters/_tool_adapter_base.py +706 -0
  9. crackerjack/adapters/ai/README.md +65 -0
  10. crackerjack/adapters/ai/__init__.py +5 -0
  11. crackerjack/adapters/ai/claude.py +853 -0
  12. crackerjack/adapters/complexity/README.md +53 -0
  13. crackerjack/adapters/complexity/__init__.py +10 -0
  14. crackerjack/adapters/complexity/complexipy.py +641 -0
  15. crackerjack/adapters/dependency/__init__.py +22 -0
  16. crackerjack/adapters/dependency/pip_audit.py +418 -0
  17. crackerjack/adapters/format/README.md +72 -0
  18. crackerjack/adapters/format/__init__.py +11 -0
  19. crackerjack/adapters/format/mdformat.py +313 -0
  20. crackerjack/adapters/format/ruff.py +516 -0
  21. crackerjack/adapters/lint/README.md +47 -0
  22. crackerjack/adapters/lint/__init__.py +11 -0
  23. crackerjack/adapters/lint/codespell.py +273 -0
  24. crackerjack/adapters/lsp/README.md +49 -0
  25. crackerjack/adapters/lsp/__init__.py +27 -0
  26. crackerjack/adapters/{rust_tool_manager.py → lsp/_manager.py} +3 -3
  27. crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
  28. crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
  29. crackerjack/adapters/refactor/README.md +59 -0
  30. crackerjack/adapters/refactor/__init__.py +12 -0
  31. crackerjack/adapters/refactor/creosote.py +318 -0
  32. crackerjack/adapters/refactor/refurb.py +406 -0
  33. crackerjack/adapters/refactor/skylos.py +494 -0
  34. crackerjack/adapters/sast/README.md +132 -0
  35. crackerjack/adapters/sast/__init__.py +32 -0
  36. crackerjack/adapters/sast/_base.py +201 -0
  37. crackerjack/adapters/sast/bandit.py +423 -0
  38. crackerjack/adapters/sast/pyscn.py +405 -0
  39. crackerjack/adapters/sast/semgrep.py +241 -0
  40. crackerjack/adapters/security/README.md +111 -0
  41. crackerjack/adapters/security/__init__.py +17 -0
  42. crackerjack/adapters/security/gitleaks.py +339 -0
  43. crackerjack/adapters/type/README.md +52 -0
  44. crackerjack/adapters/type/__init__.py +12 -0
  45. crackerjack/adapters/type/pyrefly.py +402 -0
  46. crackerjack/adapters/type/ty.py +402 -0
  47. crackerjack/adapters/type/zuban.py +522 -0
  48. crackerjack/adapters/utility/README.md +51 -0
  49. crackerjack/adapters/utility/__init__.py +10 -0
  50. crackerjack/adapters/utility/checks.py +884 -0
  51. crackerjack/agents/README.md +264 -0
  52. crackerjack/agents/__init__.py +40 -12
  53. crackerjack/agents/base.py +1 -0
  54. crackerjack/agents/claude_code_bridge.py +641 -0
  55. crackerjack/agents/coordinator.py +49 -53
  56. crackerjack/agents/dry_agent.py +187 -3
  57. crackerjack/agents/enhanced_coordinator.py +279 -0
  58. crackerjack/agents/enhanced_proactive_agent.py +185 -0
  59. crackerjack/agents/error_middleware.py +53 -0
  60. crackerjack/agents/formatting_agent.py +6 -8
  61. crackerjack/agents/helpers/__init__.py +9 -0
  62. crackerjack/agents/helpers/performance/__init__.py +22 -0
  63. crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
  64. crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
  65. crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
  66. crackerjack/agents/helpers/refactoring/__init__.py +22 -0
  67. crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
  68. crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
  69. crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
  70. crackerjack/agents/helpers/test_creation/__init__.py +19 -0
  71. crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
  72. crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
  73. crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
  74. crackerjack/agents/performance_agent.py +121 -1152
  75. crackerjack/agents/refactoring_agent.py +156 -655
  76. crackerjack/agents/semantic_agent.py +479 -0
  77. crackerjack/agents/semantic_helpers.py +356 -0
  78. crackerjack/agents/test_creation_agent.py +19 -1605
  79. crackerjack/api.py +5 -7
  80. crackerjack/cli/README.md +394 -0
  81. crackerjack/cli/__init__.py +1 -1
  82. crackerjack/cli/cache_handlers.py +23 -18
  83. crackerjack/cli/cache_handlers_enhanced.py +1 -4
  84. crackerjack/cli/facade.py +70 -8
  85. crackerjack/cli/formatting.py +13 -0
  86. crackerjack/cli/handlers/__init__.py +85 -0
  87. crackerjack/cli/handlers/advanced.py +103 -0
  88. crackerjack/cli/handlers/ai_features.py +62 -0
  89. crackerjack/cli/handlers/analytics.py +479 -0
  90. crackerjack/cli/handlers/changelog.py +271 -0
  91. crackerjack/cli/handlers/config_handlers.py +16 -0
  92. crackerjack/cli/handlers/coverage.py +84 -0
  93. crackerjack/cli/handlers/documentation.py +280 -0
  94. crackerjack/cli/handlers/main_handlers.py +497 -0
  95. crackerjack/cli/handlers/monitoring.py +371 -0
  96. crackerjack/cli/handlers.py +249 -49
  97. crackerjack/cli/interactive.py +8 -5
  98. crackerjack/cli/options.py +203 -110
  99. crackerjack/cli/semantic_handlers.py +292 -0
  100. crackerjack/cli/version.py +19 -0
  101. crackerjack/code_cleaner.py +60 -24
  102. crackerjack/config/README.md +472 -0
  103. crackerjack/config/__init__.py +256 -0
  104. crackerjack/config/global_lock_config.py +191 -54
  105. crackerjack/config/hooks.py +188 -16
  106. crackerjack/config/loader.py +239 -0
  107. crackerjack/config/settings.py +141 -0
  108. crackerjack/config/tool_commands.py +331 -0
  109. crackerjack/core/README.md +393 -0
  110. crackerjack/core/async_workflow_orchestrator.py +79 -53
  111. crackerjack/core/autofix_coordinator.py +22 -9
  112. crackerjack/core/container.py +10 -9
  113. crackerjack/core/enhanced_container.py +9 -9
  114. crackerjack/core/performance.py +1 -1
  115. crackerjack/core/performance_monitor.py +5 -3
  116. crackerjack/core/phase_coordinator.py +1018 -634
  117. crackerjack/core/proactive_workflow.py +3 -3
  118. crackerjack/core/retry.py +275 -0
  119. crackerjack/core/service_watchdog.py +167 -23
  120. crackerjack/core/session_coordinator.py +187 -382
  121. crackerjack/core/timeout_manager.py +161 -44
  122. crackerjack/core/workflow/__init__.py +21 -0
  123. crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
  124. crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
  125. crackerjack/core/workflow/workflow_issue_parser.py +714 -0
  126. crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
  127. crackerjack/core/workflow/workflow_security_gates.py +400 -0
  128. crackerjack/core/workflow_orchestrator.py +1247 -953
  129. crackerjack/data/README.md +11 -0
  130. crackerjack/data/__init__.py +8 -0
  131. crackerjack/data/models.py +79 -0
  132. crackerjack/data/repository.py +210 -0
  133. crackerjack/decorators/README.md +180 -0
  134. crackerjack/decorators/__init__.py +35 -0
  135. crackerjack/decorators/error_handling.py +649 -0
  136. crackerjack/decorators/error_handling_decorators.py +334 -0
  137. crackerjack/decorators/helpers.py +58 -0
  138. crackerjack/decorators/patterns.py +281 -0
  139. crackerjack/decorators/utils.py +58 -0
  140. crackerjack/docs/README.md +11 -0
  141. crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
  142. crackerjack/documentation/README.md +11 -0
  143. crackerjack/documentation/ai_templates.py +1 -1
  144. crackerjack/documentation/dual_output_generator.py +11 -9
  145. crackerjack/documentation/reference_generator.py +104 -59
  146. crackerjack/dynamic_config.py +52 -61
  147. crackerjack/errors.py +1 -1
  148. crackerjack/events/README.md +11 -0
  149. crackerjack/events/__init__.py +16 -0
  150. crackerjack/events/telemetry.py +175 -0
  151. crackerjack/events/workflow_bus.py +346 -0
  152. crackerjack/exceptions/README.md +301 -0
  153. crackerjack/exceptions/__init__.py +5 -0
  154. crackerjack/exceptions/config.py +4 -0
  155. crackerjack/exceptions/tool_execution_error.py +245 -0
  156. crackerjack/executors/README.md +591 -0
  157. crackerjack/executors/__init__.py +2 -0
  158. crackerjack/executors/async_hook_executor.py +539 -77
  159. crackerjack/executors/cached_hook_executor.py +3 -3
  160. crackerjack/executors/hook_executor.py +967 -102
  161. crackerjack/executors/hook_lock_manager.py +31 -22
  162. crackerjack/executors/individual_hook_executor.py +66 -32
  163. crackerjack/executors/lsp_aware_hook_executor.py +136 -57
  164. crackerjack/executors/progress_hook_executor.py +282 -0
  165. crackerjack/executors/tool_proxy.py +23 -7
  166. crackerjack/hooks/README.md +485 -0
  167. crackerjack/hooks/lsp_hook.py +8 -9
  168. crackerjack/intelligence/README.md +557 -0
  169. crackerjack/interactive.py +37 -10
  170. crackerjack/managers/README.md +369 -0
  171. crackerjack/managers/async_hook_manager.py +41 -57
  172. crackerjack/managers/hook_manager.py +449 -79
  173. crackerjack/managers/publish_manager.py +81 -36
  174. crackerjack/managers/test_command_builder.py +290 -12
  175. crackerjack/managers/test_executor.py +93 -8
  176. crackerjack/managers/test_manager.py +1082 -75
  177. crackerjack/managers/test_progress.py +118 -26
  178. crackerjack/mcp/README.md +374 -0
  179. crackerjack/mcp/cache.py +25 -2
  180. crackerjack/mcp/client_runner.py +35 -18
  181. crackerjack/mcp/context.py +9 -9
  182. crackerjack/mcp/dashboard.py +24 -8
  183. crackerjack/mcp/enhanced_progress_monitor.py +34 -23
  184. crackerjack/mcp/file_monitor.py +27 -6
  185. crackerjack/mcp/progress_components.py +45 -34
  186. crackerjack/mcp/progress_monitor.py +6 -9
  187. crackerjack/mcp/rate_limiter.py +11 -7
  188. crackerjack/mcp/server.py +2 -0
  189. crackerjack/mcp/server_core.py +187 -55
  190. crackerjack/mcp/service_watchdog.py +12 -9
  191. crackerjack/mcp/task_manager.py +2 -2
  192. crackerjack/mcp/tools/README.md +27 -0
  193. crackerjack/mcp/tools/__init__.py +2 -0
  194. crackerjack/mcp/tools/core_tools.py +75 -52
  195. crackerjack/mcp/tools/execution_tools.py +87 -31
  196. crackerjack/mcp/tools/intelligence_tools.py +2 -2
  197. crackerjack/mcp/tools/proactive_tools.py +1 -1
  198. crackerjack/mcp/tools/semantic_tools.py +584 -0
  199. crackerjack/mcp/tools/utility_tools.py +180 -132
  200. crackerjack/mcp/tools/workflow_executor.py +87 -46
  201. crackerjack/mcp/websocket/README.md +31 -0
  202. crackerjack/mcp/websocket/app.py +11 -1
  203. crackerjack/mcp/websocket/event_bridge.py +188 -0
  204. crackerjack/mcp/websocket/jobs.py +27 -4
  205. crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
  206. crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
  207. crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
  208. crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
  209. crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
  210. crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
  211. crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
  212. crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
  213. crackerjack/mcp/websocket/monitoring/factory.py +109 -0
  214. crackerjack/mcp/websocket/monitoring/filters.py +10 -0
  215. crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
  216. crackerjack/mcp/websocket/monitoring/models.py +90 -0
  217. crackerjack/mcp/websocket/monitoring/utils.py +171 -0
  218. crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
  219. crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
  220. crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
  221. crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
  222. crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
  223. crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
  224. crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
  225. crackerjack/mcp/websocket/server.py +1 -3
  226. crackerjack/mcp/websocket/websocket_handler.py +107 -6
  227. crackerjack/models/README.md +308 -0
  228. crackerjack/models/__init__.py +10 -1
  229. crackerjack/models/config.py +639 -22
  230. crackerjack/models/config_adapter.py +6 -6
  231. crackerjack/models/protocols.py +1167 -23
  232. crackerjack/models/pydantic_models.py +320 -0
  233. crackerjack/models/qa_config.py +145 -0
  234. crackerjack/models/qa_results.py +134 -0
  235. crackerjack/models/results.py +35 -0
  236. crackerjack/models/semantic_models.py +258 -0
  237. crackerjack/models/task.py +19 -3
  238. crackerjack/models/test_models.py +60 -0
  239. crackerjack/monitoring/README.md +11 -0
  240. crackerjack/monitoring/ai_agent_watchdog.py +5 -4
  241. crackerjack/monitoring/metrics_collector.py +4 -3
  242. crackerjack/monitoring/regression_prevention.py +4 -3
  243. crackerjack/monitoring/websocket_server.py +4 -241
  244. crackerjack/orchestration/README.md +340 -0
  245. crackerjack/orchestration/__init__.py +43 -0
  246. crackerjack/orchestration/advanced_orchestrator.py +20 -67
  247. crackerjack/orchestration/cache/README.md +312 -0
  248. crackerjack/orchestration/cache/__init__.py +37 -0
  249. crackerjack/orchestration/cache/memory_cache.py +338 -0
  250. crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
  251. crackerjack/orchestration/config.py +297 -0
  252. crackerjack/orchestration/coverage_improvement.py +13 -6
  253. crackerjack/orchestration/execution_strategies.py +6 -6
  254. crackerjack/orchestration/hook_orchestrator.py +1398 -0
  255. crackerjack/orchestration/strategies/README.md +401 -0
  256. crackerjack/orchestration/strategies/__init__.py +39 -0
  257. crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
  258. crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
  259. crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
  260. crackerjack/orchestration/test_progress_streamer.py +1 -1
  261. crackerjack/plugins/README.md +11 -0
  262. crackerjack/plugins/hooks.py +3 -2
  263. crackerjack/plugins/loader.py +3 -3
  264. crackerjack/plugins/managers.py +1 -1
  265. crackerjack/py313.py +191 -0
  266. crackerjack/security/README.md +11 -0
  267. crackerjack/services/README.md +374 -0
  268. crackerjack/services/__init__.py +8 -21
  269. crackerjack/services/ai/README.md +295 -0
  270. crackerjack/services/ai/__init__.py +7 -0
  271. crackerjack/services/ai/advanced_optimizer.py +878 -0
  272. crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
  273. crackerjack/services/ai/embeddings.py +444 -0
  274. crackerjack/services/ai/intelligent_commit.py +328 -0
  275. crackerjack/services/ai/predictive_analytics.py +510 -0
  276. crackerjack/services/api_extractor.py +5 -3
  277. crackerjack/services/bounded_status_operations.py +45 -5
  278. crackerjack/services/cache.py +249 -318
  279. crackerjack/services/changelog_automation.py +7 -3
  280. crackerjack/services/command_execution_service.py +305 -0
  281. crackerjack/services/config_integrity.py +83 -39
  282. crackerjack/services/config_merge.py +9 -6
  283. crackerjack/services/config_service.py +198 -0
  284. crackerjack/services/config_template.py +13 -26
  285. crackerjack/services/coverage_badge_service.py +6 -4
  286. crackerjack/services/coverage_ratchet.py +53 -27
  287. crackerjack/services/debug.py +18 -7
  288. crackerjack/services/dependency_analyzer.py +4 -4
  289. crackerjack/services/dependency_monitor.py +13 -13
  290. crackerjack/services/documentation_generator.py +4 -2
  291. crackerjack/services/documentation_service.py +62 -33
  292. crackerjack/services/enhanced_filesystem.py +81 -27
  293. crackerjack/services/enterprise_optimizer.py +1 -1
  294. crackerjack/services/error_pattern_analyzer.py +10 -10
  295. crackerjack/services/file_filter.py +221 -0
  296. crackerjack/services/file_hasher.py +5 -7
  297. crackerjack/services/file_io_service.py +361 -0
  298. crackerjack/services/file_modifier.py +615 -0
  299. crackerjack/services/filesystem.py +80 -109
  300. crackerjack/services/git.py +99 -5
  301. crackerjack/services/health_metrics.py +4 -6
  302. crackerjack/services/heatmap_generator.py +12 -3
  303. crackerjack/services/incremental_executor.py +380 -0
  304. crackerjack/services/initialization.py +101 -49
  305. crackerjack/services/log_manager.py +2 -2
  306. crackerjack/services/logging.py +120 -68
  307. crackerjack/services/lsp_client.py +12 -12
  308. crackerjack/services/memory_optimizer.py +27 -22
  309. crackerjack/services/monitoring/README.md +30 -0
  310. crackerjack/services/monitoring/__init__.py +9 -0
  311. crackerjack/services/monitoring/dependency_monitor.py +678 -0
  312. crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
  313. crackerjack/services/monitoring/health_metrics.py +716 -0
  314. crackerjack/services/monitoring/metrics.py +587 -0
  315. crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
  316. crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
  317. crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
  318. crackerjack/services/parallel_executor.py +166 -55
  319. crackerjack/services/patterns/__init__.py +142 -0
  320. crackerjack/services/patterns/agents.py +107 -0
  321. crackerjack/services/patterns/code/__init__.py +15 -0
  322. crackerjack/services/patterns/code/detection.py +118 -0
  323. crackerjack/services/patterns/code/imports.py +107 -0
  324. crackerjack/services/patterns/code/paths.py +159 -0
  325. crackerjack/services/patterns/code/performance.py +119 -0
  326. crackerjack/services/patterns/code/replacement.py +36 -0
  327. crackerjack/services/patterns/core.py +212 -0
  328. crackerjack/services/patterns/documentation/__init__.py +14 -0
  329. crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
  330. crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
  331. crackerjack/services/patterns/documentation/docstrings.py +89 -0
  332. crackerjack/services/patterns/formatting.py +226 -0
  333. crackerjack/services/patterns/operations.py +339 -0
  334. crackerjack/services/patterns/security/__init__.py +23 -0
  335. crackerjack/services/patterns/security/code_injection.py +122 -0
  336. crackerjack/services/patterns/security/credentials.py +190 -0
  337. crackerjack/services/patterns/security/path_traversal.py +221 -0
  338. crackerjack/services/patterns/security/unsafe_operations.py +216 -0
  339. crackerjack/services/patterns/templates.py +62 -0
  340. crackerjack/services/patterns/testing/__init__.py +18 -0
  341. crackerjack/services/patterns/testing/error_patterns.py +107 -0
  342. crackerjack/services/patterns/testing/pytest_output.py +126 -0
  343. crackerjack/services/patterns/tool_output/__init__.py +16 -0
  344. crackerjack/services/patterns/tool_output/bandit.py +72 -0
  345. crackerjack/services/patterns/tool_output/other.py +97 -0
  346. crackerjack/services/patterns/tool_output/pyright.py +67 -0
  347. crackerjack/services/patterns/tool_output/ruff.py +44 -0
  348. crackerjack/services/patterns/url_sanitization.py +114 -0
  349. crackerjack/services/patterns/utilities.py +42 -0
  350. crackerjack/services/patterns/utils.py +339 -0
  351. crackerjack/services/patterns/validation.py +46 -0
  352. crackerjack/services/patterns/versioning.py +62 -0
  353. crackerjack/services/predictive_analytics.py +21 -8
  354. crackerjack/services/profiler.py +280 -0
  355. crackerjack/services/quality/README.md +415 -0
  356. crackerjack/services/quality/__init__.py +11 -0
  357. crackerjack/services/quality/anomaly_detector.py +392 -0
  358. crackerjack/services/quality/pattern_cache.py +333 -0
  359. crackerjack/services/quality/pattern_detector.py +479 -0
  360. crackerjack/services/quality/qa_orchestrator.py +491 -0
  361. crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
  362. crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
  363. crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
  364. crackerjack/services/regex_patterns.py +58 -2987
  365. crackerjack/services/regex_utils.py +55 -29
  366. crackerjack/services/secure_status_formatter.py +42 -15
  367. crackerjack/services/secure_subprocess.py +35 -2
  368. crackerjack/services/security.py +16 -8
  369. crackerjack/services/server_manager.py +40 -51
  370. crackerjack/services/smart_scheduling.py +46 -6
  371. crackerjack/services/status_authentication.py +3 -3
  372. crackerjack/services/thread_safe_status_collector.py +1 -0
  373. crackerjack/services/tool_filter.py +368 -0
  374. crackerjack/services/tool_version_service.py +9 -5
  375. crackerjack/services/unified_config.py +43 -351
  376. crackerjack/services/vector_store.py +689 -0
  377. crackerjack/services/version_analyzer.py +6 -4
  378. crackerjack/services/version_checker.py +14 -8
  379. crackerjack/services/zuban_lsp_service.py +5 -4
  380. crackerjack/slash_commands/README.md +11 -0
  381. crackerjack/slash_commands/init.md +2 -12
  382. crackerjack/slash_commands/run.md +84 -50
  383. crackerjack/tools/README.md +11 -0
  384. crackerjack/tools/__init__.py +30 -0
  385. crackerjack/tools/_git_utils.py +105 -0
  386. crackerjack/tools/check_added_large_files.py +139 -0
  387. crackerjack/tools/check_ast.py +105 -0
  388. crackerjack/tools/check_json.py +103 -0
  389. crackerjack/tools/check_jsonschema.py +297 -0
  390. crackerjack/tools/check_toml.py +103 -0
  391. crackerjack/tools/check_yaml.py +110 -0
  392. crackerjack/tools/codespell_wrapper.py +72 -0
  393. crackerjack/tools/end_of_file_fixer.py +202 -0
  394. crackerjack/tools/format_json.py +128 -0
  395. crackerjack/tools/mdformat_wrapper.py +114 -0
  396. crackerjack/tools/trailing_whitespace.py +198 -0
  397. crackerjack/tools/validate_regex_patterns.py +7 -3
  398. crackerjack/ui/README.md +11 -0
  399. crackerjack/ui/dashboard_renderer.py +28 -0
  400. crackerjack/ui/templates/README.md +11 -0
  401. crackerjack/utils/console_utils.py +13 -0
  402. crackerjack/utils/dependency_guard.py +230 -0
  403. crackerjack/utils/retry_utils.py +275 -0
  404. crackerjack/workflows/README.md +590 -0
  405. crackerjack/workflows/__init__.py +46 -0
  406. crackerjack/workflows/actions.py +811 -0
  407. crackerjack/workflows/auto_fix.py +444 -0
  408. crackerjack/workflows/container_builder.py +499 -0
  409. crackerjack/workflows/definitions.py +443 -0
  410. crackerjack/workflows/engine.py +177 -0
  411. crackerjack/workflows/event_bridge.py +242 -0
  412. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
  413. crackerjack-0.45.2.dist-info/RECORD +478 -0
  414. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
  415. crackerjack/managers/test_manager_backup.py +0 -1075
  416. crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
  417. crackerjack/mixins/__init__.py +0 -3
  418. crackerjack/mixins/error_handling.py +0 -145
  419. crackerjack/services/config.py +0 -358
  420. crackerjack/ui/server_panels.py +0 -125
  421. crackerjack-0.37.9.dist-info/RECORD +0 -231
  422. /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
  423. /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
  424. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
  425. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,52 +1,80 @@
1
- import logging
2
- import time
1
+ from __future__ import annotations
2
+
3
+ import re
3
4
  import typing as t
4
5
  from pathlib import Path
5
6
 
6
- from rich.console import Console
7
+ from acb.console import Console
8
+ from acb.depends import Inject, depends
9
+ from acb.logger import Logger
10
+ from rich import box
11
+ from rich.panel import Panel
12
+ from rich.progress import (
13
+ BarColumn,
14
+ MofNCompleteColumn,
15
+ Progress,
16
+ SpinnerColumn,
17
+ TextColumn,
18
+ TimeElapsedColumn,
19
+ )
20
+ from rich.table import Table
7
21
 
8
- from crackerjack.code_cleaner import CodeCleaner, PackageCleaningResult
22
+ from crackerjack.cli.formatting import separator as make_separator
23
+ from crackerjack.code_cleaner import CodeCleaner
24
+ from crackerjack.config import get_console_width
9
25
  from crackerjack.core.autofix_coordinator import AutofixCoordinator
10
- from crackerjack.mixins import ErrorHandlingMixin
26
+ from crackerjack.core.session_coordinator import SessionCoordinator
27
+ from crackerjack.decorators import handle_errors
11
28
  from crackerjack.models.protocols import (
12
29
  ConfigMergeServiceProtocol,
13
30
  FileSystemInterface,
14
31
  GitInterface,
15
32
  HookManager,
33
+ MemoryOptimizerProtocol,
16
34
  OptionsProtocol,
17
35
  PublishManager,
18
36
  TestManagerProtocol,
19
37
  )
20
- from crackerjack.services.memory_optimizer import (
21
- create_lazy_service,
22
- get_memory_optimizer,
38
+ from crackerjack.models.task import HookResult
39
+ from crackerjack.services.memory_optimizer import create_lazy_service
40
+ from crackerjack.services.monitoring.performance_cache import (
41
+ FileSystemCache,
42
+ GitOperationCache,
23
43
  )
24
44
  from crackerjack.services.parallel_executor import (
25
- get_async_executor,
26
- get_parallel_executor,
45
+ AsyncCommandExecutor,
46
+ ParallelHookExecutor,
27
47
  )
28
- from crackerjack.services.performance_cache import get_filesystem_cache, get_git_cache
29
48
 
30
- from .session_coordinator import SessionCoordinator
49
+ if t.TYPE_CHECKING:
50
+ pass # All imports moved to top-level for runtime availability
31
51
 
32
52
 
33
- class PhaseCoordinator(ErrorHandlingMixin):
53
+ class PhaseCoordinator:
54
+ @depends.inject
34
55
  def __init__(
35
56
  self,
36
- console: Console,
37
- pkg_path: Path,
38
- session: SessionCoordinator,
39
- filesystem: FileSystemInterface,
40
- git_service: GitInterface,
41
- hook_manager: HookManager,
42
- test_manager: TestManagerProtocol,
43
- publish_manager: PublishManager,
44
- config_merge_service: ConfigMergeServiceProtocol,
57
+ console: Inject[Console],
58
+ logger: Inject[Logger],
59
+ memory_optimizer: Inject[MemoryOptimizerProtocol],
60
+ parallel_executor: Inject[ParallelHookExecutor],
61
+ async_executor: Inject[AsyncCommandExecutor],
62
+ git_cache: Inject[GitOperationCache],
63
+ filesystem_cache: Inject[FileSystemCache],
64
+ pkg_path: Inject[Path],
65
+ session: Inject[SessionCoordinator],
66
+ filesystem: Inject[FileSystemInterface],
67
+ git_service: Inject[GitInterface],
68
+ hook_manager: Inject[HookManager],
69
+ test_manager: Inject[TestManagerProtocol],
70
+ publish_manager: Inject[PublishManager],
71
+ config_merge_service: Inject[ConfigMergeServiceProtocol],
45
72
  ) -> None:
46
73
  self.console = console
47
74
  self.pkg_path = pkg_path
48
75
  self.session = session
49
76
 
77
+ # Dependencies injected via ACB's depends.get() from WorkflowOrchestrator
50
78
  self.filesystem = filesystem
51
79
  self.git_service = git_service
52
80
  self.hook_manager = hook_manager
@@ -65,779 +93,1135 @@ class PhaseCoordinator(ErrorHandlingMixin):
65
93
  backup_service=None,
66
94
  )
67
95
 
68
- from crackerjack.services.config import ConfigurationService
96
+ # Ensure logger is a proper instance, not an empty tuple, string, or other invalid value
97
+ if isinstance(logger, tuple) and len(logger) == 0:
98
+ # Log this issue for debugging
99
+ print(
100
+ "WARNING: PhaseCoordinator received empty tuple for logger dependency, creating fallback"
101
+ )
102
+ # Import and create a fallback logger if we got an empty tuple
103
+ from acb.logger import Logger as ACBLogger
104
+
105
+ self._logger = ACBLogger()
106
+ elif isinstance(logger, str):
107
+ # Log this issue for debugging
108
+ print(
109
+ f"WARNING: PhaseCoordinator received string for logger dependency: {logger!r}, creating fallback"
110
+ )
111
+ # Import and create a fallback logger if we got a string
112
+ from acb.logger import Logger as ACBLogger
69
113
 
70
- self.config_service = ConfigurationService(console=console, pkg_path=pkg_path)
114
+ self._logger = ACBLogger()
115
+ else:
116
+ self._logger = logger
71
117
 
72
- self.logger = logging.getLogger("crackerjack.phases")
118
+ # Services injected via ACB DI
119
+ self._memory_optimizer = memory_optimizer
120
+ self._parallel_executor = parallel_executor
121
+ self._async_executor = async_executor
122
+ self._git_cache = git_cache
123
+ self._filesystem_cache = filesystem_cache
73
124
 
74
- self._memory_optimizer = get_memory_optimizer()
75
- self._parallel_executor = get_parallel_executor()
76
- self._async_executor = get_async_executor()
77
- self._git_cache = get_git_cache()
78
- self._filesystem_cache = get_filesystem_cache()
125
+ self._last_hook_summary: dict[str, t.Any] | None = None
126
+ self._last_hook_results: list[HookResult] = []
79
127
 
80
128
  self._lazy_autofix = create_lazy_service(
81
- lambda: AutofixCoordinator(console=console, pkg_path=pkg_path),
129
+ lambda: AutofixCoordinator(pkg_path=pkg_path),
82
130
  "autofix_coordinator",
83
131
  )
132
+ self.console.print()
133
+
134
+ # Track if fast hooks have already started in this session to prevent duplicates
135
+ self._fast_hooks_started: bool = False
84
136
 
85
- super().__init__()
137
+ @property
138
+ def logger(self) -> Logger:
139
+ """Safely access the logger instance, ensuring it's not an empty tuple or string."""
140
+ if hasattr(self, "_logger") and (
141
+ (isinstance(self._logger, tuple) and len(self._logger) == 0)
142
+ or isinstance(self._logger, str)
143
+ ):
144
+ from acb.logger import Logger as ACBLogger
145
+
146
+ print(
147
+ f"WARNING: PhaseCoordinator logger was invalid type ({type(self._logger).__name__}: {self._logger!r}), creating fresh logger instance"
148
+ )
149
+ self._logger = ACBLogger()
150
+ return self._logger
151
+
152
+ @logger.setter
153
+ def logger(self, value: Logger) -> None:
154
+ """Set the logger instance."""
155
+ self._logger = value
156
+
157
+ # --- Output/formatting helpers -------------------------------------------------
158
+ @staticmethod
159
+ def _strip_ansi(text: str) -> str:
160
+ """Remove ANSI escape sequences (SGR and cursor controls).
161
+
162
+ This is more comprehensive than stripping only color codes ending with 'm'.
163
+ """
164
+ ansi_re = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
165
+ return ansi_re.sub("", text)
166
+
167
+ def _is_plain_output(self) -> bool:
168
+ """Detect if we should avoid rich formatting entirely.
169
+
170
+ Leverages ACB Console's plain-mode flag when available and falls back
171
+ to Rich Console properties when not.
172
+ """
173
+ try:
174
+ if bool(getattr(self.console, "_plain_mode", False)):
175
+ return True
176
+ # Fallback on Rich Console capabilities
177
+ is_tty = bool(getattr(self.console, "is_terminal", True))
178
+ color_system = getattr(self.console, "color_system", None)
179
+ return (not is_tty) or (color_system in (None, "null"))
180
+ except Exception:
181
+ # Prefer plain in ambiguous environments
182
+ return True
86
183
 
184
+ @handle_errors
87
185
  def run_cleaning_phase(self, options: OptionsProtocol) -> bool:
88
186
  if not options.clean:
89
187
  return True
90
188
 
91
189
  self.session.track_task("cleaning", "Code cleaning")
92
- try:
93
- self._display_cleaning_header()
94
- return self._execute_cleaning_process()
95
- except Exception as e:
96
- self.handle_subprocess_error(e, [], "Code cleaning", critical=False)
97
- self.session.fail_task("cleaning", str(e))
98
- return False
190
+ self._display_cleaning_header()
191
+ return self._execute_cleaning_process()
99
192
 
100
- def _display_cleaning_header(self) -> None:
101
- self.console.print("\n" + "-" * 40)
193
+ @handle_errors
194
+ def run_configuration_phase(self, options: OptionsProtocol) -> bool:
195
+ if options.no_config_updates:
196
+ return True
197
+ self.session.track_task("configuration", "Configuration updates")
102
198
  self.console.print(
103
- "[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]",
199
+ "[dim]⚙️ Configuration phase skipped (no automated updates defined).[/dim]"
104
200
  )
105
- self.console.print("-" * 40 + "\n")
106
- self.console.print("[yellow]🧹[/yellow] Starting code cleaning...")
107
-
108
- def _display_version_bump_header(self, version_type: str) -> None:
109
- self.console.print("\n" + "-" * 74)
110
- self.console.print(
111
- f"[bold bright_magenta]📦 BUMP VERSION[/bold bright_magenta] [bold bright_white]Updating package version ({version_type})[/bold bright_white]",
201
+ self.session.complete_task(
202
+ "configuration", "No configuration updates were required."
112
203
  )
113
- self.console.print("-" * 74 + "\n")
204
+ return True
114
205
 
115
- def _display_publish_header(self) -> None:
116
- self.console.print("\n" + "-" * 74)
117
- self.console.print(
118
- "[bold bright_yellow]🚀 PUBLISH[/bold bright_yellow] [bold bright_white]Publishing to PyPI[/bold bright_white]",
119
- )
120
- self.console.print("-" * 74 + "\n")
206
+ @handle_errors
207
+ def run_hooks_phase(self, options: OptionsProtocol) -> bool:
208
+ if options.skip_hooks:
209
+ return True
121
210
 
122
- def _display_git_staging_header(self) -> None:
123
- self.console.print("\n" + "-" * 74)
124
- self.console.print(
125
- "[bold bright_cyan]🏷️ GIT OPERATIONS[/bold bright_cyan] [bold bright_white]Staging files and creating tags[/bold bright_white]",
126
- )
127
- self.console.print("-" * 74 + "\n")
211
+ if not self.run_fast_hooks_only(options):
212
+ return False
128
213
 
129
- def _display_commit_push_header(self) -> None:
130
- self.console.print("\n" + "-" * 74)
131
- self.console.print(
132
- "[bold bright_green]📤 COMMIT & PUSH[/bold bright_green] [bold bright_white]Committing and pushing changes[/bold bright_white]",
133
- )
134
- self.console.print("-" * 74 + "\n")
214
+ return self.run_comprehensive_hooks_only(options)
135
215
 
136
- def _execute_cleaning_process(self) -> bool:
137
- cleaning_result = self.code_cleaner.clean_files(self.pkg_path, use_backup=True)
216
+ def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
217
+ if options.skip_hooks:
218
+ self.console.print("[yellow]⚠️[/yellow] Skipping fast hooks (--skip-hooks)")
219
+ return True
138
220
 
139
- if isinstance(cleaning_result, list):
140
- cleaned_files = [str(r.file_path) for r in cleaning_result if r.success]
141
- self._report_cleaning_results(cleaned_files)
142
- return all(r.success for r in cleaning_result) if cleaning_result else True
143
- else:
144
- self._report_package_cleaning_results(cleaning_result)
145
- return cleaning_result.overall_success
221
+ # Prevent multiple fast-hook runs in a single workflow session unless
222
+ # explicitly reset by post-cleaning sanity check.
223
+ if getattr(self, "_fast_hooks_started", False):
224
+ self.logger.debug("Duplicate fast hooks invocation detected; skipping")
225
+ return True
146
226
 
147
- def _handle_no_files_to_clean(self) -> bool:
148
- self.console.print("[yellow]⚠️[/ yellow] No Python files found to clean")
149
- self.session.complete_task("cleaning", "No files to clean")
150
- return True
227
+ # Mark fast hooks as started immediately to prevent duplicate calls in case of failures
228
+ self._fast_hooks_started = True
229
+ self.session.track_task("hooks_fast", "Fast quality checks")
151
230
 
152
- def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
153
- if cleaned_files:
154
- self.console.print(f"[green]✅[/ green] Cleaned {len(cleaned_files)} files")
155
- self.session.complete_task(
156
- "cleaning",
157
- f"Cleaned {len(cleaned_files)} files",
158
- )
159
- else:
160
- self.console.print("[green]✅[/ green] No cleaning needed")
161
- self.session.complete_task("cleaning", "No cleaning needed")
231
+ # Fast hooks get 2 attempts (auto-fix on failure), comprehensive hooks run once
232
+ max_attempts = 2
233
+ attempt = 0
162
234
 
163
- def _report_package_cleaning_results(self, result: PackageCleaningResult) -> None:
164
- if result.overall_success:
165
- self.console.print(
166
- f"[green]✅[/ green] Package cleaning completed successfully! "
167
- f"({result.successful_files}/{result.total_files} files cleaned)"
168
- )
169
- self.session.complete_task(
170
- "cleaning",
171
- f"Cleaned {result.successful_files}/{result.total_files} files with backup protection",
172
- )
173
- else:
174
- self.console.print(
175
- f"[red]❌[/ red] Package cleaning failed! "
176
- f"({result.failed_files}/{result.total_files} files failed)"
177
- )
178
-
179
- if result.backup_restored:
180
- self.console.print(
181
- "[yellow]⚠️[/ yellow] Files were automatically restored from backup"
182
- )
183
- self.session.complete_task(
184
- "cleaning", "Failed with automatic backup restoration"
185
- )
186
- else:
187
- self.session.fail_task(
188
- "cleaning", f"Failed to clean {result.failed_files} files"
189
- )
235
+ while attempt < max_attempts:
236
+ attempt += 1
190
237
 
191
- if result.backup_metadata:
238
+ # Display stage header for each attempt
239
+ if attempt > 1:
192
240
  self.console.print(
193
- f"[blue]📦[/ blue] Backup available at: {result.backup_metadata.backup_directory}"
241
+ f"\n[yellow]♻️[/yellow] Verification Retry {attempt}/{max_attempts}\n"
194
242
  )
195
243
 
196
- def run_configuration_phase(self, options: OptionsProtocol) -> bool:
197
- if options.no_config_updates:
198
- return True
199
- self.session.track_task("configuration", "Configuration updates")
200
- try:
201
- success = self._execute_configuration_steps(options)
202
- self._complete_configuration_task(success)
203
- return success
204
- except Exception as e:
205
- self.handle_subprocess_error(e, [], "Configuration phase", critical=False)
206
- self.session.fail_task("configuration", str(e))
207
- return False
208
-
209
- def _execute_configuration_steps(self, options: OptionsProtocol) -> bool:
210
- success = True
244
+ self._display_hook_phase_header(
245
+ "FAST HOOKS",
246
+ "Formatters, import sorting, and quick static analysis",
247
+ )
211
248
 
212
- success &= self._update_configuration_files(options)
249
+ # Run hooks (now configured to run in fix mode by default)
250
+ success = self._execute_hooks_once(
251
+ "fast", self.hook_manager.run_fast_hooks, options, attempt
252
+ )
213
253
 
214
- return success
254
+ if success:
255
+ break
215
256
 
216
- def _update_configuration_files(self, options: OptionsProtocol) -> bool:
217
- success = True
218
- if not self.config_service.update_precommit_config(options):
219
- success = False
220
- if not self.config_service.update_pyproject_config(options):
221
- success = False
222
- return success
257
+ # Fast iteration mode intentionally avoids retries
258
+ if getattr(options, "fast_iteration", False):
259
+ break
223
260
 
224
- def _complete_configuration_task(self, success: bool) -> None:
225
- message = (
226
- "Configuration updated successfully"
227
- if success
228
- else "Some configuration updates failed"
229
- )
230
- self.session.complete_task("configuration", message)
261
+ # If we have more attempts, continue to retry to verify fixes worked
262
+ if attempt < max_attempts:
263
+ self._display_hook_failures("fast", self._last_hook_results, options)
231
264
 
232
- def _is_crackerjack_project(self) -> bool:
233
- pyproject_path = self.pkg_path / "pyproject.toml"
234
- if not pyproject_path.exists():
235
- return False
265
+ summary = self._last_hook_summary or {}
266
+ details = self._format_hook_summary(summary)
236
267
 
237
- try:
238
- import tomllib
268
+ if success:
269
+ self.session.complete_task("hooks_fast", details=details)
270
+ else:
271
+ self.session.fail_task("hooks_fast", "Fast hook failures detected")
239
272
 
240
- with pyproject_path.open("rb") as f:
241
- data = tomllib.load(f)
273
+ # Ensure fast hooks output is fully rendered before comprehensive hooks start
274
+ self.console.print()
242
275
 
243
- project_name: str = data.get("project", {}).get("name", "")
244
- return project_name == "crackerjack"
245
- except Exception:
246
- return False
276
+ return success
247
277
 
248
- def run_hooks_phase(self, options: OptionsProtocol) -> bool:
278
+ def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
249
279
  if options.skip_hooks:
280
+ self.console.print(
281
+ "[yellow]⚠️[/yellow] Skipping comprehensive hooks (--skip-hooks)"
282
+ )
250
283
  return True
251
284
 
252
- temp_config = self.config_service.get_temp_config_path()
253
- if temp_config:
254
- self.hook_manager.set_config_path(temp_config)
255
-
256
- if not self.run_fast_hooks_only(options):
257
- return False
285
+ self.session.track_task("hooks_comprehensive", "Comprehensive quality checks")
286
+ self._display_hook_phase_header(
287
+ "COMPREHENSIVE HOOKS",
288
+ "Type, security, and complexity checking",
289
+ )
258
290
 
259
- return self.run_comprehensive_hooks_only(options)
291
+ # Comprehensive hooks run once (no retry)
292
+ success = self._execute_hooks_once(
293
+ "comprehensive",
294
+ self.hook_manager.run_comprehensive_hooks,
295
+ options,
296
+ attempt=1,
297
+ )
260
298
 
261
- def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
262
- if options.skip_hooks:
263
- return True
299
+ if not success:
300
+ self._display_hook_failures(
301
+ "comprehensive", self._last_hook_results, options
302
+ )
264
303
 
265
- hook_results = self.hook_manager.run_fast_hooks()
266
- return all(r.status == "passed" for r in hook_results)
304
+ summary = self._last_hook_summary or {}
305
+ details = self._format_hook_summary(summary)
267
306
 
268
- def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
269
- if options.skip_hooks:
270
- return True
307
+ if success:
308
+ self.session.complete_task("hooks_comprehensive", details=details)
309
+ else:
310
+ self.session.fail_task(
311
+ "hooks_comprehensive", "Comprehensive hook failures detected"
312
+ )
271
313
 
272
- hook_results = self.hook_manager.run_comprehensive_hooks()
273
- return all(r.status == "passed" for r in hook_results)
314
+ return success
274
315
 
316
+ @handle_errors
275
317
  def run_testing_phase(self, options: OptionsProtocol) -> bool:
276
318
  if not options.test:
277
319
  return True
278
320
  self.session.track_task("testing", "Test execution")
279
- try:
280
- self.console.print("\n" + "-" * 74)
281
- self.console.print(
282
- "[bold bright_blue]🧪 TESTS[/ bold bright_blue] [bold bright_white]Running test suite[/ bold bright_white]",
321
+ self.console.print("\n" + make_separator("-"))
322
+ self.console.print(
323
+ "[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
324
+ )
325
+ self.console.print(make_separator("-") + "\n")
326
+ if not self.test_manager.validate_test_environment():
327
+ self.session.fail_task("testing", "Test environment validation failed")
328
+ return False
329
+ test_success = self.test_manager.run_tests(options)
330
+ if test_success:
331
+ coverage_info = self.test_manager.get_coverage()
332
+ self.session.complete_task(
333
+ "testing",
334
+ f"Tests passed, coverage: {coverage_info.get('total_coverage', 0):.1f}%",
283
335
  )
284
- self.console.print("-" * 74 + "\n")
285
- if not self.test_manager.validate_test_environment():
286
- self.session.fail_task("testing", "Test environment validation failed")
287
- return False
288
- test_success = self.test_manager.run_tests(options)
289
- if test_success:
290
- coverage_info = self.test_manager.get_coverage()
291
- self.session.complete_task(
292
- "testing",
293
- f"Tests passed, coverage: {coverage_info.get('total_coverage', 0): .1f}%",
294
- )
295
- else:
296
- self.session.fail_task("testing", "Tests failed")
336
+ else:
337
+ self.session.fail_task("testing", "Tests failed")
297
338
 
298
- return test_success
299
- except Exception as e:
300
- self.console.print(f"Testing error: {e}")
301
- self.session.fail_task("testing", str(e))
302
- return False
339
+ return test_success
303
340
 
341
+ @handle_errors
304
342
  def run_publishing_phase(self, options: OptionsProtocol) -> bool:
305
343
  version_type = self._determine_version_type(options)
306
344
  if not version_type:
307
345
  return True
308
346
 
309
347
  self.session.track_task("publishing", f"Publishing ({version_type})")
310
- try:
311
- return self._execute_publishing_workflow(options, version_type)
312
- except Exception as e:
313
- self.console.print(f"[red]❌[/ red] Publishing failed: {e}")
314
- self.session.fail_task("publishing", str(e))
315
- return False
348
+ return self._execute_publishing_workflow(options, version_type)
316
349
 
317
- def _determine_version_type(self, options: OptionsProtocol) -> str | None:
318
- if options.publish:
319
- publish_value: str | None = (
320
- options.publish if isinstance(options.publish, str) else None
321
- )
322
- return publish_value
323
- if options.all:
324
- all_value: str | None = (
325
- options.all if isinstance(options.all, str) else None
326
- )
327
- return all_value
328
- if options.bump:
329
- self._handle_version_bump_only(options.bump)
330
- return None
331
- return None
332
-
333
- def _execute_publishing_workflow(
334
- self,
335
- options: OptionsProtocol,
336
- version_type: str,
337
- ) -> bool:
338
- # Display version bump header
339
- self._display_version_bump_header(version_type)
340
- new_version = self.publish_manager.bump_version(version_type)
350
+ @handle_errors
351
+ def run_commit_phase(self, options: OptionsProtocol) -> bool:
352
+ if not options.commit:
353
+ return True
341
354
 
342
- # Display git operations header for staging and tagging
343
- self._display_git_staging_header()
344
- self.console.print("[blue]📂[/ blue] Staging all changes for publishing...")
345
- if not self.git_service.add_all_files():
355
+ # Skip if publishing phase already handled commits
356
+ # (Publishing phase handles both pre-publish and version-bump commits when -c is used)
357
+ version_type = self._determine_version_type(options)
358
+ if version_type:
359
+ # Publishing workflow already committed everything
346
360
  self.console.print(
347
- "[yellow]⚠️[/ yellow] Failed to stage files, continuing with publish..."
361
+ "[dim]ℹ️ Commit phase skipped (handled by publish workflow)[/dim]"
348
362
  )
349
-
350
- if not options.no_git_tags:
351
- self.publish_manager.create_git_tag(new_version)
352
-
353
- # Display publish header
354
- self._display_publish_header()
355
- if self.publish_manager.publish_package():
356
- self._handle_successful_publish(options, new_version)
357
363
  return True
358
- self.session.fail_task("publishing", "Package publishing failed")
359
- return False
360
364
 
361
- def _handle_successful_publish(
365
+ # Display commit & push header
366
+ self._display_commit_push_header()
367
+ self.session.track_task("commit", "Git commit and push")
368
+ changed_files = self.git_service.get_changed_files()
369
+ if not changed_files:
370
+ return self._handle_no_changes_to_commit()
371
+ commit_message = self._get_commit_message(changed_files, options)
372
+ return self._execute_commit_and_push(changed_files, commit_message)
373
+
374
+ def _execute_hooks_once(
362
375
  self,
376
+ suite_name: str,
377
+ hook_runner: t.Callable[[], list[HookResult]],
363
378
  options: OptionsProtocol,
364
- new_version: str,
365
- ) -> None:
366
- self.console.print(f"[green]🚀[/ green] Successfully published {new_version}!")
367
-
368
- if options.cleanup_pypi:
369
- self.publish_manager.cleanup_old_releases(options.keep_releases)
379
+ attempt: int,
380
+ ) -> bool:
381
+ """Execute a hook suite once with progress bar (no retry logic - retry is handled at stage level)."""
382
+ self._last_hook_summary = None
383
+ self._last_hook_results = []
370
384
 
371
- self.session.complete_task("publishing", f"Published {new_version}")
385
+ hook_count = self.hook_manager.get_hook_count(suite_name)
386
+ progress = self._create_progress_bar()
372
387
 
373
- def run_commit_phase(self, options: OptionsProtocol) -> bool:
374
- if not options.commit:
375
- return True
388
+ callbacks = self._setup_progress_callbacks(progress)
389
+ elapsed_time = self._run_hooks_with_progress(
390
+ suite_name, hook_runner, progress, hook_count, attempt, callbacks
391
+ )
376
392
 
377
- # Display commit & push header
378
- self._display_commit_push_header()
379
- self.session.track_task("commit", "Git commit and push")
380
- try:
381
- changed_files = self.git_service.get_changed_files()
382
- if not changed_files:
383
- return self._handle_no_changes_to_commit()
384
- commit_message = self._get_commit_message(changed_files, options)
385
- return self._execute_commit_and_push(changed_files, commit_message)
386
- except Exception as e:
387
- self.console.print(f"[red]❌[/ red] Commit failed: {e}")
388
- self.session.fail_task("commit", str(e))
393
+ if elapsed_time is None:
389
394
  return False
390
395
 
391
- def _handle_no_changes_to_commit(self) -> bool:
392
- self.console.print("[yellow]ℹ️[/ yellow] No changes to commit")
393
-
394
- from contextlib import suppress
396
+ return self._process_hook_results(suite_name, elapsed_time, attempt)
397
+
398
+ def _create_progress_bar(self) -> Progress:
399
+ """Create compact progress bar for hook execution."""
400
+ return Progress(
401
+ SpinnerColumn(spinner_name="dots"),
402
+ TextColumn("[cyan]{task.description}[/cyan]"),
403
+ BarColumn(bar_width=20),
404
+ MofNCompleteColumn(),
405
+ TimeElapsedColumn(),
406
+ console=self.console,
407
+ transient=True,
408
+ )
395
409
 
396
- with suppress(ValueError, Exception):
397
- commit_count = self.git_service.get_unpushed_commit_count()
398
- if commit_count > 0:
399
- self.console.print(
400
- f"[blue]📤[/ blue] Found {commit_count} unpushed commit(s), attempting push..."
401
- )
402
- if self.git_service.push():
403
- self.session.complete_task(
404
- "commit",
405
- f"No new changes, pushed {commit_count} existing commit(s)",
406
- )
407
- return True
408
- else:
409
- self.console.print(
410
- "[yellow]⚠️[/ yellow] Push failed for existing commits"
411
- )
410
+ def _setup_progress_callbacks(
411
+ self, progress: Progress
412
+ ) -> dict[str, t.Callable[[int, int], None] | None | dict[str, t.Any]]:
413
+ """Setup progress callbacks and store originals for restoration."""
414
+ task_id_holder = {"task_id": None}
412
415
 
413
- self.session.complete_task("commit", "No changes to commit")
414
- return True
416
+ def update_progress(completed: int, total: int) -> None:
417
+ if task_id_holder["task_id"] is not None:
418
+ progress.update(task_id_holder["task_id"], completed=completed)
415
419
 
416
- def _execute_commit_and_push(
417
- self,
418
- changed_files: list[str],
419
- commit_message: str,
420
- ) -> bool:
421
- if not self.git_service.add_files(changed_files):
422
- self.session.fail_task("commit", "Failed to stage files")
423
- return False
420
+ def update_progress_started(started: int, total: int) -> None:
421
+ if task_id_holder["task_id"] is not None:
422
+ progress.update(task_id_holder["task_id"], completed=started)
424
423
 
425
- if not self.git_service.commit(commit_message):
426
- self.session.fail_task("commit", "Commit failed")
427
- return False
424
+ original_callback = getattr(self.hook_manager, "_progress_callback", None)
425
+ original_start_callback = getattr(
426
+ self.hook_manager, "_progress_start_callback", None
427
+ )
428
428
 
429
- return self._handle_push_result(commit_message)
429
+ self.hook_manager._progress_callback = update_progress
430
+ self.hook_manager._progress_start_callback = update_progress_started
430
431
 
431
- def _handle_push_result(self, commit_message: str) -> bool:
432
- if self.git_service.push():
433
- self.console.print(
434
- f"[green]🎉[/ green] Committed and pushed: {commit_message}",
435
- )
436
- self.session.complete_task(
437
- "commit",
438
- f"Committed and pushed: {commit_message}",
439
- )
440
- else:
441
- self.console.print(
442
- f"[yellow]⚠️[/ yellow] Committed but push failed: {commit_message}",
443
- )
444
- self.session.complete_task(
445
- "commit",
446
- f"Committed (push failed): {commit_message}",
447
- )
448
- return True
432
+ return {
433
+ "update": update_progress,
434
+ "update_started": update_progress_started,
435
+ "original": original_callback,
436
+ "original_started": original_start_callback,
437
+ "task_id_holder": task_id_holder,
438
+ }
449
439
 
450
- def execute_hooks_with_retry(
440
+ def _run_hooks_with_progress(
451
441
  self,
452
- hook_type: str,
453
- hook_runner: t.Callable[[], list[t.Any]],
454
- options: OptionsProtocol,
455
- ) -> bool:
456
- return self._execute_hooks_with_retry(hook_type, hook_runner, options)
457
-
458
- def _handle_version_bump_only(self, bump_type: str) -> bool:
459
- self.session.track_task("version_bump", f"Version bump ({bump_type})")
442
+ suite_name: str,
443
+ hook_runner: t.Callable[[], list[HookResult]],
444
+ progress: Progress,
445
+ hook_count: int,
446
+ attempt: int,
447
+ callbacks: dict[str, t.Any],
448
+ ) -> float | None:
449
+ """Run hooks with progress tracking, return elapsed time or None on error."""
460
450
  try:
461
- # Display version bump header
462
- self._display_version_bump_header(bump_type)
463
- new_version = self.publish_manager.bump_version(bump_type)
464
- self.console.print(f"[green]🎯[/ green] Version bumped to {new_version}")
465
- self.session.complete_task("version_bump", f"Bumped to {new_version}")
466
- return True
467
- except Exception as e:
468
- self.console.print(f"[red]❌[/ red] Version bump failed: {e}")
469
- self.session.fail_task("version_bump", str(e))
470
- return False
471
-
472
- def _get_commit_message(
473
- self,
474
- changed_files: list[str],
475
- options: OptionsProtocol,
476
- ) -> str:
477
- # Smart commit is now enabled by default for all commit operations
478
- # Use basic commit messages only if explicitly disabled
479
- use_smart_commit = getattr(options, "smart_commit", True)
480
- disable_smart_commit = getattr(options, "basic_commit", False)
481
-
482
- if use_smart_commit and not disable_smart_commit:
483
- try:
484
- from crackerjack.services.intelligent_commit import (
485
- CommitMessageGenerator,
451
+ with progress:
452
+ task_id = progress.add_task(
453
+ f"Running {suite_name} hooks:",
454
+ total=hook_count,
486
455
  )
456
+ callbacks["task_id_holder"]["task_id"] = task_id
487
457
 
488
- self.console.print(
489
- "[cyan]🤖[/cyan] Generating intelligent commit message..."
490
- )
491
- commit_generator = CommitMessageGenerator(
492
- console=self.console, git_service=self.git_service
493
- )
458
+ import time
494
459
 
495
- intelligent_message = commit_generator.generate_commit_message(
496
- include_body=False,
497
- conventional_commits=True,
498
- )
460
+ start_time = time.time()
461
+ hook_results = hook_runner()
462
+ self._last_hook_results = hook_results
463
+ elapsed_time = time.time() - start_time
499
464
 
500
- if not options.interactive:
501
- self.console.print(
502
- f"[green]✨[/green] Generated: {intelligent_message}"
503
- )
504
- return intelligent_message
465
+ return elapsed_time
505
466
 
506
- # In interactive mode, offer the intelligent message plus fallback suggestions
507
- suggestions = [intelligent_message]
508
- fallback_suggestions = self.git_service.get_commit_message_suggestions(
509
- changed_files
510
- )
511
- suggestions.extend(
512
- fallback_suggestions[:3]
513
- ) # Add up to 3 fallback options
467
+ except Exception as exc:
468
+ self._handle_hook_execution_error(suite_name, exc, attempt)
469
+ return None
470
+ finally:
471
+ self._restore_progress_callbacks(callbacks)
514
472
 
515
- return self._interactive_commit_message_selection(suggestions)
473
+ def _handle_hook_execution_error(
474
+ self, suite_name: str, exc: Exception, attempt: int
475
+ ) -> None:
476
+ """Handle errors during hook execution."""
477
+ self.console.print(
478
+ f"[red]❌[/red] {suite_name.title()} hooks encountered an unexpected error: {exc}"
479
+ )
480
+ self.logger.exception(
481
+ "Hook execution raised exception",
482
+ extra={"suite": suite_name, "attempt": attempt},
483
+ )
516
484
 
517
- except Exception as e:
518
- self.console.print(
519
- f"[yellow]⚠️[/yellow] Intelligent commit generation failed: {e}"
520
- )
521
- # Fallback to original logic
485
+ def _restore_progress_callbacks(self, callbacks: dict[str, t.Any]) -> None:
486
+ """Restore original progress callbacks."""
487
+ self.hook_manager._progress_callback = callbacks["original"]
488
+ self.hook_manager._progress_start_callback = callbacks["original_started"]
522
489
 
523
- # Original logic for non-smart commits
524
- suggestions = self.git_service.get_commit_message_suggestions(changed_files)
490
+ def _process_hook_results(
491
+ self, suite_name: str, elapsed_time: float, attempt: int
492
+ ) -> bool:
493
+ """Process hook results and determine success."""
494
+ summary = self.hook_manager.get_hook_summary(
495
+ self._last_hook_results, elapsed_time=elapsed_time
496
+ )
497
+ self._last_hook_summary = summary
498
+ self._report_hook_results(suite_name, self._last_hook_results, summary, attempt)
525
499
 
526
- if not suggestions:
527
- return "Update project files"
500
+ if summary.get("failed", 0) == 0 == summary.get("errors", 0):
501
+ return True
528
502
 
529
- if not options.interactive:
530
- return suggestions[0]
503
+ self.logger.warning(
504
+ "Hook suite reported failures",
505
+ extra={
506
+ "suite": suite_name,
507
+ "attempt": attempt,
508
+ "failed": summary.get("failed", 0),
509
+ "errors": summary.get("errors", 0),
510
+ },
511
+ )
512
+ return False
531
513
 
532
- return self._interactive_commit_message_selection(suggestions)
514
+ def _display_hook_phase_header(self, title: str, description: str) -> None:
515
+ sep = make_separator("-")
516
+ self.console.print("\n" + sep)
517
+ # Combine title and description into a single line with leading icon
518
+ pretty_title = title.title()
519
+ message = (
520
+ f"[bold bright_cyan]🔍 {pretty_title}[/bold bright_cyan][bold bright_white]"
521
+ f" - {description}[/bold bright_white]"
522
+ )
523
+ self.console.print(message)
524
+ self.console.print(sep + "\n")
533
525
 
534
- def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
535
- self._display_commit_suggestions(suggestions)
526
+ def _report_hook_results(
527
+ self,
528
+ suite_name: str,
529
+ results: list[HookResult],
530
+ summary: dict[str, t.Any],
531
+ attempt: int,
532
+ ) -> None:
533
+ total = summary.get("total", 0)
534
+ passed = summary.get("passed", 0)
535
+ failed = summary.get("failed", 0)
536
+ errors = summary.get("errors", 0)
537
+ duration = summary.get("total_duration", 0.0)
536
538
 
537
- try:
538
- choice = self.console.input(
539
- f"\nSelect message (1-{len(suggestions)}) or enter custom: ",
539
+ if total == 0:
540
+ self.console.print(
541
+ f"[yellow]⚠️[/yellow] No {suite_name} hooks are configured for this project."
540
542
  )
541
- return self._process_commit_choice(choice, suggestions)
542
- except (KeyboardInterrupt, EOFError):
543
- return suggestions[0]
543
+ return
544
544
 
545
- def _display_commit_suggestions(self, suggestions: list[str]) -> None:
546
- self.console.print("[cyan]📝[/ cyan] Commit message suggestions: ")
547
- for i, suggestion in enumerate(suggestions, 1):
548
- self.console.print(f" {i}. {suggestion}")
545
+ base_message = (
546
+ f"{suite_name.title()} hooks attempt {attempt}: "
547
+ f"{passed}/{total} passed in {duration:.2f}s"
548
+ )
549
549
 
550
- def _process_commit_choice(self, choice: str, suggestions: list[str]) -> str:
551
- if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
552
- return suggestions[int(choice) - 1]
553
- return choice or suggestions[0]
550
+ if failed or errors:
551
+ self.console.print(f"\n[red]❌[/red] {base_message}\n")
552
+ # Always show a results table to aid debugging when there are failures
553
+ self._render_hook_results_table(suite_name, results)
554
+ else:
555
+ self.console.print(f"\n[green]✅[/green] {base_message}\n")
556
+ self._render_hook_results_table(suite_name, results)
554
557
 
555
- def _execute_hooks_with_retry(
558
+ def _render_hook_results_table(
556
559
  self,
557
- hook_type: str,
558
- hook_runner: t.Callable[[], list[t.Any]],
559
- options: OptionsProtocol,
560
- ) -> bool:
561
- self._initialize_hook_execution(hook_type)
562
- max_retries = self._get_max_retries(hook_type)
563
-
564
- for attempt in range(max_retries):
565
- try:
566
- execution_result = self._execute_single_hook_attempt(hook_runner)
567
- if execution_result is None:
568
- return False
569
-
570
- results, summary = execution_result
571
- should_continue = self._process_hook_results(
572
- hook_type, options, summary, results, attempt, max_retries
573
- )
560
+ suite_name: str,
561
+ results: list[HookResult],
562
+ ) -> None:
563
+ if not results:
564
+ return
574
565
 
575
- if should_continue == "continue":
576
- continue
577
- elif should_continue == "success":
578
- return True
579
- else:
580
- return False
566
+ if self._is_plain_output():
567
+ self._render_plain_hook_results(suite_name, results)
568
+ else:
569
+ self._render_rich_hook_results(suite_name, results)
581
570
 
582
- except Exception as e:
583
- return self._handle_hook_exception(hook_type, e)
571
+ def _render_plain_hook_results(
572
+ self, suite_name: str, results: list[HookResult]
573
+ ) -> None:
574
+ """Render hook results in plain text format."""
575
+ self.console.print(f"{suite_name.title()} Hook Results:", highlight=False)
576
+
577
+ stats = self._calculate_hook_statistics(results)
578
+ hooks_to_show = stats["failed_hooks"] or results
579
+
580
+ for result in hooks_to_show:
581
+ self._print_plain_hook_result(result)
582
+
583
+ if not stats["failed_hooks"] and results:
584
+ self._print_plain_summary(stats)
585
+
586
+ self.console.print()
587
+
588
+ def _calculate_hook_statistics(self, results: list[HookResult]) -> dict[str, t.Any]:
589
+ """Calculate statistics from hook results."""
590
+ passed_hooks = [r for r in results if r.status.lower() in {"passed", "success"}]
591
+ failed_hooks = [
592
+ r for r in results if r.status.lower() in {"failed", "error", "timeout"}
593
+ ]
594
+ other_hooks = [
595
+ r
596
+ for r in results
597
+ if r.status.lower()
598
+ not in {"passed", "success", "failed", "error", "timeout"}
599
+ ]
600
+
601
+ # Calculate total issues using issues_count (which may be larger than len(issues_found))
602
+ # Passed hooks always contribute 0 issues
603
+ # Config errors (is_config_error=True) are counted separately
604
+ total_issues = 0
605
+ config_errors = 0
606
+ for r in results:
607
+ if r.status == "passed":
608
+ continue
609
+ # Count config errors separately - they're not code quality issues
610
+ if hasattr(r, "is_config_error") and r.is_config_error:
611
+ config_errors += 1
612
+ continue
613
+ # Use issues_count directly (don't fall back to len(issues_found))
614
+ # because issues_found may contain error detail lines, not actual issues
615
+ if hasattr(r, "issues_count"):
616
+ total_issues += r.issues_count
617
+ elif r.issues_found:
618
+ # Legacy fallback for old HookResults without issues_count
619
+ total_issues += len(r.issues_found)
620
+
621
+ return {
622
+ "total_hooks": len(results),
623
+ "passed_hooks": passed_hooks,
624
+ "failed_hooks": failed_hooks,
625
+ "other_hooks": other_hooks,
626
+ "total_passed": len(passed_hooks),
627
+ "total_failed": len(failed_hooks),
628
+ "total_other": len(other_hooks),
629
+ "total_issues_found": total_issues,
630
+ "config_errors": config_errors,
631
+ }
632
+
633
+ def _print_plain_hook_result(self, result: HookResult) -> None:
634
+ """Print a single hook result in plain format."""
635
+ name = self._strip_ansi(result.name)
636
+ status = result.status.upper()
637
+ duration = f"{result.duration:.2f}s"
638
+
639
+ # Determine issues display (matches Rich table logic)
640
+ if result.status == "passed":
641
+ issues = "0"
642
+ elif hasattr(result, "is_config_error") and result.is_config_error:
643
+ # Config/tool error - show simple symbol instead of misleading count
644
+ issues = "[yellow]![/yellow]"
645
+ else:
646
+ # For failed hooks with code violations, use issues_count
647
+ # Don't fall back to len(issues_found) - it may contain error detail lines
648
+ issues = str(result.issues_count if hasattr(result, "issues_count") else 0)
584
649
 
585
- return False
650
+ self.console.print(
651
+ f" - {name} :: {status} | {duration} | issues={issues}",
652
+ )
586
653
 
587
- def _execute_single_hook_attempt(
588
- self, hook_runner: t.Callable[[], list[t.Any]]
589
- ) -> tuple[list[t.Any], dict[str, t.Any]] | None:
590
- try:
591
- results = hook_runner()
592
- summary = self.hook_manager.get_hook_summary(results)
593
- return results, summary
594
- except Exception:
595
- return None
654
+ def _print_plain_summary(self, stats: dict[str, t.Any]) -> None:
655
+ """Print summary statistics in plain format."""
656
+ issues_text = f"{stats['total_issues_found']} issues found"
657
+ if stats.get("config_errors", 0) > 0:
658
+ issues_text += f" ({stats['config_errors']} config)"
596
659
 
597
- def _process_hook_results(
598
- self,
599
- hook_type: str,
600
- options: OptionsProtocol,
601
- summary: dict[str, t.Any],
602
- results: list[t.Any],
603
- attempt: int,
604
- max_retries: int,
605
- ) -> str:
606
- if not self._has_hook_failures(summary):
607
- self._handle_hook_success(hook_type, summary)
608
- return "success"
660
+ self.console.print(
661
+ f" Summary: {stats['total_passed']}/{stats['total_hooks']} hooks passed, {issues_text}",
662
+ highlight=False,
663
+ )
609
664
 
610
- if self._should_retry_hooks(hook_type, attempt, max_retries, results):
611
- return "continue"
665
+ def _render_rich_hook_results(
666
+ self, suite_name: str, results: list[HookResult]
667
+ ) -> None:
668
+ """Render hook results in Rich format."""
669
+ stats = self._calculate_hook_statistics(results)
670
+ summary_text = self._build_summary_text(stats)
671
+ table = self._build_results_table(results)
672
+ panel = self._build_results_panel(suite_name, table, summary_text)
612
673
 
613
- self._handle_hook_failures(
614
- hook_type, options, summary, results, attempt, max_retries
615
- )
616
- return "failure"
674
+ self.console.print(panel)
617
675
 
618
- def _initialize_hook_execution(self, hook_type: str) -> None:
619
- self.logger.info(f"Starting {hook_type} hooks execution")
620
- self.session.track_task(
621
- f"{hook_type}_hooks",
622
- f"{hook_type.title()} hooks execution",
676
+ # Add legend if any config errors are present
677
+ has_config_errors = any(
678
+ hasattr(r, "is_config_error") and r.is_config_error for r in results
623
679
  )
680
+ if has_config_errors:
681
+ self.console.print(
682
+ " [dim][yellow]![/yellow] = Configuration or tool error (not code "
683
+ "issues)[/dim]"
684
+ )
624
685
 
625
- def _get_max_retries(self, hook_type: str) -> int:
626
- return 2 if hook_type == "fast" else 1
686
+ self.console.print()
627
687
 
628
- def _has_hook_failures(self, summary: dict[str, t.Any]) -> bool:
629
- return summary["failed"] > 0 or summary["errors"] > 0
688
+ @staticmethod
689
+ def _build_summary_text(stats: dict[str, t.Any]) -> str:
690
+ """Build summary text for Rich display."""
691
+ summary_text = (
692
+ f"Total: [white]{stats['total_hooks']}[/white] | Passed:"
693
+ f" [green]{stats['total_passed']}[/green] | Failed: [red]{stats['total_failed']}[/red]"
694
+ )
695
+ if stats["total_other"] > 0:
696
+ summary_text += f" | Other: [yellow]{stats['total_other']}[/yellow]"
697
+
698
+ # Show issues found with config count in parentheses if present
699
+ issues_text = f"[white]{stats['total_issues_found']}[/white]"
700
+ if stats.get("config_errors", 0) > 0:
701
+ issues_text += f" [dim]({stats['config_errors']} config)[/dim]"
702
+ summary_text += f" | Issues found: {issues_text}"
703
+ return summary_text
704
+
705
+ def _build_results_table(self, results: list[HookResult]) -> Table:
706
+ """Build Rich table from hook results."""
707
+ table = Table(
708
+ box=box.SIMPLE,
709
+ header_style="bold bright_white",
710
+ expand=True,
711
+ )
712
+ table.add_column("Hook", style="cyan", overflow="fold", min_width=20)
713
+ table.add_column("Status", style="bright_white", min_width=8)
714
+ table.add_column("Duration", justify="right", style="magenta", min_width=10)
715
+ table.add_column("Issues", justify="right", style="bright_white", min_width=8)
630
716
 
631
- def _should_retry_hooks(
632
- self,
633
- hook_type: str,
634
- attempt: int,
635
- max_retries: int,
636
- results: list[t.Any],
637
- ) -> bool:
638
- if hook_type == "fast" and attempt < max_retries - 1:
639
- if self._should_retry_fast_hooks(results):
640
- self.console.print(
641
- "[yellow]🔄[/ yellow] Fast hooks modified files, retrying all fast hooks...",
717
+ for result in results:
718
+ status_style = self._status_style(result.status)
719
+ # Passed hooks always show 0 issues (files processed != issues found)
720
+ if result.status == "passed":
721
+ issues_display = "0"
722
+ elif hasattr(result, "is_config_error") and result.is_config_error:
723
+ # Config/tool error - show simple symbol instead of misleading count
724
+ # Using "!" instead of emoji to avoid width issues in terminal
725
+ issues_display = "[yellow]![/yellow]"
726
+ else:
727
+ # For failed hooks with code violations, use issues_count
728
+ # IMPORTANT: Use issues_count directly, don't fall back to len(issues_found)
729
+ # because issues_found may contain display messages that aren't actual issues
730
+ issues_display = str(
731
+ result.issues_count if hasattr(result, "issues_count") else 0
642
732
  )
643
- return True
644
- return False
733
+ table.add_row(
734
+ self._strip_ansi(result.name),
735
+ f"[{status_style}]{result.status.upper()}[/{status_style}]",
736
+ f"{result.duration:.2f}s",
737
+ issues_display,
738
+ )
645
739
 
646
- def _attempt_autofix_for_fast_hooks(self, results: list[t.Any]) -> bool:
647
- try:
648
- self.logger.info("Attempting autofix for fast hook failures")
740
+ return table
741
+
742
+ def _format_issues(self, issues: list[str]) -> list[dict[str, str | int | None]]:
743
+ """Format hook issues into structured dictionaries."""
744
+
745
+ def _format_single_issue(issue):
746
+ if hasattr(issue, "file_path") and hasattr(issue, "line_number"):
747
+ return {
748
+ "file": str(getattr(issue, "file_path", "unknown")),
749
+ "line": getattr(issue, "line_number", 0),
750
+ "message": getattr(issue, "message", str(issue)),
751
+ "code": getattr(issue, "code", None),
752
+ "severity": getattr(issue, "severity", "warning"),
753
+ "suggestion": getattr(issue, "suggestion", None),
754
+ }
755
+
756
+ return {
757
+ "file": "unknown",
758
+ "line": 0,
759
+ "message": str(issue),
760
+ "code": None,
761
+ "severity": "warning",
762
+ "suggestion": None,
763
+ }
764
+
765
+ return [_format_single_issue(issue) for issue in issues]
766
+
767
+ def to_json(self, results: list[HookResult], suite_name: str = "") -> dict:
768
+ """Export hook results as structured JSON for automation.
769
+
770
+ Args:
771
+ results: List of HookResult objects to export
772
+ suite_name: Optional suite name (fast/comprehensive)
773
+
774
+ Returns:
775
+ Dictionary with structured results data
776
+
777
+ Example:
778
+ >>> json_data = coordinator.to_json(results, "comprehensive")
779
+ >>> print(json.dumps(json_data, indent=2))
780
+ """
781
+ return {
782
+ "suite": suite_name,
783
+ "summary": self._calculate_hook_statistics(results),
784
+ "hooks": [
785
+ {
786
+ "name": result.name,
787
+ "status": result.status,
788
+ "duration": round(result.duration, 2),
789
+ "issues_count": len(result.issues_found)
790
+ if result.issues_found
791
+ else 0,
792
+ "issues": self._format_issues(result.issues_found)
793
+ if result.issues_found
794
+ else [],
795
+ }
796
+ for result in results
797
+ ],
798
+ }
799
+
800
+ def _build_results_panel(
801
+ self, suite_name: str, table: Table, summary_text: str
802
+ ) -> Panel:
803
+ """Build Rich panel containing results table."""
804
+ return Panel(
805
+ table,
806
+ title=f"[bold]{suite_name.title()} Hook Results[/bold]",
807
+ subtitle=summary_text,
808
+ border_style="cyan" if suite_name == "fast" else "magenta",
809
+ padding=(0, 1),
810
+ width=get_console_width(),
811
+ expand=True,
812
+ )
649
813
 
650
- autofix_coordinator = self._lazy_autofix.get()
651
- fix_result: bool = autofix_coordinator.apply_fast_stage_fixes()
652
- return fix_result
653
- except Exception as e:
654
- self.logger.warning(f"Autofix attempt failed: {e}")
655
- return False
814
+ def _format_failing_hooks(
815
+ self, suite_name: str, results: list[HookResult]
816
+ ) -> list[HookResult]:
817
+ """Get list of failing hooks and print header.
818
+
819
+ Returns:
820
+ List of failing hook results
821
+ """
822
+ failing = [
823
+ result
824
+ for result in results
825
+ if result.status.lower() in {"failed", "error", "timeout"}
826
+ ]
827
+
828
+ if failing:
829
+ self.console.print(
830
+ f"[red]Details for failing {suite_name} hooks:[/red]", highlight=False
831
+ )
832
+
833
+ return failing
656
834
 
657
- def _handle_hook_failures(
835
+ def _display_issue_details(self, result: HookResult) -> None:
836
+ """Display specific issue details if found."""
837
+ if not result.issues_found:
838
+ return
839
+
840
+ for issue in result.issues_found:
841
+ self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
842
+
843
+ def _display_timeout_info(self, result: HookResult) -> None:
844
+ """Display timeout information."""
845
+ if result.is_timeout:
846
+ self.console.print(
847
+ " - Hook timed out during execution", highlight=False
848
+ )
849
+
850
+ def _display_exit_code_info(self, result: HookResult) -> None:
851
+ """Display exit code with helpful context."""
852
+ if result.exit_code is not None and result.exit_code != 0:
853
+ exit_msg = f"Exit code: {result.exit_code}"
854
+ # Add helpful context for common exit codes
855
+ if result.exit_code == 137:
856
+ exit_msg += " (killed - possibly timeout or out of memory)"
857
+ elif result.exit_code == 139:
858
+ exit_msg += " (segmentation fault)"
859
+ elif result.exit_code in {126, 127}:
860
+ exit_msg += " (command not found or not executable)"
861
+ self.console.print(f" - {exit_msg}", highlight=False)
862
+
863
+ def _display_error_message(self, result: HookResult) -> None:
864
+ """Display error message preview."""
865
+ if result.error_message:
866
+ # Show first line or first 200 chars of error
867
+ error_preview = result.error_message.split("\n")[0][:200]
868
+ self.console.print(f" - Error: {error_preview}", highlight=False)
869
+
870
+ def _display_generic_failure(self, result: HookResult) -> None:
871
+ """Display generic failure message if no specific details available."""
872
+ if not result.is_timeout and not result.exit_code and not result.error_message:
873
+ self.console.print(
874
+ " - Hook failed with no detailed error information",
875
+ highlight=False,
876
+ )
877
+
878
+ def _display_hook_failures(
658
879
  self,
659
- hook_type: str,
880
+ suite_name: str,
881
+ results: list[HookResult],
660
882
  options: OptionsProtocol,
661
- summary: dict[str, t.Any],
662
- results: list[t.Any],
663
- attempt: int,
664
- max_retries: int,
665
- ) -> bool:
666
- self.logger.debug(
667
- f"{hook_type} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
668
- )
883
+ ) -> None:
884
+ # Show detailed failures if --verbose or --ai-debug flag is set
885
+ if not (options.verbose or getattr(options, "ai_debug", False)):
886
+ return
669
887
 
888
+ failing = self._format_failing_hooks(suite_name, results)
889
+ if not failing:
890
+ return
891
+
892
+ # Process each failing hook
893
+ for result in failing:
894
+ self._print_single_hook_failure(result)
895
+
896
+ self.console.print()
897
+
898
+ def _print_single_hook_failure(self, result: HookResult) -> None:
899
+ """Print details of a single hook failure."""
670
900
  self.console.print(
671
- f"[red]❌[/ red] {hook_type.title()} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
901
+ f" - [red]{self._strip_ansi(result.name)}[/red] ({result.status})",
902
+ highlight=False,
672
903
  )
673
904
 
674
- if hook_type == "fast" and attempt < max_retries - 1:
675
- if self._attempt_autofix_for_fast_hooks(results):
676
- self.console.print(
677
- "[yellow]🔧[/ yellow] Applied autofixes for fast hooks, retrying...",
678
- )
679
- return True
905
+ if result.issues_found:
906
+ self._print_hook_issues(result)
907
+ else:
908
+ self._display_failure_reasons(result)
909
+
910
+ def _print_hook_issues(self, result: HookResult) -> None:
911
+ """Print issues found for a hook."""
912
+ # Type assertion: issues_found is never None after __post_init__
913
+ assert result.issues_found is not None
914
+ for issue in result.issues_found:
915
+ # Show the issue with consistent formatting
916
+ self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
917
+
918
+ def _display_failure_reasons(self, result: HookResult) -> None:
919
+ """Display reasons why a hook failed."""
920
+ self._display_timeout_info(result)
921
+ self._display_exit_code_info(result)
922
+ self._display_error_message(result)
923
+ self._display_generic_failure(result)
680
924
 
681
- if getattr(options, "verbose", False):
682
- self._display_verbose_hook_errors(results, hook_type)
925
+ def _display_cleaning_header(self) -> None:
926
+ sep = make_separator("-")
927
+ self.console.print("\n" + sep)
928
+ self.console.print("[bold bright_green]🧹 CLEANING[/bold bright_green]")
929
+ self.console.print(sep + "\n")
683
930
 
684
- detailed_error_msg = self._build_detailed_hook_error_message(results, summary)
931
+ def _execute_cleaning_process(self) -> bool:
932
+ py_files = list(self.pkg_path.rglob("*.py"))
933
+ if not py_files:
934
+ return self._handle_no_files_to_clean()
685
935
 
686
- self.session.fail_task(
687
- f"{hook_type}_hooks",
688
- detailed_error_msg,
689
- )
690
- return False
936
+ cleaned_files = self._clean_python_files(py_files)
937
+ self._report_cleaning_results(cleaned_files)
938
+ return True
691
939
 
692
- def _display_verbose_hook_errors(
693
- self, results: list[t.Any], hook_type: str
694
- ) -> None:
695
- self.console.print(
696
- f"\n[bold yellow]📋 Detailed {hook_type} hook errors: [/bold yellow]"
697
- )
940
+ def _handle_no_files_to_clean(self) -> bool:
941
+ self.console.print("No Python files found to clean")
942
+ self.session.complete_task("cleaning", "No files to clean")
943
+ return True
698
944
 
699
- for result in results:
700
- status = getattr(result, "status", "")
701
- if status not in ("failed", "error", "timeout"):
702
- continue
945
+ def _clean_python_files(self, files: list[Path]) -> list[str]:
946
+ cleaned_files = []
947
+ for file in files:
948
+ if self.code_cleaner.should_process_file(file):
949
+ result = self.code_cleaner.clean_file(file)
950
+ if result.success:
951
+ cleaned_files.append(str(file))
952
+ return cleaned_files
953
+
954
+ def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
955
+ if cleaned_files:
956
+ self.console.print(f"Cleaned {len(cleaned_files)} files")
957
+ self.session.complete_task(
958
+ "cleaning", f"Cleaned {len(cleaned_files)} files"
959
+ )
960
+ else:
961
+ self.console.print("No cleaning needed for any files")
962
+ self.session.complete_task("cleaning", "No cleaning needed")
703
963
 
704
- hook_name = getattr(result, "name", "unknown")
705
- issues = getattr(result, "issues_found", [])
964
+ @staticmethod
965
+ def _determine_version_type(options: OptionsProtocol) -> str | None:
966
+ return options.publish or options.all or options.bump
706
967
 
707
- self.console.print(f"\n[red]❌ {hook_name}[/red]")
968
+ def _execute_publishing_workflow(
969
+ self, options: OptionsProtocol, version_type: str
970
+ ) -> bool:
971
+ # Store reference to current HEAD to allow rollback if needed
972
+ original_head = (
973
+ self.git_service.get_current_commit_hash()
974
+ if hasattr(self.git_service, "get_current_commit_hash")
975
+ else None
976
+ )
708
977
 
709
- if issues:
710
- for issue in issues:
711
- if isinstance(issue, str) and issue.strip():
712
- cleaned_issue = issue.strip()
713
- self.console.print(f" {cleaned_issue}")
714
- else:
715
- self.console.print(f" Hook failed with exit code (status: {status})")
978
+ # STAGE 0: Pre-publish commit if needed
979
+ if not self._handle_pre_publish_commit(options):
980
+ return False
716
981
 
717
- def _build_detailed_hook_error_message(
718
- self, results: list[t.Any], summary: dict[str, t.Any]
719
- ) -> str:
720
- error_parts = [f"{summary['failed']} failed, {summary['errors']} errors"]
982
+ # STAGE 1: Version bump
983
+ new_version = self._perform_version_bump(version_type)
984
+ if not new_version:
985
+ return False
721
986
 
722
- failed_hooks = []
723
- for result in results:
724
- if hasattr(result, "failed") and result.failed:
725
- hook_name = getattr(result, "hook_id", "") or getattr(
726
- result, "name", "unknown"
727
- )
728
- failed_hooks.append(hook_name.lower())
987
+ # STAGE 2: Commit, tag, and push changes
988
+ current_commit_hash = self._commit_version_changes(new_version)
989
+ if not current_commit_hash:
990
+ return False
729
991
 
730
- if failed_hooks:
731
- error_parts.append(f"Failed hooks: {', '.join(failed_hooks)}")
992
+ # STAGE 3: Publish to PyPI
993
+ if not self._publish_to_pypi(
994
+ options, new_version, original_head, current_commit_hash
995
+ ):
996
+ return False
732
997
 
733
- return " | ".join(error_parts)
998
+ # Finalize publishing
999
+ self._finalize_publishing(options, new_version)
734
1000
 
735
- def _should_retry_fast_hooks(self, results: list[t.Any]) -> bool:
736
- for result in results:
737
- if hasattr(result, "failed") and result.failed:
738
- return True
1001
+ self.session.complete_task("publishing", f"Published version {new_version}")
1002
+ return True
739
1003
 
740
- status = getattr(result, "status", "")
741
- if status in ("failed", "error", "timeout"):
742
- return True
743
- return False
1004
+ def _handle_pre_publish_commit(self, options: OptionsProtocol) -> bool:
1005
+ """Handle committing existing changes before version bump if needed."""
1006
+ if not options.commit:
1007
+ return True
744
1008
 
745
- def _apply_retry_backoff(self, attempt: int) -> None:
746
- if attempt > 0:
747
- backoff_delay = 2 ** (attempt - 1)
748
- self.logger.debug(f"Applying exponential backoff: {backoff_delay}s")
749
- self.console.print(f"[dim]Waiting {backoff_delay}s before retry...[/ dim]")
750
- time.sleep(backoff_delay)
1009
+ existing_changes = self.git_service.get_changed_files()
1010
+ if not existing_changes:
1011
+ return True
751
1012
 
752
- def _handle_hook_success(self, hook_type: str, summary: dict[str, t.Any]) -> bool:
753
- self.logger.info(
754
- f"{hook_type} hooks passed: {summary['passed']} / {summary['total']}",
755
- )
1013
+ self._display_commit_push_header()
756
1014
  self.console.print(
757
- f"[green][/ green] {hook_type.title()} hooks passed: {summary['passed']} / {summary['total']}",
1015
+ "[cyan]ℹ️[/cyan] Committing existing changes before version bump..."
758
1016
  )
759
- self.session.complete_task(
760
- f"{hook_type}_hooks",
761
- f"{summary['passed']} / {summary['total']} passed",
1017
+ commit_message = self._get_commit_message(existing_changes, options)
1018
+ if not self._execute_commit_and_push(existing_changes, commit_message):
1019
+ self.session.fail_task("publishing", "Failed to commit pre-publish changes")
1020
+ return False
1021
+ self.console.print(
1022
+ "[green]✅[/green] Pre-publish changes committed and pushed\n"
762
1023
  )
763
1024
  return True
764
1025
 
765
- def _handle_hook_exception(self, hook_type: str, e: Exception) -> bool:
766
- self.console.print(f"[red]❌[/ red] {hook_type.title()} hooks error: {e}")
767
- self.session.fail_task(f"{hook_type}_hooks", str(e))
768
- return False
1026
+ def _perform_version_bump(self, version_type: str) -> str | None:
1027
+ """Perform the version bump operation."""
1028
+ self._display_version_bump_header()
769
1029
 
770
- async def _execute_hooks_with_parallel_support(
1030
+ new_version = self.publish_manager.bump_version(version_type)
1031
+ if not new_version:
1032
+ self.session.fail_task("publishing", "Version bumping failed")
1033
+ return None
1034
+
1035
+ self.console.print(f"[green]✅[/green] Version bumped to {new_version}")
1036
+ self.console.print(
1037
+ f"[green]✅[/green] Changelog updated for version {new_version}"
1038
+ )
1039
+ return new_version
1040
+
1041
+ def _commit_version_changes(self, new_version: str) -> str | None:
1042
+ """Commit the version changes to git."""
1043
+ self._display_commit_push_header()
1044
+
1045
+ # Stage changes
1046
+ changed_files = self.git_service.get_changed_files()
1047
+ if not changed_files:
1048
+ self.console.print("[yellow]⚠️[/yellow] No changes to stage")
1049
+ self.session.fail_task("publishing", "No changes to commit")
1050
+ return None
1051
+
1052
+ if not self.git_service.add_files(changed_files):
1053
+ self.session.fail_task("publishing", "Failed to stage files")
1054
+ return None
1055
+ self.console.print(f"[green]✅[/green] Staged {len(changed_files)} files")
1056
+
1057
+ # Commit
1058
+ commit_message = f"chore: bump version to {new_version}"
1059
+ if not self.git_service.commit(commit_message):
1060
+ self.session.fail_task("publishing", "Failed to commit changes")
1061
+ return None
1062
+ current_commit_hash = (
1063
+ self.git_service.get_current_commit_hash()
1064
+ if hasattr(self.git_service, "get_current_commit_hash")
1065
+ else None
1066
+ )
1067
+ return current_commit_hash
1068
+
1069
+ def _publish_to_pypi(
771
1070
  self,
772
- hook_type: str,
773
- hook_runner: t.Callable[[], list[t.Any]],
774
1071
  options: OptionsProtocol,
1072
+ new_version: str,
1073
+ original_head: str | None,
1074
+ current_commit_hash: str | None,
775
1075
  ) -> bool:
776
- self._initialize_hook_execution(hook_type)
1076
+ """Publish the package to PyPI."""
1077
+ self._display_publish_header()
1078
+
1079
+ # Build and publish package
1080
+ if not self.publish_manager.publish_package():
1081
+ self.session.fail_task("publishing", "Package publishing failed")
1082
+ # Attempt to rollback the version bump commit if publishing fails
1083
+ if current_commit_hash and original_head:
1084
+ self._attempt_rollback_version_bump(original_head, current_commit_hash)
1085
+ return False
1086
+ return True
1087
+
1088
+ def _finalize_publishing(self, options: OptionsProtocol, new_version: str) -> None:
1089
+ """Finalize the publishing process after successful PyPI publishing."""
1090
+ # Create git tag and push only after successful PyPI publishing
1091
+ if not options.no_git_tags:
1092
+ if not self.publish_manager.create_git_tag_local(new_version):
1093
+ self.console.print(
1094
+ f"[yellow]⚠️[/yellow] Failed to create git tag v{new_version}"
1095
+ )
1096
+
1097
+ # Push commit and tag together in single operation only after successful PyPI publishing
1098
+ if not self.git_service.push_with_tags():
1099
+ self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
1100
+ # Not failing the whole workflow for a push failure
777
1101
 
1102
+ def _attempt_rollback_version_bump(
1103
+ self, original_head: str, current_commit_hash: str
1104
+ ) -> bool:
1105
+ """Attempt to undo the version bump commit if publishing fails."""
778
1106
  try:
779
- return await self._process_parallel_hook_execution(
780
- hook_type, hook_runner, options
1107
+ self.console.print(
1108
+ "[yellow]🔄 Attempting to rollback version bump commit...[/yellow]"
781
1109
  )
782
1110
 
1111
+ # Reset to the original HEAD (before version bump commit)
1112
+ result = self.git_service.reset_hard(original_head)
1113
+
1114
+ if result:
1115
+ self.console.print(
1116
+ f"[green]✅ Version bump commit ({current_commit_hash[:8]}...) reverted[/green]"
1117
+ )
1118
+ return True
1119
+ else:
1120
+ self.console.print("[red]❌ Failed to revert version bump commit[/red]")
1121
+ return False
783
1122
  except Exception as e:
784
- return self._handle_hook_exception(hook_type, e)
1123
+ self.console.print(f"[red]❌ Error during version rollback: {e}[/red]")
1124
+ return False
785
1125
 
786
- async def _process_parallel_hook_execution(
787
- self,
788
- hook_type: str,
789
- hook_runner: t.Callable[[], list[t.Any]],
790
- options: OptionsProtocol,
791
- ) -> bool:
792
- results = hook_runner()
793
- summary = self.hook_manager.get_hook_summary(results)
1126
+ def _display_version_bump_header(self) -> None:
1127
+ sep = make_separator("-")
1128
+ self.console.print("\n" + sep)
1129
+ self.console.print("[bold bright_cyan]📝 VERSION BUMP[/bold bright_cyan]")
1130
+ self.console.print(sep + "\n")
794
1131
 
795
- if not self._has_hook_failures(summary):
796
- return self._handle_hook_success(hook_type, summary)
1132
+ def _display_commit_push_header(self) -> None:
1133
+ sep = make_separator("-")
1134
+ self.console.print("\n" + sep)
1135
+ self.console.print("[bold bright_blue]📦 COMMIT & PUSH[/bold bright_blue]")
1136
+ self.console.print(sep + "\n")
797
1137
 
798
- return self._handle_parallel_hook_failures(
799
- hook_type, hook_runner, options, results, summary
800
- )
1138
+ def _display_publish_header(self) -> None:
1139
+ sep = make_separator("-")
1140
+ self.console.print("\n" + sep)
1141
+ self.console.print("[bold bright_green]🚀 PUBLISH TO PYPI[/bold bright_green]")
1142
+ self.console.print(sep + "\n")
801
1143
 
802
- def _handle_parallel_hook_failures(
803
- self,
804
- hook_type: str,
805
- hook_runner: t.Callable[[], list[t.Any]],
806
- options: OptionsProtocol,
807
- results: list[t.Any],
808
- summary: dict[str, t.Any],
809
- ) -> bool:
810
- if hook_type != "fast":
811
- return self._handle_hook_failures(
812
- hook_type, options, summary, results, 0, 1
813
- )
1144
+ def _handle_no_changes_to_commit(self) -> bool:
1145
+ self.console.print("No changes to commit")
1146
+ self.session.complete_task("commit", "No changes to commit")
1147
+ return True
814
1148
 
815
- if not self._attempt_autofix_for_fast_hooks(results):
816
- return self._handle_hook_failures(
817
- hook_type, options, summary, results, 0, 1
818
- )
1149
+ def _get_commit_message(
1150
+ self, changed_files: list[str], options: OptionsProtocol
1151
+ ) -> str:
1152
+ suggestions = self.git_service.get_commit_message_suggestions(changed_files)
1153
+ if not suggestions:
1154
+ return "Update project files"
819
1155
 
820
- return self._retry_hooks_after_autofix(hook_type, hook_runner, options)
1156
+ if not options.interactive:
1157
+ return suggestions[0]
821
1158
 
822
- def _retry_hooks_after_autofix(
823
- self,
824
- hook_type: str,
825
- hook_runner: t.Callable[[], list[t.Any]],
826
- options: OptionsProtocol,
827
- ) -> bool:
828
- self.console.print(
829
- "[yellow]🔧[/ yellow] Applied autofixes for fast hooks, retrying..."
830
- )
1159
+ return self._interactive_commit_message_selection(suggestions)
1160
+
1161
+ def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
1162
+ self._display_commit_suggestions(suggestions)
1163
+ choice = self.console.input(
1164
+ "\nEnter number, custom message, or press Enter for default: "
1165
+ ).strip()
1166
+ return self._process_commit_choice(choice, suggestions)
831
1167
 
832
- results = hook_runner()
833
- summary = self.hook_manager.get_hook_summary(results)
1168
+ def _display_commit_suggestions(self, suggestions: list[str]) -> None:
1169
+ self.console.print("\n[bold]Commit message suggestions:[/bold]")
1170
+ for i, suggestion in enumerate(suggestions, 1):
1171
+ self.console.print(f" [cyan]{i}[/cyan]: {suggestion}")
834
1172
 
835
- if not self._has_hook_failures(summary):
836
- return self._handle_hook_success(hook_type, summary)
1173
+ @staticmethod
1174
+ def _process_commit_choice(choice: str, suggestions: list[str]) -> str:
1175
+ if not choice:
1176
+ return suggestions[0]
1177
+ if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
1178
+ return suggestions[int(choice) - 1]
1179
+ return choice
837
1180
 
838
- return self._handle_hook_failures(hook_type, options, summary, results, 0, 1)
1181
+ def _execute_commit_and_push(
1182
+ self, changed_files: list[str], commit_message: str
1183
+ ) -> bool:
1184
+ if not self.git_service.add_files(changed_files):
1185
+ self.session.fail_task("commit", "Failed to add files to git")
1186
+ return False
1187
+ if not self.git_service.commit(commit_message):
1188
+ self.session.fail_task("commit", "Failed to commit files")
1189
+ return False
1190
+ if not self.git_service.push():
1191
+ self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
1192
+ # Not failing the whole workflow for a push failure
1193
+ self.session.complete_task("commit", "Committed and pushed changes")
1194
+ return True
839
1195
 
840
- @property
841
- def autofix_coordinator(self) -> AutofixCoordinator:
842
- coordinator: AutofixCoordinator = self._lazy_autofix.get()
843
- return coordinator
1196
+ @staticmethod
1197
+ def _format_hook_summary(summary: dict[str, t.Any]) -> str:
1198
+ if not summary:
1199
+ return "No hooks executed"
1200
+
1201
+ total = summary.get("total", 0)
1202
+ passed = summary.get("passed", 0)
1203
+ failed = summary.get("failed", 0)
1204
+ errors = summary.get("errors", 0)
1205
+ duration = summary.get("total_duration", 0.0)
1206
+
1207
+ parts = [f"{passed}/{total} passed"]
1208
+ if failed:
1209
+ parts.append(f"{failed} failed")
1210
+ if errors:
1211
+ parts.append(f"{errors} errors")
1212
+
1213
+ summary_str = ", ".join(parts)
1214
+ return f"{summary_str} in {duration:.2f}s"
1215
+
1216
+ @staticmethod
1217
+ def _status_style(status: str) -> str:
1218
+ normalized = status.lower()
1219
+ if normalized == "passed":
1220
+ return "green"
1221
+ if normalized in {"failed", "error"}:
1222
+ return "red"
1223
+ if normalized == "timeout":
1224
+ return "yellow"
1225
+ return "bright_white"
1226
+
1227
+ # (All printing is handled by acb.console.Console which supports robust I/O.)