claude-mpm 4.1.4__py3-none-any.whl → 4.1.6__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 (81) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/research.json +39 -13
  3. claude_mpm/cli/__init__.py +2 -0
  4. claude_mpm/cli/commands/__init__.py +2 -0
  5. claude_mpm/cli/commands/configure.py +1221 -0
  6. claude_mpm/cli/commands/configure_tui.py +1921 -0
  7. claude_mpm/cli/commands/tickets.py +365 -784
  8. claude_mpm/cli/parsers/base_parser.py +7 -0
  9. claude_mpm/cli/parsers/configure_parser.py +119 -0
  10. claude_mpm/cli/startup_logging.py +39 -12
  11. claude_mpm/constants.py +1 -0
  12. claude_mpm/core/output_style_manager.py +24 -0
  13. claude_mpm/core/socketio_pool.py +35 -3
  14. claude_mpm/core/unified_agent_registry.py +46 -15
  15. claude_mpm/dashboard/static/css/connection-status.css +370 -0
  16. claude_mpm/dashboard/static/js/components/connection-debug.js +654 -0
  17. claude_mpm/dashboard/static/js/connection-manager.js +536 -0
  18. claude_mpm/dashboard/templates/index.html +11 -0
  19. claude_mpm/hooks/claude_hooks/services/__init__.py +3 -1
  20. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +190 -0
  21. claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
  22. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
  23. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
  24. claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
  25. claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
  26. claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
  27. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
  28. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  29. claude_mpm/services/diagnostics/checks/instructions_check.py +418 -0
  30. claude_mpm/services/diagnostics/diagnostic_runner.py +15 -2
  31. claude_mpm/services/event_bus/direct_relay.py +173 -0
  32. claude_mpm/services/infrastructure/__init__.py +31 -5
  33. claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
  34. claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
  35. claude_mpm/services/infrastructure/monitoring/base.py +130 -0
  36. claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
  37. claude_mpm/services/infrastructure/monitoring/network.py +218 -0
  38. claude_mpm/services/infrastructure/monitoring/process.py +342 -0
  39. claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
  40. claude_mpm/services/infrastructure/monitoring/service.py +367 -0
  41. claude_mpm/services/infrastructure/monitoring.py +67 -1030
  42. claude_mpm/services/project/analyzer.py +13 -4
  43. claude_mpm/services/project/analyzer_refactored.py +450 -0
  44. claude_mpm/services/project/analyzer_v2.py +566 -0
  45. claude_mpm/services/project/architecture_analyzer.py +461 -0
  46. claude_mpm/services/project/dependency_analyzer.py +462 -0
  47. claude_mpm/services/project/language_analyzer.py +265 -0
  48. claude_mpm/services/project/metrics_collector.py +410 -0
  49. claude_mpm/services/socketio/handlers/connection_handler.py +345 -0
  50. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  51. claude_mpm/services/socketio/server/connection_manager.py +516 -0
  52. claude_mpm/services/socketio/server/core.py +63 -0
  53. claude_mpm/services/socketio/server/eventbus_integration.py +20 -9
  54. claude_mpm/services/socketio/server/main.py +27 -1
  55. claude_mpm/services/ticket_manager.py +5 -1
  56. claude_mpm/services/ticket_services/__init__.py +26 -0
  57. claude_mpm/services/ticket_services/crud_service.py +328 -0
  58. claude_mpm/services/ticket_services/formatter_service.py +290 -0
  59. claude_mpm/services/ticket_services/search_service.py +324 -0
  60. claude_mpm/services/ticket_services/validation_service.py +303 -0
  61. claude_mpm/services/ticket_services/workflow_service.py +244 -0
  62. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/METADATA +3 -1
  63. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/RECORD +67 -46
  64. claude_mpm/agents/OUTPUT_STYLE.md +0 -73
  65. claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
  66. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +0 -156
  67. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -79
  68. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -68
  69. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -77
  70. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -78
  71. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -67
  72. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +0 -88
  73. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -72
  74. claude_mpm/agents/templates/backup/research_memory_efficient.json +0 -88
  75. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -78
  76. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -62
  77. claude_mpm/agents/templates/vercel_ops_instructions.md +0 -582
  78. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/WHEEL +0 -0
  79. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/entry_points.txt +0 -0
  80. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/licenses/LICENSE +0 -0
  81. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,418 @@
1
+ """
2
+ Check for duplicate or conflicting CLAUDE.md and instruction files.
3
+
4
+ WHY: Detect duplicate content, conflicting directives, and improperly placed
5
+ instruction files that could cause confusion in agent behavior.
6
+ """
7
+
8
+ import hashlib
9
+ import re
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+ from typing import Dict
13
+
14
+ from ..models import DiagnosticResult, DiagnosticStatus
15
+ from .base_check import BaseDiagnosticCheck
16
+
17
+
18
+ class InstructionsCheck(BaseDiagnosticCheck):
19
+ """Check for duplicate, conflicting, or misplaced instruction files."""
20
+
21
+ # Known instruction file patterns
22
+ INSTRUCTION_FILES = {
23
+ "CLAUDE.md": "Claude Code instructions (should be in project root only)",
24
+ "INSTRUCTIONS.md": "MPM agent customization",
25
+ "BASE_PM.md": "Base PM framework requirements",
26
+ }
27
+
28
+ # Patterns that indicate potential conflicts
29
+ CONFLICT_PATTERNS = [
30
+ (r"(?i)you\s+are\s+.*pm", "PM role definition"),
31
+ (r"(?i)delegation\s+rules?", "Delegation rules"),
32
+ (r"(?i)agent\s+selection", "Agent selection logic"),
33
+ (r"(?i)framework\s+behavior", "Framework behavior"),
34
+ (r"(?i)command\s+interception", "Command interception"),
35
+ (r"(?i)memory\s+system", "Memory system configuration"),
36
+ (r"(?i)response\s+format", "Response formatting"),
37
+ ]
38
+
39
+ @property
40
+ def name(self) -> str:
41
+ return "instructions_check"
42
+
43
+ @property
44
+ def category(self) -> str:
45
+ return "Instructions"
46
+
47
+ def run(self) -> DiagnosticResult:
48
+ """Run instructions file diagnostics."""
49
+ try:
50
+ sub_results = []
51
+ details = {}
52
+
53
+ # Find all instruction files
54
+ instruction_files = self._find_instruction_files()
55
+ details["found_files"] = {
56
+ str(path): file_type for path, file_type in instruction_files.items()
57
+ }
58
+
59
+ # Check for misplaced CLAUDE.md files
60
+ claude_result = self._check_claude_md_placement(instruction_files)
61
+ sub_results.append(claude_result)
62
+
63
+ # Check for duplicate content
64
+ duplicate_result = self._check_duplicates(instruction_files)
65
+ sub_results.append(duplicate_result)
66
+
67
+ # Check for conflicting directives
68
+ conflict_result = self._check_conflicts(instruction_files)
69
+ sub_results.append(conflict_result)
70
+
71
+ # Check for overlapping agent definitions
72
+ agent_result = self._check_agent_definitions(instruction_files)
73
+ sub_results.append(agent_result)
74
+
75
+ # Check proper separation of concerns
76
+ separation_result = self._check_separation_of_concerns(instruction_files)
77
+ sub_results.append(separation_result)
78
+
79
+ # Determine overall status
80
+ if any(r.status == DiagnosticStatus.ERROR for r in sub_results):
81
+ status = DiagnosticStatus.ERROR
82
+ message = "Found critical issues with instruction files"
83
+ elif any(r.status == DiagnosticStatus.WARNING for r in sub_results):
84
+ status = DiagnosticStatus.WARNING
85
+ message = "Found minor issues with instruction files"
86
+ else:
87
+ status = DiagnosticStatus.OK
88
+ message = "Instruction files are properly configured"
89
+
90
+ return DiagnosticResult(
91
+ category=self.category,
92
+ status=status,
93
+ message=message,
94
+ details=details,
95
+ sub_results=sub_results if self.verbose else [],
96
+ )
97
+
98
+ except Exception as e:
99
+ return DiagnosticResult(
100
+ category=self.category,
101
+ status=DiagnosticStatus.ERROR,
102
+ message=f"Instructions check failed: {e!s}",
103
+ details={"error": str(e)},
104
+ )
105
+
106
+ def _find_instruction_files(self) -> Dict[Path, str]:
107
+ """Find all instruction files in the project and user directories."""
108
+ found_files = {}
109
+
110
+ # Search locations
111
+ search_paths = [
112
+ Path.cwd(), # Current project
113
+ Path.home() / ".claude-mpm", # User directory
114
+ Path.home() / ".claude", # Alternative user directory
115
+ ]
116
+
117
+ for base_path in search_paths:
118
+ if not base_path.exists():
119
+ continue
120
+
121
+ for pattern, file_type in self.INSTRUCTION_FILES.items():
122
+ # Use rglob for recursive search
123
+ for file_path in base_path.rglob(pattern):
124
+ # Skip node_modules and virtual environments
125
+ if any(
126
+ part in file_path.parts
127
+ for part in [
128
+ "node_modules",
129
+ "venv",
130
+ ".venv",
131
+ "__pycache__",
132
+ ".git",
133
+ ]
134
+ ):
135
+ continue
136
+ found_files[file_path] = file_type
137
+
138
+ return found_files
139
+
140
+ def _check_claude_md_placement(self, files: Dict[Path, str]) -> DiagnosticResult:
141
+ """Check that CLAUDE.md files are properly placed."""
142
+ claude_files = [
143
+ path for path, file_type in files.items() if path.name == "CLAUDE.md"
144
+ ]
145
+
146
+ if not claude_files:
147
+ return DiagnosticResult(
148
+ category="CLAUDE.md Placement",
149
+ status=DiagnosticStatus.OK,
150
+ message="No CLAUDE.md files found",
151
+ details={},
152
+ )
153
+
154
+ issues = []
155
+ project_root = Path.cwd()
156
+
157
+ for path in claude_files:
158
+ # CLAUDE.md should only be in project root
159
+ if path.parent != project_root:
160
+ rel_path = (
161
+ path.relative_to(project_root)
162
+ if project_root in path.parents or path.parent == project_root
163
+ else path
164
+ )
165
+ issues.append(
166
+ f"CLAUDE.md found in non-root location: {rel_path}\n"
167
+ f" → Should be in project root only for Claude Code"
168
+ )
169
+
170
+ if issues:
171
+ return DiagnosticResult(
172
+ category="CLAUDE.md Placement",
173
+ status=DiagnosticStatus.WARNING,
174
+ message=f"Found {len(issues)} misplaced CLAUDE.md file(s)",
175
+ details={"issues": issues},
176
+ fix_description=(
177
+ "CLAUDE.md should only exist in the project root directory. "
178
+ "Move or remove misplaced files."
179
+ ),
180
+ )
181
+
182
+ return DiagnosticResult(
183
+ category="CLAUDE.md Placement",
184
+ status=DiagnosticStatus.OK,
185
+ message="CLAUDE.md properly placed in project root",
186
+ details={"count": len(claude_files)},
187
+ )
188
+
189
+ def _check_duplicates(self, files: Dict[Path, str]) -> DiagnosticResult:
190
+ """Check for duplicate content between instruction files."""
191
+ if len(files) < 2:
192
+ return DiagnosticResult(
193
+ category="Duplicate Content",
194
+ status=DiagnosticStatus.OK,
195
+ message="No duplicate content detected",
196
+ details={},
197
+ )
198
+
199
+ # Calculate content hashes
200
+ content_hashes = {}
201
+ content_snippets = defaultdict(list)
202
+
203
+ for path in files:
204
+ try:
205
+ content = path.read_text(encoding="utf-8")
206
+ # Hash significant blocks (paragraphs)
207
+ paragraphs = re.split(r"\n\s*\n", content)
208
+ for para in paragraphs:
209
+ para = para.strip()
210
+ if len(para) > 50: # Skip short snippets
211
+ hash_val = hashlib.md5(para.encode()).hexdigest()
212
+ content_snippets[hash_val].append((path, para[:100]))
213
+ except Exception:
214
+ continue
215
+
216
+ # Find duplicates
217
+ duplicates = []
218
+ for hash_val, occurrences in content_snippets.items():
219
+ if len(occurrences) > 1:
220
+ files_str = ", ".join(str(path) for path, _ in occurrences)
221
+ snippet = occurrences[0][1]
222
+ duplicates.append(
223
+ f"Duplicate content found in: {files_str}\n"
224
+ f" Snippet: {snippet}..."
225
+ )
226
+
227
+ if duplicates:
228
+ return DiagnosticResult(
229
+ category="Duplicate Content",
230
+ status=DiagnosticStatus.WARNING,
231
+ message=f"Found {len(duplicates)} duplicate content block(s)",
232
+ details={"duplicates": duplicates[:5]}, # Limit to first 5
233
+ fix_description=(
234
+ "Remove duplicate content between files. "
235
+ "CLAUDE.md should contain Claude Code instructions, "
236
+ "INSTRUCTIONS.md should contain MPM-specific customization."
237
+ ),
238
+ )
239
+
240
+ return DiagnosticResult(
241
+ category="Duplicate Content",
242
+ status=DiagnosticStatus.OK,
243
+ message="No significant duplicate content found",
244
+ details={},
245
+ )
246
+
247
+ def _check_conflicts(self, files: Dict[Path, str]) -> DiagnosticResult:
248
+ """Check for conflicting directives between instruction files."""
249
+ conflicts = []
250
+ pattern_occurrences = defaultdict(list)
251
+
252
+ for path in files:
253
+ try:
254
+ content = path.read_text(encoding="utf-8")
255
+ for pattern, description in self.CONFLICT_PATTERNS:
256
+ matches = re.findall(pattern, content, re.MULTILINE)
257
+ if matches:
258
+ pattern_occurrences[description].append(
259
+ (path, len(matches), matches[0][:100])
260
+ )
261
+ except Exception:
262
+ continue
263
+
264
+ # Find patterns that appear in multiple files
265
+ for description, occurrences in pattern_occurrences.items():
266
+ if len(occurrences) > 1:
267
+ files_info = []
268
+ for path, count, snippet in occurrences:
269
+ rel_path = (
270
+ path.relative_to(Path.cwd())
271
+ if Path.cwd() in path.parents or path.parent == Path.cwd()
272
+ else path
273
+ )
274
+ files_info.append(f"{rel_path} ({count} occurrence(s))")
275
+
276
+ conflicts.append(
277
+ f"Potential conflict for '{description}':\n"
278
+ f" Found in: {', '.join(files_info)}"
279
+ )
280
+
281
+ if conflicts:
282
+ return DiagnosticResult(
283
+ category="Conflicting Directives",
284
+ status=DiagnosticStatus.ERROR,
285
+ message=f"Found {len(conflicts)} potential conflict(s)",
286
+ details={"conflicts": conflicts},
287
+ fix_description=(
288
+ "Review and consolidate conflicting directives. "
289
+ "PM role and behavior should be in INSTRUCTIONS.md, "
290
+ "Claude Code directives should be in CLAUDE.md."
291
+ ),
292
+ )
293
+
294
+ return DiagnosticResult(
295
+ category="Conflicting Directives",
296
+ status=DiagnosticStatus.OK,
297
+ message="No conflicting directives detected",
298
+ details={},
299
+ )
300
+
301
+ def _check_agent_definitions(self, files: Dict[Path, str]) -> DiagnosticResult:
302
+ """Check for overlapping or duplicate agent definitions."""
303
+ agent_definitions = defaultdict(list)
304
+ agent_pattern = r"(?:agent|Agent)\s+(\w+).*?(?:specializes?|expert|handles?)"
305
+
306
+ for path in files:
307
+ try:
308
+ content = path.read_text(encoding="utf-8")
309
+ matches = re.findall(agent_pattern, content, re.IGNORECASE)
310
+ for agent_name in matches:
311
+ agent_definitions[agent_name.lower()].append(path)
312
+ except Exception:
313
+ continue
314
+
315
+ # Find agents defined in multiple places
316
+ duplicates = []
317
+ for agent_name, paths in agent_definitions.items():
318
+ if len(paths) > 1:
319
+ files_str = ", ".join(
320
+ str(
321
+ path.relative_to(Path.cwd())
322
+ if Path.cwd() in path.parents or path.parent == Path.cwd()
323
+ else path
324
+ )
325
+ for path in paths
326
+ )
327
+ duplicates.append(
328
+ f"Agent '{agent_name}' defined in multiple files: {files_str}"
329
+ )
330
+
331
+ if duplicates:
332
+ return DiagnosticResult(
333
+ category="Agent Definitions",
334
+ status=DiagnosticStatus.WARNING,
335
+ message=f"Found {len(duplicates)} duplicate agent definition(s)",
336
+ details={"duplicates": duplicates},
337
+ fix_description=(
338
+ "Consolidate agent definitions in INSTRUCTIONS.md. "
339
+ "Each agent should be defined only once."
340
+ ),
341
+ )
342
+
343
+ return DiagnosticResult(
344
+ category="Agent Definitions",
345
+ status=DiagnosticStatus.OK,
346
+ message="Agent definitions are unique",
347
+ details={"total_agents": len(agent_definitions)},
348
+ )
349
+
350
+ def _check_separation_of_concerns(self, files: Dict[Path, str]) -> DiagnosticResult:
351
+ """Check that instruction files follow proper separation of concerns."""
352
+ issues = []
353
+
354
+ # Check for MPM-specific content in CLAUDE.md
355
+ claude_files = [path for path in files if path.name == "CLAUDE.md"]
356
+ for path in claude_files:
357
+ try:
358
+ content = path.read_text(encoding="utf-8")
359
+ # Check for MPM-specific patterns
360
+ mpm_patterns = [
361
+ r"(?i)multi-agent",
362
+ r"(?i)delegation",
363
+ r"(?i)agent\s+selection",
364
+ r"(?i)PM\s+role",
365
+ ]
366
+ for pattern in mpm_patterns:
367
+ if re.search(pattern, content):
368
+ issues.append(
369
+ f"CLAUDE.md contains MPM-specific content (pattern: {pattern})\n"
370
+ f" → Move to INSTRUCTIONS.md"
371
+ )
372
+ break
373
+ except Exception:
374
+ continue
375
+
376
+ # Check for Claude Code specific content in INSTRUCTIONS.md
377
+ instructions_files = [
378
+ path for path in files if path.name == "INSTRUCTIONS.md"
379
+ ]
380
+ for path in instructions_files:
381
+ try:
382
+ content = path.read_text(encoding="utf-8")
383
+ # Check for Claude Code specific patterns
384
+ claude_patterns = [
385
+ r"(?i)claude\s+code",
386
+ r"(?i)development\s+guidelines",
387
+ r"(?i)project\s+structure",
388
+ ]
389
+ for pattern in claude_patterns:
390
+ if re.search(pattern, content):
391
+ issues.append(
392
+ f"INSTRUCTIONS.md contains Claude Code content (pattern: {pattern})\n"
393
+ f" → Should focus on MPM customization only"
394
+ )
395
+ break
396
+ except Exception:
397
+ continue
398
+
399
+ if issues:
400
+ return DiagnosticResult(
401
+ category="Separation of Concerns",
402
+ status=DiagnosticStatus.WARNING,
403
+ message=f"Found {len(issues)} separation of concerns issue(s)",
404
+ details={"issues": issues},
405
+ fix_description=(
406
+ "Maintain clear separation:\n"
407
+ "• CLAUDE.md: Claude Code development guidelines\n"
408
+ "• INSTRUCTIONS.md: MPM agent behavior and customization\n"
409
+ "• BASE_PM.md: Framework requirements (do not modify)"
410
+ ),
411
+ )
412
+
413
+ return DiagnosticResult(
414
+ category="Separation of Concerns",
415
+ status=DiagnosticStatus.OK,
416
+ message="Instruction files properly separated",
417
+ details={},
418
+ )
@@ -18,6 +18,7 @@ from .checks import (
18
18
  ConfigurationCheck,
19
19
  FilesystemCheck,
20
20
  InstallationCheck,
21
+ InstructionsCheck,
21
22
  MCPCheck,
22
23
  MonitorCheck,
23
24
  StartupLogCheck,
@@ -48,6 +49,7 @@ class DiagnosticRunner:
48
49
  InstallationCheck,
49
50
  ConfigurationCheck,
50
51
  FilesystemCheck,
52
+ InstructionsCheck, # Check instruction files early
51
53
  ClaudeDesktopCheck,
52
54
  AgentCheck,
53
55
  MCPCheck,
@@ -107,9 +109,20 @@ class DiagnosticRunner:
107
109
 
108
110
  # Group checks by dependency level
109
111
  # Level 1: No dependencies
110
- level1 = [InstallationCheck, FilesystemCheck, ConfigurationCheck]
112
+ level1 = [
113
+ InstallationCheck,
114
+ FilesystemCheck,
115
+ ConfigurationCheck,
116
+ InstructionsCheck,
117
+ ]
111
118
  # Level 2: May depend on level 1
112
- level2 = [ClaudeDesktopCheck, AgentCheck, MCPCheck, MonitorCheck]
119
+ level2 = [
120
+ ClaudeDesktopCheck,
121
+ AgentCheck,
122
+ MCPCheck,
123
+ MonitorCheck,
124
+ StartupLogCheck,
125
+ ]
113
126
  # Level 3: Depends on others
114
127
  level3 = [CommonIssuesCheck]
115
128
 
@@ -0,0 +1,173 @@
1
+ """Direct EventBus to Socket.IO relay that uses server broadcaster.
2
+
3
+ This module provides a relay that connects EventBus directly to the
4
+ Socket.IO server's broadcaster, avoiding the client loopback issue.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ from .event_bus import EventBus
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class DirectSocketIORelay:
17
+ """Relay EventBus events directly to Socket.IO broadcaster.
18
+
19
+ WHY: The original SocketIORelay creates a client connection back to the server,
20
+ which causes events to not reach the dashboard properly. This direct relay
21
+ uses the server's broadcaster directly for proper event emission.
22
+ """
23
+
24
+ def __init__(self, server_instance):
25
+ """Initialize the direct relay.
26
+
27
+ Args:
28
+ server_instance: The SocketIOServer instance with broadcaster
29
+ """
30
+ self.server = server_instance
31
+ self.event_bus = EventBus.get_instance()
32
+ self.enabled = True
33
+ self.connected = False # Track connection state
34
+ self.stats = {
35
+ "events_relayed": 0,
36
+ "events_failed": 0,
37
+ "last_relay_time": None,
38
+ }
39
+ self.debug = logger.isEnabledFor(logging.DEBUG)
40
+
41
+ def start(self) -> None:
42
+ """Start the relay by subscribing to EventBus events."""
43
+ if not self.enabled:
44
+ logger.warning("DirectSocketIORelay is disabled")
45
+ return
46
+
47
+ # Create handler for wildcard events
48
+ def handle_wildcard_hook_event(event_type: str, data: Any):
49
+ """Handle wildcard hook events from the event bus.
50
+
51
+ Wildcard handlers receive both event_type and data.
52
+ This is the primary handler that knows the correct event type.
53
+ """
54
+ self._handle_hook_event(event_type, data)
55
+
56
+ # Subscribe to all hook events via wildcard
57
+ # This single subscription handles all hook.* events efficiently
58
+ self.event_bus.on("hook.*", handle_wildcard_hook_event)
59
+
60
+ # Add debug logging for verification
61
+ logger.info("[DirectRelay] Subscribed to hook.* events on EventBus")
62
+ logger.info(
63
+ f"[DirectRelay] Server broadcaster available: {self.server and self.server.broadcaster is not None}"
64
+ )
65
+ logger.info(f"[DirectRelay] EventBus instance: {self.event_bus is not None}")
66
+
67
+ # Mark as connected after successful subscription
68
+ self.connected = True
69
+ logger.info("[DirectRelay] Started and subscribed to hook events")
70
+
71
+ def _handle_hook_event(self, event_type: str, data: Any):
72
+ """Internal method to handle hook events and broadcast them.
73
+
74
+ Args:
75
+ event_type: The event type (e.g., "hook.pre_tool")
76
+ data: The event data
77
+ """
78
+ try:
79
+ # Log the event reception
80
+ if self.debug:
81
+ logger.debug(f"[DirectRelay] Received event: {event_type}")
82
+
83
+ # Only relay hook events
84
+ if event_type.startswith("hook."):
85
+ # Extract the event subtype from the event_type (e.g., "hook.pre_tool" -> "pre_tool")
86
+ event_subtype = (
87
+ event_type.split(".", 1)[1] if "." in event_type else event_type
88
+ )
89
+
90
+ # The data passed to us is the raw event data from the publisher
91
+ # We don't need to extract anything - just use it as is
92
+ actual_data = data
93
+
94
+ # Always log important hook events for debugging
95
+ if event_subtype in [
96
+ "pre_tool",
97
+ "post_tool",
98
+ "user_prompt",
99
+ "subagent_stop",
100
+ ]:
101
+ logger.info(f"[DirectRelay] Processing {event_type} event")
102
+
103
+ # Use the server's broadcaster directly
104
+ if self.server and self.server.broadcaster:
105
+ # Log debug info about the broadcaster state
106
+ if self.debug:
107
+ has_sio = (
108
+ hasattr(self.server.broadcaster, "sio")
109
+ and self.server.broadcaster.sio is not None
110
+ )
111
+ has_loop = (
112
+ hasattr(self.server.broadcaster, "loop")
113
+ and self.server.broadcaster.loop is not None
114
+ )
115
+ logger.debug(
116
+ f"[DirectRelay] Broadcaster state - has_sio: {has_sio}, has_loop: {has_loop}"
117
+ )
118
+ logger.debug(
119
+ f"[DirectRelay] Event subtype: {event_subtype}, data keys: {list(actual_data.keys()) if isinstance(actual_data, dict) else 'not-dict'}"
120
+ )
121
+
122
+ # The broadcaster's broadcast_event expects an event_type string and data dict
123
+ # The EventNormalizer will map dotted event names like "hook.pre_tool" correctly
124
+ # So we pass the full event_type (e.g., "hook.pre_tool") as the event name
125
+ # This way the normalizer will correctly extract type="hook" and subtype="pre_tool"
126
+
127
+ # Prepare the broadcast data - just the actual event data
128
+ broadcast_data = (
129
+ actual_data
130
+ if isinstance(actual_data, dict)
131
+ else {"data": actual_data}
132
+ )
133
+
134
+ # Use the full event_type (e.g., "hook.pre_tool") as the event name
135
+ # The normalizer handles dotted names and will extract type and subtype correctly
136
+ self.server.broadcaster.broadcast_event(event_type, broadcast_data)
137
+
138
+ self.stats["events_relayed"] += 1
139
+ self.stats["last_relay_time"] = datetime.now().isoformat()
140
+
141
+ if self.debug:
142
+ logger.debug(
143
+ f"[DirectRelay] Broadcasted hook event: {event_type}"
144
+ )
145
+ else:
146
+ logger.warning(
147
+ f"[DirectRelay] Server broadcaster not available for {event_type}"
148
+ )
149
+ self.stats["events_failed"] += 1
150
+
151
+ except Exception as e:
152
+ self.stats["events_failed"] += 1
153
+ logger.error(f"[DirectRelay] Failed to relay event {event_type}: {e}")
154
+
155
+ def stop(self) -> None:
156
+ """Stop the relay."""
157
+ self.enabled = False
158
+ self.connected = False
159
+ # EventBus doesn't provide an off() method, so listeners remain
160
+ # but the enabled flag prevents processing
161
+ logger.info("[DirectRelay] Stopped")
162
+
163
+ def get_stats(self) -> dict:
164
+ """Get relay statistics."""
165
+ return {
166
+ "enabled": self.enabled,
167
+ "connected": self.connected,
168
+ "events_relayed": self.stats["events_relayed"],
169
+ "events_failed": self.stats["events_failed"],
170
+ "last_relay_time": self.stats["last_relay_time"],
171
+ "has_server": self.server is not None,
172
+ "has_broadcaster": self.server and self.server.broadcaster is not None,
173
+ }
@@ -13,14 +13,40 @@ Services:
13
13
  - MemoryGuardian: Memory monitoring and process restart management
14
14
  """
15
15
 
16
- from .health_monitor import HealthMonitor
17
16
  from .logging import LoggingService
18
- from .memory_guardian import MemoryGuardian
19
- from .monitoring import AdvancedHealthMonitor
17
+ from .monitoring import (
18
+ AdvancedHealthMonitor,
19
+ MonitoringAggregatorService,
20
+ NetworkHealthService,
21
+ ProcessHealthService,
22
+ ResourceMonitorService,
23
+ ServiceHealthService,
24
+ )
25
+
26
+ # Check if optional modules exist
27
+ try:
28
+ from .health_monitor import HealthMonitor
29
+ except ImportError:
30
+ HealthMonitor = None
31
+
32
+ try:
33
+ from .memory_guardian import MemoryGuardian
34
+ except ImportError:
35
+ MemoryGuardian = None
20
36
 
21
37
  __all__ = [
22
38
  "AdvancedHealthMonitor", # For SocketIO server monitoring
23
- "HealthMonitor", # For Memory Guardian system monitoring
24
39
  "LoggingService",
25
- "MemoryGuardian",
40
+ # New service-based monitoring API
41
+ "MonitoringAggregatorService",
42
+ "NetworkHealthService",
43
+ "ProcessHealthService",
44
+ "ResourceMonitorService",
45
+ "ServiceHealthService",
26
46
  ]
47
+
48
+ # Add optional modules if they exist
49
+ if HealthMonitor is not None:
50
+ __all__.append("HealthMonitor")
51
+ if MemoryGuardian is not None:
52
+ __all__.append("MemoryGuardian")