crackerjack 0.30.3__py3-none-any.whl → 0.31.7__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (156) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +227 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +170 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +657 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +409 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +585 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +826 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +433 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +443 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +114 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +621 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +372 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +217 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +565 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/coverage_improvement.py +223 -0
  107. crackerjack/orchestration/execution_strategies.py +341 -0
  108. crackerjack/orchestration/test_progress_streamer.py +636 -0
  109. crackerjack/plugins/__init__.py +15 -0
  110. crackerjack/plugins/base.py +200 -0
  111. crackerjack/plugins/hooks.py +246 -0
  112. crackerjack/plugins/loader.py +335 -0
  113. crackerjack/plugins/managers.py +259 -0
  114. crackerjack/py313.py +8 -3
  115. crackerjack/services/__init__.py +22 -0
  116. crackerjack/services/cache.py +314 -0
  117. crackerjack/services/config.py +358 -0
  118. crackerjack/services/config_integrity.py +99 -0
  119. crackerjack/services/contextual_ai_assistant.py +516 -0
  120. crackerjack/services/coverage_ratchet.py +356 -0
  121. crackerjack/services/debug.py +736 -0
  122. crackerjack/services/dependency_monitor.py +617 -0
  123. crackerjack/services/enhanced_filesystem.py +439 -0
  124. crackerjack/services/file_hasher.py +151 -0
  125. crackerjack/services/filesystem.py +421 -0
  126. crackerjack/services/git.py +176 -0
  127. crackerjack/services/health_metrics.py +611 -0
  128. crackerjack/services/initialization.py +873 -0
  129. crackerjack/services/log_manager.py +286 -0
  130. crackerjack/services/logging.py +174 -0
  131. crackerjack/services/metrics.py +578 -0
  132. crackerjack/services/pattern_cache.py +362 -0
  133. crackerjack/services/pattern_detector.py +515 -0
  134. crackerjack/services/performance_benchmarks.py +653 -0
  135. crackerjack/services/security.py +163 -0
  136. crackerjack/services/server_manager.py +234 -0
  137. crackerjack/services/smart_scheduling.py +144 -0
  138. crackerjack/services/tool_version_service.py +61 -0
  139. crackerjack/services/unified_config.py +437 -0
  140. crackerjack/services/version_checker.py +248 -0
  141. crackerjack/slash_commands/__init__.py +14 -0
  142. crackerjack/slash_commands/init.md +122 -0
  143. crackerjack/slash_commands/run.md +163 -0
  144. crackerjack/slash_commands/status.md +127 -0
  145. crackerjack-0.31.7.dist-info/METADATA +742 -0
  146. crackerjack-0.31.7.dist-info/RECORD +149 -0
  147. crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
  148. crackerjack/.gitignore +0 -34
  149. crackerjack/.libcst.codemod.yaml +0 -18
  150. crackerjack/.pdm.toml +0 -1
  151. crackerjack/crackerjack.py +0 -3805
  152. crackerjack/pyproject.toml +0 -286
  153. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  154. crackerjack-0.30.3.dist-info/RECORD +0 -16
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
  156. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,949 @@
1
+ import asyncio
2
+ import atexit
3
+ import signal
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ import time
8
+ from contextlib import suppress
9
+ from pathlib import Path
10
+
11
+ from rich.console import Console
12
+ from textual.app import App, ComposeResult
13
+ from textual.containers import Container
14
+ from textual.widget import Widget
15
+ from textual.widgets import DataTable, Footer, Label, ProgressBar
16
+
17
+ from .progress_components import (
18
+ ErrorCollector,
19
+ JobDataCollector,
20
+ ServiceHealthChecker,
21
+ ServiceManager,
22
+ TerminalRestorer,
23
+ )
24
+
25
+
26
+ class AgentStatusPanel(Widget):
27
+ def __init__(self, **kwargs) -> None:
28
+ super().__init__(**kwargs)
29
+ self.border_title = "🤖 AI Agents"
30
+ self.border_title_align = "left"
31
+
32
+ def compose(self) -> ComposeResult:
33
+ yield DataTable(id="agents-table")
34
+ yield Label("Coordinator: Loading...", id="coordinator-status")
35
+ yield Label("Stats: Loading...", id="agent-stats")
36
+
37
+ def on_mount(self) -> None:
38
+ with suppress(Exception):
39
+ agents_table = self.query_one("#agents-table", DataTable)
40
+ agents_table.add_columns(
41
+ "Agent",
42
+ "Status",
43
+ "Issue Type",
44
+ "Confidence",
45
+ "Time",
46
+ )
47
+
48
+ agents_table.styles.max_height = "8"
49
+
50
+ def update_agent_data(self, agent_data: dict) -> None:
51
+ with suppress(Exception):
52
+ self._update_coordinator_status(agent_data)
53
+ self._update_agents_table(agent_data)
54
+ self._update_stats(agent_data)
55
+
56
+ def _update_coordinator_status(self, data: dict) -> None:
57
+ with suppress(Exception):
58
+ activity = data.get("agent_activity", {})
59
+ registry = activity.get("agent_registry", {})
60
+ coordinator_status = activity.get("coordinator_status", "idle")
61
+ total_agents = registry.get("total_agents", 6)
62
+
63
+ status_emoji = "✅" if coordinator_status == "active" else "⏸️"
64
+ status_label = self.query_one("#coordinator-status", Label)
65
+ status_label.update(
66
+ f"Coordinator: {status_emoji} {coordinator_status.title()} ({total_agents} agents)",
67
+ )
68
+
69
+ def _update_agents_table(self, data: dict) -> None:
70
+ with suppress(Exception):
71
+ agents_table = self.query_one("#agents-table", DataTable)
72
+ agents_table.clear()
73
+
74
+ activity = data.get("agent_activity", {})
75
+ active_agents = activity.get("active_agents", [])
76
+
77
+ if not active_agents:
78
+ agents_table.add_row("No active agents", " - ", " - ", " - ", " - ")
79
+ return
80
+
81
+ for agent in active_agents:
82
+ agent_type = agent.get("agent_type", "Unknown")
83
+ status = agent.get("status", "unknown")
84
+ confidence = agent.get("confidence", 0)
85
+ processing_time = agent.get("processing_time", 0)
86
+
87
+ emoji = self._get_agent_emoji(agent_type)
88
+
89
+ current_issue = agent.get("current_issue", {})
90
+ issue_type = (
91
+ current_issue.get("type", " - ")
92
+ if current_issue
93
+ else agent.get("issue_type", " - ")
94
+ )
95
+
96
+ status_display = f"{self._get_status_emoji(status)} {status.title()}"
97
+
98
+ agents_table.add_row(
99
+ f"{emoji} {agent_type}",
100
+ status_display,
101
+ issue_type,
102
+ f"{confidence: .0 % }" if confidence > 0 else " - ",
103
+ f"{processing_time: .1f}s" if processing_time > 0 else " - ",
104
+ )
105
+
106
+ def _update_stats(self, data: dict) -> None:
107
+ with suppress(Exception):
108
+ performance = data.get("agent_performance", {})
109
+ total_issues = performance.get("total_issues_processed", 0)
110
+ success_rate = performance.get("success_rate", 0)
111
+ avg_time = performance.get("average_processing_time", 0)
112
+ cache_hits = performance.get("cache_hits", 0)
113
+
114
+ stats_label = self.query_one("#agent-stats", Label)
115
+ stats_text = f"Stats: {total_issues} issues | {success_rate: .0 % } success"
116
+ if avg_time > 0:
117
+ stats_text += f" | {avg_time: .1f}s avg"
118
+ if cache_hits > 0:
119
+ stats_text += f" | {cache_hits} cached"
120
+
121
+ if success_rate >= 80:
122
+ stats_text += " ↑🟢"
123
+ elif success_rate >= 60:
124
+ stats_text += " 🟡"
125
+ elif total_issues > 0:
126
+ stats_text += " ↓🔴"
127
+
128
+ stats_label.update(stats_text)
129
+
130
+ def _get_agent_emoji(self, agent_type: str) -> str:
131
+ emojis = {
132
+ "FormattingAgent": "🎨",
133
+ "SecurityAgent": "🔒",
134
+ "TestSpecialistAgent": "🧪",
135
+ "TestCreationAgent": "➕",
136
+ "RefactoringAgent": "🔧",
137
+ "ImportOptimizationAgent": "📦",
138
+ }
139
+ return emojis.get(agent_type) or "🤖"
140
+
141
+ def _get_status_emoji(self, status: str) -> str:
142
+ emojis = {
143
+ "evaluating": "🔍",
144
+ "processing": "⏳",
145
+ "completed": "✅",
146
+ "failed": "❌",
147
+ "idle": "⏸️",
148
+ }
149
+ return emojis.get(status.lower()) or "❓"
150
+
151
+
152
+ class JobPanel(Widget):
153
+ def __init__(self, job_data: dict, **kwargs) -> None:
154
+ super().__init__(**kwargs)
155
+ self.job_data = job_data
156
+ self.completion_time = None
157
+ self.iteration_count = job_data.get("iteration", 0)
158
+ self.max_iterations = job_data.get("max_iterations", 10)
159
+ self.fade_timer = None
160
+ self.remove_timer = None
161
+ self.fade_level = 0
162
+ self.border_style = self._calculate_border_style()
163
+
164
+ def _calculate_border_style(self) -> str:
165
+ status = self.job_data.get("status", "unknown").lower()
166
+
167
+ if status == "completed":
168
+ errors = self.job_data.get("errors", [])
169
+ hook_failures = self.job_data.get("hook_failures", [])
170
+ test_failures = self.job_data.get("test_failures", [])
171
+ total_failures = (
172
+ len(hook_failures)
173
+ + len(test_failures)
174
+ + len([e for e in errors if "failed" in str(e).lower()])
175
+ )
176
+
177
+ if total_failures == 0:
178
+ return "round green"
179
+ return "round red"
180
+ if status == "failed" or self.iteration_count >= 10:
181
+ return "round red"
182
+ if status == "running":
183
+ return "round blue"
184
+ return "round white"
185
+
186
+ def on_mount(self) -> None:
187
+ project_name = self.job_data.get("project", "crackerjack")
188
+ status = self.job_data.get("status", "").lower()
189
+ if status == "running":
190
+ self.border_title = f"📁 {project_name}"
191
+ self.border_subtitle = "💓"
192
+ self.border_subtitle_align = "right"
193
+ else:
194
+ self.border_title = f"📁 {project_name}"
195
+ self.border_title_align = "left"
196
+
197
+ self._setup_errors_table()
198
+
199
+ self._update_progress_bar()
200
+
201
+ status = self.job_data.get("status", "").lower()
202
+ if status in ("completed", "failed") and self.completion_time is None:
203
+ self.completion_time = time.time()
204
+
205
+ self.fade_timer = self.set_timer(300.0, self._start_fade)
206
+
207
+ self.remove_timer = self.set_timer(1200.0, self._remove_panel)
208
+
209
+ def _setup_errors_table(self) -> None:
210
+ with suppress(Exception):
211
+ errors_container = self.query_one(".job-errors")
212
+ errors_container.border_title = "❌ Errors"
213
+
214
+ errors_table = self.query_one(
215
+ f"#job-errors-{self.job_data.get('job_id', 'unknown')}",
216
+ DataTable,
217
+ )
218
+ errors_table.add_columns("", "", "", "")
219
+
220
+ self._update_errors_table()
221
+
222
+ def _update_errors_table(self) -> None:
223
+ with suppress(Exception):
224
+ errors_table = self.query_one(
225
+ f"#job-errors-{self.job_data.get('job_id', 'unknown')}",
226
+ DataTable,
227
+ )
228
+ errors_table.clear()
229
+
230
+ total_errors = self.job_data.get("total_issues", 0)
231
+ fixed_errors = self.job_data.get("errors_fixed", 0)
232
+
233
+ remaining_errors = max(0, total_errors - fixed_errors)
234
+
235
+ progress_pct = 0
236
+ if total_errors > 0:
237
+ progress_pct = int((fixed_errors / total_errors) * 100)
238
+
239
+ if total_errors == 0 and "errors" in self.job_data:
240
+ errors = self.job_data.get("errors", [])
241
+ hook_failures = self.job_data.get("hook_failures", [])
242
+ test_failures = self.job_data.get("test_failures", [])
243
+ total_errors = len(errors) + len(hook_failures) + len(test_failures)
244
+ failed_errors = (
245
+ len(hook_failures)
246
+ + len(test_failures)
247
+ + len([e for e in errors if "failed" in str(e).lower()])
248
+ )
249
+ fixed_errors = max(0, total_errors - failed_errors)
250
+ remaining_errors = failed_errors
251
+ if total_errors > 0:
252
+ progress_pct = int((fixed_errors / total_errors) * 100)
253
+
254
+ discovered_label = "🔍 Found"
255
+ discovered_value = f"{total_errors: > 15}"
256
+ resolved_label = "✅ Fixed"
257
+ resolved_value = f"{fixed_errors: > 15}"
258
+
259
+ remaining_label = "❌ Left"
260
+ remaining_value = f"{remaining_errors: > 15}"
261
+ progress_label = "📈 Done"
262
+ progress_value = f"{progress_pct} % ".rjust(15)
263
+
264
+ errors_table.add_rows(
265
+ [
266
+ (
267
+ discovered_label,
268
+ discovered_value,
269
+ resolved_label,
270
+ resolved_value,
271
+ ),
272
+ (remaining_label, remaining_value, progress_label, progress_value),
273
+ ],
274
+ )
275
+
276
+ def _update_progress_bar(self) -> None:
277
+ with suppress(Exception):
278
+ progress_bar = self.query_one(
279
+ f"#job-progress-{self.job_data.get('job_id', 'unknown')}",
280
+ ProgressBar,
281
+ )
282
+ progress_value = self.iteration_count / max(self.max_iterations, 1) * 100
283
+ progress_bar.update(progress=progress_value)
284
+
285
+ def _start_fade(self) -> None:
286
+ self.fade_level += 1
287
+
288
+ if self.fade_level == 1:
289
+ self.add_class("fade-1")
290
+ elif self.fade_level == 2:
291
+ self.add_class("fade-2")
292
+ elif self.fade_level == 3:
293
+ self.add_class("fade-3")
294
+ elif self.fade_level >= 4:
295
+ self.add_class("fade-4")
296
+
297
+ if self.fade_level < 4:
298
+ self.fade_timer = self.set_timer(300.0, self._start_fade)
299
+
300
+ def _remove_panel(self) -> None:
301
+ if hasattr(self.app, "completed_jobs_stats"):
302
+ job_id = self.job_data.get("job_id")
303
+
304
+ total_errors = self.job_data.get("total_issues", 0)
305
+ fixed_errors = self.job_data.get("errors_fixed", 0)
306
+ remaining_errors = max(0, total_errors - fixed_errors)
307
+
308
+ if total_errors == 0 and "errors" in self.job_data:
309
+ errors = self.job_data.get("errors", [])
310
+ hook_failures = self.job_data.get("hook_failures", [])
311
+ test_failures = self.job_data.get("test_failures", [])
312
+
313
+ total_errors = len(errors) + len(hook_failures) + len(test_failures)
314
+ failed_errors = (
315
+ len(hook_failures)
316
+ + len(test_failures)
317
+ + len([e for e in errors if "failed" in str(e).lower()])
318
+ )
319
+ fixed_errors = max(0, total_errors - failed_errors)
320
+ remaining_errors = failed_errors
321
+
322
+ self.app.completed_jobs_stats[job_id] = {
323
+ "status": self.job_data.get("status", "unknown"),
324
+ "total_errors": total_errors,
325
+ "fixed_errors": fixed_errors,
326
+ "remaining_errors": remaining_errors,
327
+ "completion_time": self.completion_time,
328
+ }
329
+
330
+ if hasattr(self.app, "active_jobs"):
331
+ job_id = self.job_data.get("job_id")
332
+ if job_id in self.app.active_jobs:
333
+ del self.app.active_jobs[job_id]
334
+ self.remove()
335
+
336
+ def compose(self) -> ComposeResult:
337
+ with Container(classes="job-panel"):
338
+ yield from self._compose_status_column()
339
+ yield from self._compose_errors_column()
340
+ yield from self._compose_mcp_message()
341
+
342
+ def _compose_mcp_message(self) -> ComposeResult:
343
+ mcp_message = self.job_data.get("message", "Processing...")
344
+ yield Label(f"💬 {mcp_message}", classes="mcp-message")
345
+
346
+ def _compose_status_column(self) -> ComposeResult:
347
+ with Container(classes="job-status"):
348
+ yield from self._compose_job_identifiers()
349
+ yield from self._compose_progress_info()
350
+ yield from self._compose_stage_and_status()
351
+ yield from self._compose_agent_info()
352
+ yield from self._compose_warning_messages()
353
+
354
+ def _compose_job_identifiers(self) -> ComposeResult:
355
+ job_id = self.job_data.get(
356
+ "full_job_id",
357
+ self.job_data.get("job_id", "Unknown"),
358
+ )
359
+ yield Label(f"🆔 UUID: {job_id}")
360
+
361
+ def _compose_progress_info(self) -> ComposeResult:
362
+ progress_stage = f"{self.iteration_count} / {self.max_iterations}"
363
+ yield Label(f"📊 Progress: {progress_stage}")
364
+ yield ProgressBar(
365
+ total=100,
366
+ show_eta=False,
367
+ show_percentage=False,
368
+ id=f"job-progress-{self.job_data.get('job_id', 'unknown')}",
369
+ )
370
+
371
+ def _compose_stage_and_status(self) -> ComposeResult:
372
+ yield Label(f"🎯 Stage: {self.job_data.get('stage', 'Unknown')}")
373
+ yield Label(f"📝 Status: {self.job_data.get('status', 'Unknown')}")
374
+
375
+ def _compose_agent_info(self) -> ComposeResult:
376
+ agent_summary = self.job_data.get("agent_summary", {})
377
+ if not agent_summary:
378
+ return
379
+
380
+ active_count = agent_summary.get("active_count", 0)
381
+ cached_fixes = agent_summary.get("cached_fixes", 0)
382
+
383
+ if active_count > 0 or cached_fixes > 0:
384
+ agent_text = f"🤖 Agents: {active_count} active"
385
+ if cached_fixes > 0:
386
+ agent_text += f", {cached_fixes} cached"
387
+
388
+ agents_data = agent_summary.get("agents", [])
389
+ if agents_data:
390
+ avg_confidence = sum(
391
+ agent.get("confidence", 0) for agent in agents_data
392
+ ) / max(len(agents_data), 1)
393
+ if avg_confidence > 0:
394
+ agent_text += f", {avg_confidence: .0 % } conf"
395
+ yield Label(agent_text)
396
+
397
+ def _compose_warning_messages(self) -> ComposeResult:
398
+ if self.iteration_count >= 10:
399
+ yield Label("⚠️ Max iterations reached")
400
+
401
+ def _compose_errors_column(self) -> ComposeResult:
402
+ with Container(classes="job-errors"):
403
+ yield DataTable(
404
+ id=f"job-errors-{self.job_data.get('job_id', 'unknown')}",
405
+ )
406
+
407
+
408
+ class CrackerjackDashboard(App):
409
+ ENABLE_COMMAND_PALETTE = False
410
+ CSS_PATH = Path(__file__).parent / "progress_monitor.tcss"
411
+
412
+ BINDINGS = [
413
+ ("q", "quit", "Quit"),
414
+ ]
415
+
416
+ def __init__(self) -> None:
417
+ super().__init__()
418
+ self.progress_dir = Path(tempfile.gettempdir()) / "crackerjack-mcp-progress"
419
+ self.websocket_url = "ws://localhost:8675"
420
+ self.refresh_timer = None
421
+ self.active_jobs = {}
422
+ self.completed_jobs_stats = {}
423
+ self.current_polling_method = "File"
424
+
425
+ self.job_collector = JobDataCollector(self.progress_dir, self.websocket_url)
426
+ self.service_checker = ServiceHealthChecker()
427
+ self.error_collector = ErrorCollector()
428
+ self.service_manager = ServiceManager()
429
+ self.terminal_restorer = TerminalRestorer()
430
+
431
+ def compose(self) -> ComposeResult:
432
+ with Container(id="main-container"):
433
+ yield from self._compose_top_section()
434
+ yield from self._compose_discovery_section()
435
+ yield Footer()
436
+
437
+ def _compose_top_section(self) -> ComposeResult:
438
+ with Container(id="top-section"):
439
+ yield from self._compose_left_column()
440
+ yield from self._compose_right_column()
441
+
442
+ def _compose_left_column(self) -> ComposeResult:
443
+ with Container(id="left-column"):
444
+ yield from self._compose_jobs_panel()
445
+ yield AgentStatusPanel(id="agent-status-panel")
446
+
447
+ def _compose_right_column(self) -> ComposeResult:
448
+ with Container(id="right-column"):
449
+ yield from self._compose_errors_panel()
450
+ yield from self._compose_services_panel()
451
+
452
+ def _compose_jobs_panel(self) -> ComposeResult:
453
+ with Container(id="jobs-panel"):
454
+ yield DataTable(
455
+ id="jobs-table",
456
+ )
457
+
458
+ def _compose_errors_panel(self) -> ComposeResult:
459
+ with Container(id="errors-panel"):
460
+ yield DataTable(
461
+ id="errors-table",
462
+ )
463
+
464
+ def _compose_services_panel(self) -> ComposeResult:
465
+ with Container(id="services-panel"):
466
+ yield DataTable(
467
+ id="services-table",
468
+ zebra_stripes=True,
469
+ )
470
+
471
+ def _compose_discovery_section(self) -> ComposeResult:
472
+ with Container(id="discovery-section"):
473
+ yield Container(id="job-discovery-container")
474
+
475
+ def on_mount(self) -> None:
476
+ self._setup_border_titles()
477
+ self._setup_datatables()
478
+
479
+ asyncio.create_task(self._ensure_services_running())
480
+ self._start_refresh_timer()
481
+
482
+ atexit.register(self._restore_terminal_fallback)
483
+
484
+ signal.signal(signal.SIGINT, self._signal_handler)
485
+ signal.signal(signal.SIGTERM, self._signal_handler)
486
+
487
+ def on_unmount(self) -> None:
488
+ with suppress(Exception):
489
+ self._cleanup_started_services()
490
+
491
+ def _setup_border_titles(self) -> None:
492
+ self.query_one("#top-section").border_title = "🚀 Crackerjack Dashboard"
493
+ self.query_one("#jobs-panel").border_title = "📊 Issue Metrics"
494
+ self.query_one("#errors-panel").border_title = "❌ Error Tracking"
495
+ self.query_one("#services-panel").border_title = "🔧 Service Health"
496
+ self.query_one("#discovery-section").border_title = "🔍 Active Jobs"
497
+
498
+ def _setup_datatables(self) -> None:
499
+ jobs_table = self.query_one("#jobs-table", DataTable)
500
+ jobs_table.add_columns("", "", "", "")
501
+
502
+ services_table = self.query_one("#services-table", DataTable)
503
+ services_table.add_columns("Service", "Status", "Restarts")
504
+
505
+ errors_table = self.query_one("#errors-table", DataTable)
506
+ errors_table.add_columns("", "", "", "")
507
+
508
+ self._show_default_values()
509
+
510
+ asyncio.create_task(self._refresh_data())
511
+
512
+ def _show_default_values(self) -> None:
513
+ default_jobs_data = {
514
+ "active": 0,
515
+ "completed": 0,
516
+ "failed": 0,
517
+ "total": 0,
518
+ "individual_jobs": [],
519
+ }
520
+ self._update_jobs_table(default_jobs_data)
521
+
522
+ self._update_errors_table([])
523
+
524
+ async def _ensure_services_running(self) -> None:
525
+ await self.service_manager.ensure_services_running()
526
+
527
+ def _start_refresh_timer(self) -> None:
528
+ self.refresh_timer = self.set_interval(0.5, self._refresh_data)
529
+
530
+ async def _refresh_data(self) -> None:
531
+ with suppress(Exception):
532
+ if hasattr(self, "_refresh_counter"):
533
+ self._refresh_counter += 1
534
+ else:
535
+ self._refresh_counter = 0
536
+
537
+ if self._refresh_counter % 10 == 0:
538
+ await self._ensure_services_running()
539
+
540
+ jobs_data = await self._discover_jobs()
541
+
542
+ services_data = await self._collect_services_data()
543
+
544
+ errors_data = await self._collect_recent_errors()
545
+
546
+ if jobs_data["individual_jobs"]:
547
+ jobs_data["individual_jobs"][0].get("project", "crackerjack")
548
+
549
+ self.query_one("#services-panel").border_title = "🔧 Services"
550
+
551
+ self._update_jobs_table(jobs_data)
552
+ self._update_services_table(services_data)
553
+ self._update_errors_table(errors_data)
554
+ self._update_job_panels(jobs_data)
555
+ self._update_agent_panel(jobs_data)
556
+ self._update_status_bars(jobs_data)
557
+
558
+ async def _discover_jobs(self) -> dict:
559
+ result = await self.job_collector.discover_jobs()
560
+ self.current_polling_method = result["method"]
561
+ return result["data"]
562
+
563
+ async def _collect_services_data(self) -> list:
564
+ return await self.service_checker.collect_services_data()
565
+
566
+ async def _collect_recent_errors(self) -> list:
567
+ return await self.error_collector.collect_recent_errors()
568
+
569
+ def _update_jobs_table(self, jobs_data: dict) -> None:
570
+ with suppress(Exception):
571
+ jobs_table = self.query_one("#jobs-table", DataTable)
572
+ jobs_table.clear()
573
+
574
+ total_issues = jobs_data.get("total_issues", 0)
575
+ errors_fixed = jobs_data.get("errors_fixed", 0)
576
+ errors_failed = jobs_data.get("errors_failed", 0)
577
+ jobs_data.get("current_errors", 0)
578
+
579
+ for job_stats in self.completed_jobs_stats.values():
580
+ total_issues += job_stats.get("total_issues", 0)
581
+ errors_fixed += job_stats.get("errors_fixed", 0)
582
+ errors_failed += job_stats.get("errors_failed", 0)
583
+
584
+ remaining_errors = max(0, total_issues - errors_fixed)
585
+
586
+ discovered_label = "🔍 Found"
587
+ discovered_value = f"{total_issues: > 12}"
588
+ resolved_label = "✅ Fixed"
589
+ resolved_value = f"{errors_fixed: > 12}"
590
+
591
+ remaining_label = "❌ Left"
592
+ remaining_value = f"{remaining_errors: > 12}"
593
+ progress_label = "📈 Done"
594
+ progress_pct = (
595
+ int(errors_fixed / total_issues * 100) if total_issues > 0 else 0
596
+ )
597
+ progress_value = f"{progress_pct} % ".rjust(12)
598
+
599
+ jobs_table.add_rows(
600
+ [
601
+ (
602
+ discovered_label,
603
+ discovered_value,
604
+ resolved_label,
605
+ resolved_value,
606
+ ),
607
+ (remaining_label, remaining_value, progress_label, progress_value),
608
+ ],
609
+ )
610
+
611
+ def _update_services_table(self, services_data: list) -> None:
612
+ with suppress(Exception):
613
+ services_table = self.query_one("#services-table", DataTable)
614
+ services_table.clear()
615
+
616
+ for service in services_data:
617
+ service_name = (
618
+ service[0]
619
+ .replace("WebSocket Server", "WebSocket")
620
+ .replace(" Server", "")
621
+ )
622
+
623
+ status_text = service[1] or "❓ Unknown"
624
+ restart_count = service[2] if len(service) > 2 else "0"
625
+ restart_value = f"{restart_count: ^ 8}"
626
+ services_table.add_row(service_name, status_text, restart_value)
627
+
628
+ method_emoji = "🌐" if self.current_polling_method == "WebSocket" else "📁"
629
+ polling_status = f"{method_emoji} {self.current_polling_method}"
630
+
631
+ if self.current_polling_method == "WebSocket":
632
+ polling_status += " 🟢"
633
+ services_table.add_row("Polling", polling_status, "")
634
+
635
+ def _update_errors_table(self, errors_data: list) -> None:
636
+ with suppress(Exception):
637
+ errors_table = self.query_one("#errors-table", DataTable)
638
+ errors_table.clear()
639
+
640
+ job_errors = (
641
+ [
642
+ e
643
+ for e in errors_data
644
+ if "crackerjack" in str(e).lower() and "job" in str(e).lower()
645
+ ]
646
+ if errors_data
647
+ else []
648
+ )
649
+
650
+ active_errors = 0
651
+ fixed_errors = 0
652
+ total_errors = 0
653
+
654
+ if job_errors:
655
+ total_errors = len(job_errors)
656
+ active_errors = sum(
657
+ 1
658
+ for e in job_errors
659
+ if "running" in str(e).lower() or "active" in str(e).lower()
660
+ )
661
+ sum(
662
+ 1
663
+ for e in job_errors
664
+ if "failed" in str(e).lower() or "error" in str(e).lower()
665
+ )
666
+ fixed_errors = sum(
667
+ 1
668
+ for e in job_errors
669
+ if "fixed" in str(e).lower() or "resolved" in str(e).lower()
670
+ )
671
+
672
+ discovered_label = "🔍 Found"
673
+ discovered_value = f"{total_errors: > 12}"
674
+ resolved_label = "✅ Fixed"
675
+ resolved_value = f"{fixed_errors: > 12}"
676
+
677
+ remaining_label = "❌ Left"
678
+ remaining_value = f"{active_errors: > 12}"
679
+ progress_label = "📈 Done"
680
+ progress_pct = (
681
+ int(fixed_errors / total_errors * 100) if total_errors > 0 else 0
682
+ )
683
+ progress_value = f"{progress_pct} % ".rjust(12)
684
+
685
+ errors_table.add_rows(
686
+ [
687
+ (
688
+ discovered_label,
689
+ discovered_value,
690
+ resolved_label,
691
+ resolved_value,
692
+ ),
693
+ (remaining_label, remaining_value, progress_label, progress_value),
694
+ ],
695
+ )
696
+
697
+ def _update_agent_panel(self, jobs_data: dict) -> None:
698
+ with suppress(Exception):
699
+ agent_panel = self.query_one("#agent-status-panel", AgentStatusPanel)
700
+
701
+ agent_data = {}
702
+ for job in jobs_data.get("individual_jobs", []):
703
+ if "agent_activity" in job or "agent_performance" in job:
704
+ agent_data = job
705
+ break
706
+
707
+ if agent_data:
708
+ agent_panel.update_agent_data(agent_data)
709
+
710
+ def _update_job_panels(self, jobs_data: dict) -> None:
711
+ with suppress(Exception):
712
+ container = self.query_one("#job-discovery-container")
713
+ current_job_ids = self._get_current_job_ids(jobs_data)
714
+
715
+ self._remove_obsolete_panels(current_job_ids)
716
+ self._update_or_create_panels(jobs_data, container)
717
+ self._handle_placeholder_visibility(container)
718
+
719
+ def _get_current_job_ids(self, jobs_data: dict) -> set:
720
+ return (
721
+ {job["job_id"] for job in jobs_data["individual_jobs"]}
722
+ if jobs_data["individual_jobs"]
723
+ else set()
724
+ )
725
+
726
+ def _remove_obsolete_panels(self, current_job_ids: set) -> None:
727
+ jobs_to_remove = []
728
+ for job_id, panel in self.active_jobs.items():
729
+ panel_status = panel.job_data.get("status", "").lower()
730
+ if (
731
+ job_id not in current_job_ids
732
+ and panel_status not in ("completed", "failed")
733
+ and panel.completion_time is None
734
+ ):
735
+ jobs_to_remove.append(job_id)
736
+
737
+ for job_id in jobs_to_remove:
738
+ panel = self.active_jobs.pop(job_id)
739
+ panel.remove()
740
+
741
+ def _update_or_create_panels(self, jobs_data: dict, container) -> None:
742
+ if not jobs_data["individual_jobs"]:
743
+ return
744
+
745
+ for job in jobs_data["individual_jobs"]:
746
+ job_id = job["job_id"]
747
+ if job_id in self.active_jobs:
748
+ self._update_existing_panel(job)
749
+ else:
750
+ self._create_new_panel(job, container)
751
+
752
+ def _update_existing_panel(self, job: dict) -> None:
753
+ existing_panel = self.active_jobs[job["job_id"]]
754
+ existing_panel.job_data = job
755
+ existing_panel.iteration_count = job.get("iteration", 0)
756
+
757
+ self._update_panel_title(existing_panel, job)
758
+ existing_panel._update_errors_table()
759
+ existing_panel._update_progress_bar()
760
+ self._handle_job_completion(existing_panel, job)
761
+ self._update_panel_border(existing_panel)
762
+
763
+ def _update_panel_title(self, panel, job: dict) -> None:
764
+ project_name = job.get("project", "crackerjack")
765
+ status = job.get("status", "").lower()
766
+
767
+ panel.border_title = f"📁 {project_name}"
768
+ panel.border_title_align = "left"
769
+
770
+ if status == "running":
771
+ panel.border_subtitle = "💓"
772
+ panel.border_subtitle_align = "right"
773
+ else:
774
+ panel.border_subtitle = ""
775
+
776
+ def _handle_job_completion(self, panel, job: dict) -> None:
777
+ job_status = job.get("status", "").lower()
778
+ if job_status in ("completed", "failed") and panel.completion_time is None:
779
+ panel.completion_time = time.time()
780
+ panel.fade_timer = panel.set_timer(300.0, panel._start_fade)
781
+ panel.remove_timer = panel.set_timer(1200.0, panel._remove_panel)
782
+
783
+ def _update_panel_border(self, panel) -> None:
784
+ new_border = panel._calculate_border_style()
785
+ if new_border != panel.border_style:
786
+ panel.border_style = new_border
787
+ panel.refresh()
788
+
789
+ def _create_new_panel(self, job: dict, container) -> None:
790
+ job_panel = JobPanel(job)
791
+ self.active_jobs[job["job_id"]] = job_panel
792
+ container.mount(job_panel)
793
+
794
+ def _handle_placeholder_visibility(self, container) -> None:
795
+ has_placeholder = bool(container.query("#no-jobs-label"))
796
+
797
+ if not self.active_jobs and not has_placeholder:
798
+ container.mount(
799
+ Label(
800
+ "No active jobs detected. Start a Crackerjack job to see progress here.",
801
+ id="no-jobs-label",
802
+ ),
803
+ )
804
+ elif self.active_jobs and has_placeholder:
805
+ container.query("#no-jobs-label").remove()
806
+
807
+ def _update_status_bars(self, jobs_data: dict) -> None:
808
+ pass
809
+
810
+ def action_refresh(self) -> None:
811
+ asyncio.create_task(self._refresh_data())
812
+
813
+ def action_clear(self) -> None:
814
+ with suppress(Exception):
815
+ for table_id in ("#jobs-table", "#services-table", "#errors-table"):
816
+ table = self.query_one(table_id, DataTable)
817
+ table.clear()
818
+
819
+ container = self.query_one("#job-discovery-container")
820
+ container.query("JobPanel").remove()
821
+ container.query("Label").remove()
822
+ self.active_jobs.clear()
823
+
824
+ def action_quit(self) -> None:
825
+ with suppress(Exception):
826
+ if self.refresh_timer:
827
+ self.refresh_timer.stop()
828
+ self._cleanup_started_services()
829
+ self._restore_terminal()
830
+ self.exit()
831
+
832
+ def _restore_terminal(self) -> None:
833
+ self.terminal_restorer.restore_terminal()
834
+
835
+ def _restore_terminal_fallback(self) -> None:
836
+ self.terminal_restorer.restore_terminal()
837
+
838
+ def _signal_handler(self, _signum, _frame) -> None:
839
+ with suppress(Exception):
840
+ self._restore_terminal()
841
+ self._cleanup_started_services()
842
+ sys.exit(0)
843
+
844
+ def _cleanup_started_services(self) -> None:
845
+ self.service_manager.cleanup_services()
846
+
847
+ def _format_time_metric(self, seconds: float) -> str:
848
+ if seconds < 60:
849
+ return f"{seconds: .0f}s"
850
+ if seconds < 3600:
851
+ return f"{seconds / 60: .0f}m {seconds % 60: .0f}s"
852
+ return f"{seconds / 3600: .0f}h {(seconds % 3600) / 60: .0f}m"
853
+
854
+ def _format_metric_with_trend(self, value: int, trend: str = "") -> str:
855
+ formatted = f"{value: , }"
856
+ if trend:
857
+ formatted += f" {trend}"
858
+ return formatted
859
+
860
+
861
+ class JobMetrics:
862
+ def __init__(self, job_id: str, project_path: str = "") -> None:
863
+ self.job_id = job_id
864
+ self.project_path = project_path
865
+ self.project_name = Path(project_path).name if project_path else "crackerjack"
866
+ self.start_time = time.time()
867
+ self.last_update = time.time()
868
+ self.completion_time: float | None = None
869
+
870
+ self.iteration = 0
871
+ self.max_iterations = 10
872
+ self.current_stage = "Initializing"
873
+ self.status = "running"
874
+ self.message = ""
875
+
876
+ self.stages_completed = set()
877
+ self.stages_failed = set()
878
+
879
+ self.errors = []
880
+ self.warnings = []
881
+ self.hook_failures = []
882
+ self.test_failures = []
883
+
884
+
885
+ async def run_progress_monitor(
886
+ enable_watchdog: bool = True,
887
+ dev_mode: bool = False,
888
+ ) -> None:
889
+ with suppress(Exception):
890
+ console = Console()
891
+ console.print(
892
+ "[bold green]🚀 Starting Crackerjack Progress Monitor[/bold green]",
893
+ )
894
+
895
+ if enable_watchdog:
896
+ console.print("[bold yellow]🐕 Service Watchdog: Enabled[/bold yellow]")
897
+
898
+ if dev_mode:
899
+ console.print("[bold cyan]🛠️ Development Mode: Enabled[/bold cyan]")
900
+
901
+ app = CrackerjackDashboard()
902
+
903
+ if dev_mode:
904
+ app.dev = True
905
+
906
+ await app.run_async()
907
+
908
+
909
+ async def run_crackerjack_with_progress(
910
+ command: str = " / crackerjack: run",
911
+ ) -> None:
912
+ with suppress(Exception):
913
+ console = Console()
914
+ console.print(
915
+ "[bold green]🚀 Starting Crackerjack Progress Monitor[/bold green]",
916
+ )
917
+
918
+ app = CrackerjackDashboard()
919
+ await app.run_async()
920
+
921
+
922
+ def main() -> None:
923
+ try:
924
+ app = CrackerjackDashboard()
925
+ app.run()
926
+ except KeyboardInterrupt:
927
+ with suppress(Exception):
928
+ sys.stdout.write("\033[?25h\033[0m")
929
+ sys.stdout.flush()
930
+ subprocess.run(
931
+ ["stty", "sane"],
932
+ check=False,
933
+ capture_output=True,
934
+ timeout=1,
935
+ )
936
+ except Exception:
937
+ with suppress(Exception):
938
+ sys.stdout.write("\033[?25h\033[0m")
939
+ sys.stdout.flush()
940
+ subprocess.run(
941
+ ["stty", "sane"],
942
+ check=False,
943
+ capture_output=True,
944
+ timeout=1,
945
+ )
946
+
947
+
948
+ if __name__ == "__main__":
949
+ main()