claude-mpm 4.0.32__py3-none-any.whl → 4.0.34__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 (67) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/documentation.json +51 -34
  3. claude_mpm/agents/templates/research.json +0 -11
  4. claude_mpm/cli/__init__.py +63 -26
  5. claude_mpm/cli/commands/agent_manager.py +10 -8
  6. claude_mpm/core/framework_loader.py +173 -84
  7. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  8. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  13. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  16. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  17. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  18. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  19. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  20. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  21. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  22. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  23. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  24. claude_mpm/dashboard/templates/index.html +1 -0
  25. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  26. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  28. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  29. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  30. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  32. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  33. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  34. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  35. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  36. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  37. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  38. claude_mpm/services/agents/memory/content_manager.py +98 -105
  39. claude_mpm/services/event_bus/__init__.py +18 -0
  40. claude_mpm/services/event_bus/event_bus.py +334 -0
  41. claude_mpm/services/event_bus/relay.py +301 -0
  42. claude_mpm/services/events/__init__.py +44 -0
  43. claude_mpm/services/events/consumers/__init__.py +18 -0
  44. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  45. claude_mpm/services/events/consumers/logging.py +183 -0
  46. claude_mpm/services/events/consumers/metrics.py +242 -0
  47. claude_mpm/services/events/consumers/socketio.py +376 -0
  48. claude_mpm/services/events/core.py +470 -0
  49. claude_mpm/services/events/interfaces.py +230 -0
  50. claude_mpm/services/events/producers/__init__.py +14 -0
  51. claude_mpm/services/events/producers/hook.py +269 -0
  52. claude_mpm/services/events/producers/system.py +327 -0
  53. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  54. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  55. claude_mpm/services/monitor_build_service.py +345 -0
  56. claude_mpm/services/socketio/event_normalizer.py +667 -0
  57. claude_mpm/services/socketio/handlers/connection.py +78 -20
  58. claude_mpm/services/socketio/handlers/hook.py +14 -5
  59. claude_mpm/services/socketio/migration_utils.py +329 -0
  60. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  61. claude_mpm/services/socketio/server/core.py +4 -3
  62. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  63. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +67 -46
  64. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  65. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  66. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  67. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -17,106 +17,29 @@ class SystemInstructionsDeployer:
17
17
  self.logger = logger
18
18
  self.working_directory = working_directory
19
19
 
20
- def deploy_system_instructions_to_claude_mpm(
21
- self,
22
- target_dir: Path,
23
- force_rebuild: bool,
24
- results: Dict[str, Any],
25
- is_project_specific: bool,
26
- ) -> None:
27
- """
28
- Deploy system instructions to .claude-mpm/ directory (not .claude/).
29
-
30
- This method should ONLY be called when explicitly requested by the user.
31
- It deploys INSTRUCTIONS.md, WORKFLOW.md, and MEMORY.md files to .claude-mpm/
32
-
33
- Args:
34
- target_dir: Target directory (should be .claude-mpm/)
35
- force_rebuild: Force rebuild even if exists
36
- results: Results dictionary to update
37
- is_project_specific: Whether this is a project-specific deployment
38
- """
39
- try:
40
- # Framework files to deploy
41
- framework_files = [
42
- ("INSTRUCTIONS.md", "INSTRUCTIONS.md"),
43
- ("WORKFLOW.md", "WORKFLOW.md"),
44
- ("MEMORY.md", "MEMORY.md"),
45
- ]
46
-
47
- # Find the agents directory with framework files
48
- from claude_mpm.config.paths import paths
49
- agents_path = paths.agents_dir
50
-
51
- for source_name, target_name in framework_files:
52
- source_path = agents_path / source_name
53
-
54
- if not source_path.exists():
55
- self.logger.warning(f"Framework file not found: {source_path}")
56
- continue
57
-
58
- target_file = target_dir / target_name
59
-
60
- # Check if update needed
61
- if not force_rebuild and target_file.exists():
62
- # Compare modification times
63
- if target_file.stat().st_mtime >= source_path.stat().st_mtime:
64
- results["skipped"].append(target_name)
65
- self.logger.debug(f"Framework file {target_name} up to date")
66
- continue
67
-
68
- # Read and deploy framework file
69
- file_content = source_path.read_text()
70
- target_file.write_text(file_content)
71
-
72
- # Track deployment
73
- file_existed = target_file.exists()
74
- deployment_info = {
75
- "name": target_name,
76
- "template": str(source_path),
77
- "target": str(target_file),
78
- }
79
-
80
- if file_existed:
81
- results["updated"].append(deployment_info)
82
- self.logger.info(f"Updated framework file in .claude-mpm: {target_name}")
83
- else:
84
- results["deployed"].append(deployment_info)
85
- self.logger.info(f"Deployed framework file to .claude-mpm: {target_name}")
86
-
87
- except Exception as e:
88
- error_msg = f"Failed to deploy system instructions to .claude-mpm: {e}"
89
- self.logger.error(error_msg)
90
- results["errors"].append(error_msg)
91
-
92
20
  def deploy_system_instructions(
93
21
  self,
94
22
  target_dir: Path,
95
23
  force_rebuild: bool,
96
24
  results: Dict[str, Any],
97
- is_project_specific: bool,
98
25
  ) -> None:
99
26
  """
100
27
  Deploy system instructions and framework files for PM framework.
101
28
 
102
- Deploys INSTRUCTIONS.md, WORKFLOW.md, and MEMORY.md files following hierarchy:
103
- - System/User versions Deploy to ~/.claude/
104
- - Project-specific versions Deploy to <project>/.claude/
29
+ Always deploys to project .claude directory regardless of agent source
30
+ (system, user, or project). This ensures consistent project-level
31
+ deployment while maintaining discovery from both user (~/.claude-mpm)
32
+ and project (.claude-mpm) directories.
105
33
 
106
34
  Args:
107
- target_dir: Target directory for deployment
35
+ target_dir: Target directory for deployment (not used - always uses project .claude)
108
36
  force_rebuild: Force rebuild even if exists
109
37
  results: Results dictionary to update
110
- is_project_specific: Whether this is a project-specific deployment
111
38
  """
112
39
  try:
113
- # Determine target location based on deployment type
114
- if is_project_specific:
115
- # Project-specific files go to project's .claude directory
116
- claude_dir = self.working_directory / ".claude"
117
- else:
118
- # System and user files go to home ~/.claude directory
119
- claude_dir = Path.home() / ".claude"
40
+ # Always use project's .claude directory
41
+ # This is the key change - all system instructions go to project .claude
42
+ claude_dir = self.working_directory / ".claude"
120
43
 
121
44
  # Ensure .claude directory exists
122
45
  claude_dir.mkdir(parents=True, exist_ok=True)
@@ -222,7 +222,7 @@ class MemoryContentManager:
222
222
  """Validate memory file and repair if needed.
223
223
 
224
224
  WHY: Memory files might be manually edited by developers or corrupted.
225
- This method ensures the file maintains required structure and sections.
225
+ This method ensures the file maintains proper simple list structure.
226
226
 
227
227
  Args:
228
228
  content: Content to validate
@@ -232,88 +232,94 @@ class MemoryContentManager:
232
232
  str: Validated and repaired content
233
233
  """
234
234
  lines = content.split("\n")
235
- existing_sections = set()
236
-
237
- # Find existing sections
238
- for line in lines:
239
- if line.startswith("## "):
240
- section_name = line[3:].split("(")[0].strip()
241
- existing_sections.add(section_name)
242
-
243
- # Check for required sections
244
- missing_sections = []
245
- for required in self.REQUIRED_SECTIONS:
246
- if required not in existing_sections:
247
- missing_sections.append(required)
248
-
249
- if missing_sections:
250
- self.logger.info(
251
- f"Adding missing sections to {agent_id} memory: {missing_sections}"
252
- )
253
-
254
- # Add missing sections before Recent Learnings
255
- insert_point = len(lines)
256
- for i, line in enumerate(lines):
257
- if line.startswith("## Recent Learnings"):
258
- insert_point = i
259
- break
260
-
261
- for section in missing_sections:
262
- section_content = [
263
- "",
264
- f"## {section}",
265
- "<!-- Section added by repair -->",
266
- "",
267
- ]
268
- for j, line in enumerate(section_content):
269
- lines.insert(insert_point + j, line)
270
- insert_point += len(section_content)
271
-
235
+
236
+ # Ensure proper header format
237
+ has_header = False
238
+ has_timestamp = False
239
+
240
+ for i, line in enumerate(lines[:5]): # Check first 5 lines
241
+ if line.startswith("# Agent Memory:"):
242
+ has_header = True
243
+ elif line.startswith("<!-- Last Updated:"):
244
+ has_timestamp = True
245
+
246
+ # Add missing header or timestamp
247
+ if not has_header or not has_timestamp:
248
+ from datetime import datetime
249
+ new_lines = []
250
+
251
+ if not has_header:
252
+ new_lines.append(f"# Agent Memory: {agent_id}")
253
+ else:
254
+ # Keep existing header
255
+ for line in lines:
256
+ if line.startswith("# "):
257
+ new_lines.append(line)
258
+ lines.remove(line)
259
+ break
260
+
261
+ if not has_timestamp:
262
+ new_lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
263
+ new_lines.append("")
264
+ else:
265
+ # Keep existing timestamp
266
+ for line in lines:
267
+ if line.startswith("<!-- Last Updated:"):
268
+ new_lines.append(line)
269
+ lines.remove(line)
270
+ break
271
+
272
+ # Add remaining content
273
+ for line in lines:
274
+ if not line.startswith("# ") and not line.startswith("<!-- Last Updated:"):
275
+ new_lines.append(line)
276
+
277
+ return "\n".join(new_lines)
278
+
272
279
  return "\n".join(lines)
273
280
 
274
- def parse_memory_content_to_dict(self, content: str) -> Dict[str, List[str]]:
275
- """Parse memory content into structured dictionary format.
281
+ def parse_memory_content_to_list(self, content: str) -> List[str]:
282
+ """Parse memory content into a simple list format.
276
283
 
277
- WHY: Provides consistent parsing of memory content into sections and items
278
- for both display and programmatic access. This ensures the same parsing
279
- logic is used across the system.
284
+ WHY: Provides consistent parsing of memory content as a simple list
285
+ for both display and programmatic access.
280
286
 
281
287
  Args:
282
288
  content: Raw memory file content
283
289
 
284
290
  Returns:
285
- Dict mapping section names to lists of items
291
+ List of memory items
286
292
  """
287
- sections = {}
288
- current_section = None
289
- current_items = []
293
+ items = []
290
294
 
291
295
  for line in content.split("\n"):
292
296
  line = line.strip()
293
297
 
294
- # Skip empty lines and header information
295
- if not line or line.startswith("#") and "Memory Usage" in line:
298
+ # Skip empty lines, headers, and metadata
299
+ if not line or line.startswith("#") or line.startswith("<!--"):
296
300
  continue
297
301
 
298
- if line.startswith("## ") and not line.startswith("## Memory Usage"):
299
- # New section found
300
- if current_section and current_items:
301
- sections[current_section] = current_items.copy()
302
-
303
- current_section = line[3:].strip()
304
- current_items = []
305
-
306
- elif line.startswith("- ") and current_section:
307
- # Item in current section
302
+ if line.startswith("- "):
303
+ # Item in list
308
304
  item = line[2:].strip()
309
305
  if item and len(item) > 3: # Filter out very short items
310
- current_items.append(item)
311
-
312
- # Add final section
313
- if current_section and current_items:
314
- sections[current_section] = current_items
306
+ items.append(item)
315
307
 
316
- return sections
308
+ return items
309
+
310
+ def parse_memory_content_to_dict(self, content: str) -> Dict[str, List[str]]:
311
+ """Legacy method for backward compatibility.
312
+
313
+ Returns a dict with single key 'memories' containing all items.
314
+
315
+ Args:
316
+ content: Raw memory file content
317
+
318
+ Returns:
319
+ Dict with 'memories' key mapping to list of items
320
+ """
321
+ items = self.parse_memory_content_to_list(content)
322
+ return {"memories": items}
317
323
 
318
324
  def _calculate_similarity(self, str1: str, str2: str) -> float:
319
325
  """Calculate similarity between two strings using fuzzy matching.
@@ -357,44 +363,27 @@ class MemoryContentManager:
357
363
 
358
364
  return similarity
359
365
 
360
- def deduplicate_section(self, content: str, section: str) -> Tuple[str, int]:
361
- """Deduplicate items within a section using NLP similarity.
366
+ def deduplicate_list(self, content: str) -> Tuple[str, int]:
367
+ """Deduplicate items in the memory list using NLP similarity.
362
368
 
363
- WHY: Over time, sections can accumulate similar or duplicate items from
364
- different sessions. This method cleans up existing sections by removing
365
- similar items while preserving the most recent/relevant ones.
369
+ WHY: Over time, memory lists can accumulate similar or duplicate items from
370
+ different sessions. This method cleans up by removing similar items while
371
+ preserving the most recent ones.
366
372
 
367
373
  Args:
368
374
  content: Current memory file content
369
- section: Section name to deduplicate
370
375
 
371
376
  Returns:
372
377
  Tuple of (updated content, number of items removed)
373
378
  """
374
379
  lines = content.split("\n")
375
- section_start = None
376
- section_end = None
377
-
378
- # Find section boundaries
379
- for i, line in enumerate(lines):
380
- if line.startswith(f"## {section}"):
381
- section_start = i
382
- elif section_start is not None and line.startswith("## "):
383
- section_end = i
384
- break
385
-
386
- if section_start is None:
387
- return content, 0 # Section not found
388
-
389
- if section_end is None:
390
- section_end = len(lines)
391
380
 
392
- # Collect all items in the section
381
+ # Collect all items in the list
393
382
  items = []
394
383
  item_indices = []
395
- for i in range(section_start + 1, section_end):
396
- if lines[i].strip().startswith("- "):
397
- items.append(lines[i].strip()[2:]) # Remove "- " prefix
384
+ for i, line in enumerate(lines):
385
+ if line.strip().startswith("- "):
386
+ items.append(line.strip()[2:]) # Remove "- " prefix
398
387
  item_indices.append(i)
399
388
 
400
389
  # Find duplicates using pairwise comparison
@@ -421,6 +410,18 @@ class MemoryContentManager:
421
410
  lines.pop(item_indices[idx])
422
411
 
423
412
  return "\n".join(lines), removed_count
413
+
414
+ def deduplicate_section(self, content: str, section: str) -> Tuple[str, int]:
415
+ """Legacy method for backward compatibility - delegates to deduplicate_list.
416
+
417
+ Args:
418
+ content: Current memory file content
419
+ section: Section name (ignored in simple list format)
420
+
421
+ Returns:
422
+ Tuple of (updated content, number of items removed)
423
+ """
424
+ return self.deduplicate_list(content)
424
425
 
425
426
  def validate_memory_size(self, content: str) -> tuple[bool, Optional[str]]:
426
427
  """Validate memory content size and structure.
@@ -434,7 +435,7 @@ class MemoryContentManager:
434
435
  try:
435
436
  # Check file size
436
437
  size_kb = len(content.encode("utf-8")) / 1024
437
- max_size_kb = self.memory_limits.get("max_file_size_kb", 8)
438
+ max_size_kb = self.memory_limits.get("max_file_size_kb", 80)
438
439
 
439
440
  if size_kb > max_size_kb:
440
441
  return (
@@ -442,20 +443,12 @@ class MemoryContentManager:
442
443
  f"Memory size {size_kb:.1f}KB exceeds limit of {max_size_kb}KB",
443
444
  )
444
445
 
445
- # Check section count
446
- sections = re.findall(r"^##\s+(.+)$", content, re.MULTILINE)
447
- max_sections = self.memory_limits.get("max_sections", 10)
448
-
449
- if len(sections) > max_sections:
450
- return False, f"Too many sections: {len(sections)} (max {max_sections})"
451
-
452
- # Check for required sections
453
- required = set(self.REQUIRED_SECTIONS)
454
- found = set(sections)
455
- missing = required - found
446
+ # Check item count
447
+ items = sum(1 for line in content.split("\n") if line.strip().startswith("- "))
448
+ max_items = self.memory_limits.get("max_items", 100)
456
449
 
457
- if missing:
458
- return False, f"Missing required sections: {', '.join(missing)}"
450
+ if items > max_items:
451
+ return False, f"Too many items: {items} (max {max_items})"
459
452
 
460
453
  return True, None
461
454
 
@@ -0,0 +1,18 @@
1
+ """Event Bus Service for decoupled event handling.
2
+
3
+ This module provides a centralized event bus that decouples event producers
4
+ (like hooks) from consumers (like Socket.IO). It uses pyee's AsyncIOEventEmitter
5
+ to support both synchronous publishing and asynchronous consumption.
6
+
7
+ WHY event bus architecture:
8
+ - Decouples hooks from Socket.IO implementation details
9
+ - Allows multiple consumers for the same events
10
+ - Enables easy testing without Socket.IO dependencies
11
+ - Provides event filtering and routing capabilities
12
+ - Supports both sync (hooks) and async (Socket.IO) contexts
13
+ """
14
+
15
+ from .event_bus import EventBus
16
+ from .relay import SocketIORelay
17
+
18
+ __all__ = ["EventBus", "SocketIORelay"]