claude-mpm 5.1.9__py3-none-any.whl → 5.4.3__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 claude-mpm might be problematic. Click here for more details.

Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +46 -0
  3. claude_mpm/agents/agent_loader.py +10 -17
  4. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  5. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  6. claude_mpm/cli/commands/configure.py +1046 -149
  7. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  8. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  9. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  10. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  11. claude_mpm/cli/commands/summarize.py +413 -0
  12. claude_mpm/cli/executor.py +8 -0
  13. claude_mpm/cli/parsers/base_parser.py +5 -0
  14. claude_mpm/cli/startup.py +60 -53
  15. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  16. claude_mpm/config/agent_sources.py +27 -0
  17. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  18. claude_mpm/core/socketio_pool.py +3 -3
  19. claude_mpm/core/unified_agent_registry.py +5 -15
  20. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  26. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  27. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  30. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  31. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  33. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  34. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  35. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  36. claude_mpm/scripts/launch_monitor.py +93 -13
  37. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  38. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  39. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
  40. claude_mpm/services/agents/git_source_manager.py +20 -0
  41. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  42. claude_mpm/services/agents/toolchain_detector.py +6 -5
  43. claude_mpm/services/analysis/__init__.py +11 -1
  44. claude_mpm/services/analysis/clone_detector.py +1030 -0
  45. claude_mpm/services/command_deployment_service.py +0 -2
  46. claude_mpm/services/event_bus/config.py +3 -1
  47. claude_mpm/services/monitor/daemon.py +9 -2
  48. claude_mpm/services/monitor/daemon_manager.py +39 -3
  49. claude_mpm/services/monitor/server.py +225 -19
  50. claude_mpm/services/socketio/event_normalizer.py +15 -1
  51. claude_mpm/services/socketio/server/core.py +160 -21
  52. claude_mpm/services/version_control/git_operations.py +103 -0
  53. claude_mpm/utils/agent_filters.py +17 -44
  54. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
  55. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
  56. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  57. claude_mpm/dashboard/analysis_runner.py +0 -455
  58. claude_mpm/dashboard/index.html +0 -13
  59. claude_mpm/dashboard/open_dashboard.py +0 -66
  60. claude_mpm/dashboard/static/css/activity.css +0 -1958
  61. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  62. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  63. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  64. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  65. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  66. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  67. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  68. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  69. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  70. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  71. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  72. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  73. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  74. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  75. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  76. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  77. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  78. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  79. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  80. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  81. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  82. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  83. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  84. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  85. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  86. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  87. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  88. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  89. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  90. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  91. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  92. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  93. claude_mpm/dashboard/templates/code_simple.html +0 -153
  94. claude_mpm/dashboard/templates/index.html +0 -606
  95. claude_mpm/dashboard/test_dashboard.html +0 -372
  96. claude_mpm/scripts/mcp_server.py +0 -75
  97. claude_mpm/scripts/mcp_wrapper.py +0 -39
  98. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  99. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  100. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  101. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  102. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  103. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  104. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  105. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  106. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  107. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  108. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  109. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  110. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  111. claude_mpm/services/mcp_gateway/main.py +0 -589
  112. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  113. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  114. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  115. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  116. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  117. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  118. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  119. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  120. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  121. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  122. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  123. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  124. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  125. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  126. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  127. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  128. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  129. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  130. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  131. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -141,6 +141,14 @@ def execute_command(command: str, args) -> int:
141
141
  result = agent_source_command(args)
142
142
  return result if result is not None else 0
143
143
 
144
+ # Handle summarize command with lazy import
145
+ if command == "summarize":
146
+ # Lazy import to avoid loading unless needed
147
+ from .commands.summarize import summarize_command
148
+
149
+ result = summarize_command(args)
150
+ return result if result is not None else 0
151
+
144
152
  # Handle auto-configure command with lazy import
145
153
  if command == "auto-configure":
146
154
  # Lazy import to avoid loading unless needed
@@ -594,6 +594,11 @@ def create_parser(
594
594
  action="store_true",
595
595
  help="Skip confirmation prompts",
596
596
  )
597
+
598
+ # Add summarize command
599
+ from ..commands.summarize import add_summarize_parser
600
+
601
+ add_summarize_parser(subparsers)
597
602
  except ImportError:
598
603
  # Commands module may not be available during testing or refactoring
599
604
  pass
claude_mpm/cli/startup.py CHANGED
@@ -221,75 +221,76 @@ def discover_and_link_runtime_skills():
221
221
 
222
222
  def deploy_output_style_on_startup():
223
223
  """
224
- Deploy claude-mpm output style to Claude Code on CLI startup.
224
+ Deploy claude-mpm output styles to PROJECT-LEVEL directory on CLI startup.
225
225
 
226
- WHY: Automatically deploy and activate the output style to ensure consistent,
227
- professional communication without emojis and exclamation points. This ensures
228
- the style is available even when using Claude Code directly (not via chat command).
226
+ WHY: Automatically deploy output styles to ensure consistent, professional
227
+ communication without emojis and exclamation points. Styles are project-specific
228
+ to allow different projects to have different communication styles.
229
229
 
230
- DESIGN DECISION: This is non-blocking and idempotent. It uses OutputStyleManager
231
- which handles version detection, file deployment, and settings activation.
232
- Only works for Claude Code >= 1.0.83.
230
+ DESIGN DECISION: This is non-blocking and idempotent. Deploys to project-level
231
+ directory (.claude/settings/output-styles/) instead of user-level to maintain
232
+ project isolation.
233
+
234
+ Deploys two styles:
235
+ - claude-mpm-style.md (professional mode)
236
+ - claude-mpm-teacher.md (teaching mode)
233
237
  """
234
238
  try:
239
+ import shutil
235
240
  from pathlib import Path
236
241
 
237
- from ..core.output_style_manager import OutputStyleManager
238
-
239
- # Create OutputStyleManager instance
240
- output_style_manager = OutputStyleManager()
241
-
242
- # Check if Claude Code supports output styles
243
- if not output_style_manager.supports_output_styles():
244
- # Silently skip - version too old or Claude not installed
245
- return
246
-
247
- # Check if already deployed and active
248
- settings_file = Path.home() / ".claude" / "settings.json"
249
- output_style_file = Path.home() / ".claude" / "output-styles" / "claude-mpm.md"
250
-
251
- already_configured = False
252
- if settings_file.exists() and output_style_file.exists():
253
- try:
254
- import json
255
-
256
- # Check if file has content (bug fix: was skipping empty files)
257
- if output_style_file.stat().st_size == 0:
258
- # File is empty, need to redeploy with content
259
- pass # Fall through to deployment below
260
- else:
261
- # File has content, check if already active
262
- settings = json.loads(settings_file.read_text())
263
- if settings.get("activeOutputStyle") == "claude-mpm":
264
- # Already deployed and active with content
265
- already_configured = True
266
- except Exception:
267
- pass # Continue with deployment if we can't read settings
242
+ # Source files (in framework package)
243
+ package_dir = Path(__file__).parent.parent / "agents"
244
+ professional_source = package_dir / "CLAUDE_MPM_OUTPUT_STYLE.md"
245
+ teacher_source = package_dir / "CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md"
246
+
247
+ # Target directory (PROJECT-LEVEL, not user-level)
248
+ project_dir = Path.cwd()
249
+ output_styles_dir = project_dir / ".claude" / "settings" / "output-styles"
250
+ professional_target = output_styles_dir / "claude-mpm-style.md"
251
+ teacher_target = output_styles_dir / "claude-mpm-teacher.md"
252
+
253
+ # Create directory if it doesn't exist
254
+ output_styles_dir.mkdir(parents=True, exist_ok=True)
255
+
256
+ # Check if already deployed (both files exist and have content)
257
+ already_deployed = (
258
+ professional_target.exists()
259
+ and teacher_target.exists()
260
+ and professional_target.stat().st_size > 0
261
+ and teacher_target.stat().st_size > 0
262
+ )
268
263
 
269
- if already_configured:
270
- # Show feedback that output style is ready
271
- print("✓ Output style configured", flush=True)
264
+ if already_deployed:
265
+ # Show feedback that output styles are ready
266
+ print("✓ Output styles ready", flush=True)
272
267
  return
273
268
 
274
- # Read OUTPUT_STYLE.md content
275
- output_style_path = Path(__file__).parent.parent / "agents" / "OUTPUT_STYLE.md"
269
+ # Deploy both styles
270
+ deployed_count = 0
271
+ if professional_source.exists():
272
+ shutil.copy2(professional_source, professional_target)
273
+ deployed_count += 1
276
274
 
277
- if not output_style_path.exists():
278
- # No output style file to deploy
279
- return
275
+ if teacher_source.exists():
276
+ shutil.copy2(teacher_source, teacher_target)
277
+ deployed_count += 1
280
278
 
281
- output_style_content = output_style_path.read_text()
279
+ if deployed_count > 0:
280
+ print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
281
+ else:
282
+ # Source files missing - log but don't fail
283
+ from ..core.logger import get_logger
282
284
 
283
- # Deploy the output style (deploys file and activates it)
284
- output_style_manager.deploy_output_style(output_style_content)
285
- print("✓ Output style configured", flush=True)
285
+ logger = get_logger("cli")
286
+ logger.debug("Output style source files not found")
286
287
 
287
288
  except Exception as e:
288
289
  # Non-critical - log but don't fail startup
289
290
  from ..core.logger import get_logger
290
291
 
291
292
  logger = get_logger("cli")
292
- logger.debug(f"Failed to deploy output style: {e}")
293
+ logger.debug(f"Failed to deploy output styles: {e}")
293
294
  # Continue execution - output style deployment shouldn't block startup
294
295
 
295
296
 
@@ -386,6 +387,7 @@ def sync_remote_agents_on_startup():
386
387
  # 1. Must have "/agents/" in path (from git repos)
387
388
  # 2. Must not be in PM templates or doc files
388
389
  # 3. Exclude BASE-AGENT.md which is not a deployable agent
390
+ # 4. Exclude build artifacts (dist/, build/, .cache/) to prevent double-counting
389
391
  agent_files = [
390
392
  f
391
393
  for f in all_md_files
@@ -396,6 +398,11 @@ def sync_remote_agents_on_startup():
396
398
  and f.name.lower() not in pm_templates
397
399
  and f.name.lower() not in doc_files
398
400
  and f.name.lower() != "base-agent.md"
401
+ # Exclude build artifacts (prevents double-counting source + built files)
402
+ and not any(
403
+ part in str(f).split("/")
404
+ for part in ["dist", "build", ".cache"]
405
+ )
399
406
  )
400
407
  ]
401
408
  agent_count = len(agent_files)
@@ -409,8 +416,8 @@ def sync_remote_agents_on_startup():
409
416
  show_counter=True,
410
417
  )
411
418
 
412
- # Deploy agents with progress callback
413
- deploy_target = Path.home() / ".claude" / "agents"
419
+ # Deploy agents to project-level directory where Claude Code expects them
420
+ deploy_target = Path.cwd() / ".claude" / "agents"
414
421
  deployment_result = deployment_service.deploy_agents(
415
422
  target_dir=deploy_target,
416
423
  force_rebuild=False, # Only deploy if versions differ
@@ -1,10 +1,9 @@
1
1
  ---
2
- namespace: mpm/ticket
2
+ namespace: mpm/system
3
3
  command: organize
4
- aliases: [mpm-ticket-organize]
5
- migration_target: /mpm/ticket:organize
6
- category: tickets
7
- deprecated_aliases: [mpm-organize]
4
+ aliases: [mpm-organize]
5
+ migration_target: /mpm/system:organize
6
+ category: system
8
7
  description: Organize project files into proper directories with intelligent pattern detection
9
8
  ---
10
9
  # /mpm-organize
@@ -316,6 +316,33 @@ class AgentSourceConfiguration:
316
316
 
317
317
  return errors
318
318
 
319
+ def list_sources(self) -> list[dict]:
320
+ """Return list of source configurations as dictionaries.
321
+
322
+ This method converts GitRepository objects to dictionaries for CLI
323
+ and API compatibility. Called by GitSourceManager and CLI commands.
324
+
325
+ Returns:
326
+ List of dicts with keys: identifier, url, subdirectory, enabled, priority
327
+
328
+ Example:
329
+ >>> config = AgentSourceConfiguration()
330
+ >>> sources = config.list_sources()
331
+ >>> for source in sources:
332
+ ... print(f"{source['identifier']} (priority: {source['priority']})")
333
+ """
334
+ repos = self.get_enabled_repositories()
335
+ return [
336
+ {
337
+ "identifier": repo.identifier,
338
+ "url": repo.url,
339
+ "subdirectory": repo.subdirectory,
340
+ "enabled": repo.enabled,
341
+ "priority": repo.priority,
342
+ }
343
+ for repo in repos
344
+ ]
345
+
319
346
  def __repr__(self) -> str:
320
347
  """Return string representation of configuration."""
321
348
  return (
@@ -115,7 +115,11 @@ class AgentLoader:
115
115
  return agents
116
116
 
117
117
  def discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
118
- """Discover local JSON agent templates from .claude-mpm/agents/ directories.
118
+ """Discover local JSON agent templates.
119
+
120
+ NOTE: This method is kept for backward compatibility but is deprecated.
121
+ The new architecture uses SOURCE (~/.claude-mpm/cache/remote-agents/)
122
+ and DEPLOYMENT (.claude/agents/) locations only.
119
123
 
120
124
  Returns:
121
125
  Dictionary mapping agent IDs to agent metadata
@@ -125,11 +129,10 @@ class AgentLoader:
125
129
  local_agents = {}
126
130
 
127
131
  # Check for local JSON templates in priority order
132
+ # NOTE: These directories are deprecated in the simplified architecture
128
133
  template_dirs = [
129
- Path.cwd()
130
- / ".claude-mpm"
131
- / "agents", # Project local agents (highest priority)
132
- Path.home() / ".claude-mpm" / "agents", # User local agents
134
+ Path.cwd() / ".claude-mpm" / "agents", # Deprecated: Project local agents
135
+ Path.home() / ".claude-mpm" / "agents", # Deprecated: User local agents
133
136
  ]
134
137
 
135
138
  for priority, template_dir in enumerate(template_dirs):
@@ -55,8 +55,8 @@ class CircuitState(Enum):
55
55
  class ConnectionStats:
56
56
  """Connection statistics for monitoring."""
57
57
 
58
- created_at: datetime = field(default_factory=datetime.now)
59
- last_used: datetime = field(default_factory=datetime.now)
58
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
59
+ last_used: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
60
60
  events_sent: int = 0
61
61
  errors: int = 0
62
62
  consecutive_errors: int = 0
@@ -70,7 +70,7 @@ class BatchEvent:
70
70
  namespace: str
71
71
  event: str
72
72
  data: Dict[str, Any]
73
- timestamp: datetime = field(default_factory=datetime.now)
73
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
74
74
 
75
75
 
76
76
  class CircuitBreaker:
@@ -174,26 +174,16 @@ class UnifiedAgentRegistry:
174
174
  if project_path.exists():
175
175
  self.discovery_paths.append(project_path)
176
176
 
177
- # Also check for local JSON templates in .claude-mpm/agents/
178
- local_project_path = self.path_manager.project_root / ".claude-mpm" / "agents"
179
- if (
180
- local_project_path.exists()
181
- and local_project_path not in self.discovery_paths
182
- ):
183
- self.discovery_paths.append(local_project_path)
184
- logger.debug(f"Added local project templates path: {local_project_path}")
177
+ # NOTE: .claude-mpm/agents/ is deprecated in the simplified architecture
178
+ # Source agents come from ~/.claude-mpm/cache/remote-agents/
179
+ # Deployed agents go to .claude/agents/
185
180
 
186
- # User-level agents
181
+ # User-level agents (deprecated in simplified architecture)
182
+ # Keeping for backward compatibility but not actively used
187
183
  user_path = self.path_manager.get_user_agents_dir()
188
184
  if user_path.exists():
189
185
  self.discovery_paths.append(user_path)
190
186
 
191
- # Also check for user JSON templates in ~/.claude-mpm/agents/
192
- local_user_path = Path.home() / ".claude-mpm" / "agents"
193
- if local_user_path.exists() and local_user_path not in self.discovery_paths:
194
- self.discovery_paths.append(local_user_path)
195
- logger.debug(f"Added local user templates path: {local_user_path}")
196
-
197
187
  # System-level agents (includes templates as a subdirectory)
198
188
  system_path = self.path_manager.get_system_agents_dir()
199
189
  if system_path.exists():
@@ -0,0 +1,60 @@
1
+ """Cross-process correlation storage using .claude-mpm directory."""
2
+
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+
7
+
8
+ def get_correlation_dir() -> Path:
9
+ """Get correlation directory in project's .claude-mpm folder."""
10
+ # Use CWD's .claude-mpm directory (where hooks run from)
11
+ cwd = Path.cwd()
12
+ return cwd / ".claude-mpm" / "correlations"
13
+
14
+
15
+ TTL_SECONDS = 3600 # 1 hour
16
+
17
+
18
+ class CorrelationManager:
19
+ """Manages correlation IDs across separate hook processes."""
20
+
21
+ @staticmethod
22
+ def store(session_id: str, tool_call_id: str, tool_name: str) -> None:
23
+ """Store correlation data for later retrieval by post_tool."""
24
+ correlation_dir = get_correlation_dir()
25
+ correlation_dir.mkdir(parents=True, exist_ok=True)
26
+ filepath = correlation_dir / f"correlation_{session_id}.json"
27
+ data = {
28
+ "tool_call_id": tool_call_id,
29
+ "tool_name": tool_name,
30
+ "timestamp": time.time(),
31
+ }
32
+ filepath.write_text(json.dumps(data))
33
+
34
+ @staticmethod
35
+ def retrieve(session_id: str) -> str | None:
36
+ """Retrieve and delete correlation data from temp file."""
37
+ correlation_dir = get_correlation_dir()
38
+ filepath = correlation_dir / f"correlation_{session_id}.json"
39
+ if not filepath.exists():
40
+ return None
41
+ try:
42
+ data = json.loads(filepath.read_text())
43
+ filepath.unlink() # Delete after reading
44
+ return data.get("tool_call_id")
45
+ except (json.JSONDecodeError, OSError):
46
+ return None
47
+
48
+ @staticmethod
49
+ def cleanup_old() -> None:
50
+ """Remove correlation files older than TTL."""
51
+ correlation_dir = get_correlation_dir()
52
+ if not correlation_dir.exists():
53
+ return
54
+ now = time.time()
55
+ for filepath in correlation_dir.glob("correlation_*.json"):
56
+ try:
57
+ if now - filepath.stat().st_mtime > TTL_SECONDS:
58
+ filepath.unlink()
59
+ except OSError:
60
+ pass
@@ -9,6 +9,7 @@ import os
9
9
  import re
10
10
  import subprocess
11
11
  import sys
12
+ import uuid
12
13
  from datetime import datetime, timezone
13
14
  from pathlib import Path
14
15
  from typing import Optional
@@ -134,6 +135,9 @@ class EventHandlers:
134
135
  tool_name = event.get("tool_name", "")
135
136
  tool_input = event.get("tool_input", {})
136
137
 
138
+ # Generate unique tool call ID for correlation with post_tool event
139
+ tool_call_id = str(uuid.uuid4())
140
+
137
141
  # Extract key parameters based on tool type
138
142
  tool_params = extract_tool_parameters(tool_name, tool_input)
139
143
 
@@ -144,6 +148,8 @@ class EventHandlers:
144
148
  working_dir = event.get("cwd", "")
145
149
  git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
146
150
 
151
+ timestamp = datetime.now(timezone.utc).isoformat()
152
+
147
153
  pre_tool_data = {
148
154
  "tool_name": tool_name,
149
155
  "operation_type": operation_type,
@@ -151,15 +157,27 @@ class EventHandlers:
151
157
  "session_id": event.get("session_id", ""),
152
158
  "working_directory": working_dir,
153
159
  "git_branch": git_branch,
154
- "timestamp": datetime.now(timezone.utc).isoformat(),
160
+ "timestamp": timestamp,
155
161
  "parameter_count": len(tool_input) if isinstance(tool_input, dict) else 0,
156
162
  "is_file_operation": tool_name
157
163
  in ["Write", "Edit", "MultiEdit", "Read", "LS", "Glob"],
158
164
  "is_execution": tool_name in ["Bash", "NotebookEdit"],
159
165
  "is_delegation": tool_name == "Task",
160
166
  "security_risk": assess_security_risk(tool_name, tool_input),
167
+ "correlation_id": tool_call_id, # Add correlation_id for pre/post correlation
161
168
  }
162
169
 
170
+ # Store tool_call_id using CorrelationManager for cross-process retrieval
171
+ if session_id:
172
+ from .correlation_manager import CorrelationManager
173
+
174
+ CorrelationManager.store(session_id, tool_call_id, tool_name)
175
+ if DEBUG:
176
+ print(
177
+ f" - Generated tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}...",
178
+ file=sys.stderr,
179
+ )
180
+
163
181
  # Add delegation-specific data if this is a Task tool
164
182
  if tool_name == "Task" and isinstance(tool_input, dict):
165
183
  self._handle_task_delegation(tool_input, pre_tool_data, session_id)
@@ -375,6 +393,7 @@ class EventHandlers:
375
393
  """
376
394
  tool_name = event.get("tool_name", "")
377
395
  exit_code = event.get("exit_code", 0)
396
+ session_id = event.get("session_id", "")
378
397
 
379
398
  # Extract result data
380
399
  result_data = extract_tool_results(event)
@@ -386,6 +405,16 @@ class EventHandlers:
386
405
  working_dir = event.get("cwd", "")
387
406
  git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
388
407
 
408
+ # Retrieve tool_call_id using CorrelationManager for cross-process correlation
409
+ from .correlation_manager import CorrelationManager
410
+
411
+ tool_call_id = CorrelationManager.retrieve(session_id) if session_id else None
412
+ if DEBUG and tool_call_id:
413
+ print(
414
+ f" - Retrieved tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}...",
415
+ file=sys.stderr,
416
+ )
417
+
389
418
  post_tool_data = {
390
419
  "tool_name": tool_name,
391
420
  "exit_code": exit_code,
@@ -399,7 +428,7 @@ class EventHandlers:
399
428
  ),
400
429
  "duration_ms": duration,
401
430
  "result_summary": result_data,
402
- "session_id": event.get("session_id", ""),
431
+ "session_id": session_id,
403
432
  "working_directory": working_dir,
404
433
  "git_branch": git_branch,
405
434
  "timestamp": datetime.now(timezone.utc).isoformat(),
@@ -412,6 +441,10 @@ class EventHandlers:
412
441
  ),
413
442
  }
414
443
 
444
+ # Add correlation_id if available for correlation with pre_tool
445
+ if tool_call_id:
446
+ post_tool_data["correlation_id"] = tool_call_id
447
+
415
448
  # Handle Task delegation completion for memory hooks and response tracking
416
449
  if tool_name == "Task":
417
450
  session_id = event.get("session_id", "")
@@ -304,6 +304,10 @@ class ClaudeHookHandler:
304
304
  # Perform periodic cleanup if needed
305
305
  if self.state_manager.increment_events_processed():
306
306
  self.state_manager.cleanup_old_entries()
307
+ # Also cleanup old correlation files
308
+ from .correlation_manager import CorrelationManager
309
+
310
+ CorrelationManager.cleanup_old()
307
311
  if DEBUG:
308
312
  print(
309
313
  f"🧹 Performed cleanup after {self.state_manager.events_processed} events",
@@ -115,6 +115,9 @@ class ConnectionManagerService:
115
115
  - Fallback: HTTP POST for reliability when direct connection fails
116
116
  - Eliminates duplicate events from multiple emission paths
117
117
  """
118
+ # Extract tool_call_id from data if present for correlation
119
+ tool_call_id = data.get("tool_call_id")
120
+
118
121
  # Create event data for normalization
119
122
  raw_event = {
120
123
  "type": "hook",
@@ -123,6 +126,7 @@ class ConnectionManagerService:
123
126
  "data": data,
124
127
  "source": "claude_hooks", # Identify the source
125
128
  "session_id": data.get("sessionId"), # Include session if available
129
+ "correlation_id": tool_call_id, # Set from tool_call_id for event correlation
126
130
  }
127
131
 
128
132
  # Normalize the event using EventNormalizer for consistent schema