claude-mpm 4.0.19__py3-none-any.whl → 4.0.22__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 (61) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/__main__.py +4 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
  5. claude_mpm/agents/INSTRUCTIONS.md +74 -0
  6. claude_mpm/agents/OUTPUT_STYLE.md +84 -0
  7. claude_mpm/agents/WORKFLOW.md +308 -4
  8. claude_mpm/agents/agents_metadata.py +52 -0
  9. claude_mpm/agents/base_agent_loader.py +75 -19
  10. claude_mpm/agents/templates/__init__.py +4 -0
  11. claude_mpm/agents/templates/api_qa.json +206 -0
  12. claude_mpm/agents/templates/qa.json +1 -1
  13. claude_mpm/agents/templates/research.json +24 -16
  14. claude_mpm/agents/templates/ticketing.json +18 -5
  15. claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
  16. claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
  17. claude_mpm/cli/__init__.py +23 -1
  18. claude_mpm/cli/__main__.py +4 -0
  19. claude_mpm/cli/commands/mcp_command_router.py +87 -1
  20. claude_mpm/cli/commands/mcp_install_commands.py +207 -26
  21. claude_mpm/cli/commands/memory.py +32 -5
  22. claude_mpm/cli/commands/run.py +33 -6
  23. claude_mpm/cli/parsers/base_parser.py +5 -0
  24. claude_mpm/cli/parsers/mcp_parser.py +23 -0
  25. claude_mpm/cli/parsers/run_parser.py +5 -0
  26. claude_mpm/cli/utils.py +17 -4
  27. claude_mpm/constants.py +1 -0
  28. claude_mpm/core/base_service.py +8 -2
  29. claude_mpm/core/config.py +122 -32
  30. claude_mpm/core/framework_loader.py +385 -34
  31. claude_mpm/core/interactive_session.py +77 -12
  32. claude_mpm/core/oneshot_session.py +7 -1
  33. claude_mpm/core/output_style_manager.py +468 -0
  34. claude_mpm/core/unified_paths.py +190 -21
  35. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
  36. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
  37. claude_mpm/init.py +1 -0
  38. claude_mpm/scripts/socketio_daemon.py +67 -7
  39. claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
  40. claude_mpm/services/agents/deployment/agent_deployment.py +216 -10
  41. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
  42. claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
  43. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
  44. claude_mpm/services/agents/memory/__init__.py +0 -2
  45. claude_mpm/services/agents/memory/agent_memory_manager.py +577 -44
  46. claude_mpm/services/agents/memory/content_manager.py +144 -14
  47. claude_mpm/services/agents/memory/template_generator.py +7 -354
  48. claude_mpm/services/mcp_gateway/server/stdio_server.py +61 -169
  49. claude_mpm/services/memory_hook_service.py +62 -4
  50. claude_mpm/services/runner_configuration_service.py +5 -9
  51. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  52. claude_mpm/services/socketio/server/core.py +4 -0
  53. claude_mpm/services/socketio/server/main.py +23 -4
  54. claude_mpm/services/subprocess_launcher_service.py +5 -0
  55. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/METADATA +1 -1
  56. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/RECORD +60 -54
  57. claude_mpm/services/agents/memory/analyzer.py +0 -430
  58. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/WHEEL +0 -0
  59. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/entry_points.txt +0 -0
  60. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/licenses/LICENSE +0 -0
  61. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,7 @@ import asyncio
16
16
  import json
17
17
  import logging
18
18
  import sys
19
- from typing import Any, Dict, List, Optional
19
+ from typing import Any, Dict, Optional
20
20
 
21
21
  # Import MCP SDK components
22
22
  from mcp.server import NotificationOptions, Server
@@ -190,7 +190,7 @@ class SimpleMCPServer:
190
190
  # Default to brief
191
191
  return self._create_brief_summary(sentences, max_length)
192
192
 
193
- def _create_brief_summary(self, sentences: List[str], max_length: int) -> str:
193
+ def _create_brief_summary(self, sentences: list[str], max_length: int) -> str:
194
194
  """Create a brief summary by selecting most important sentences."""
195
195
  if not sentences:
196
196
  return ""
@@ -272,7 +272,7 @@ class SimpleMCPServer:
272
272
  return " ".join(s[1] for s in selected)
273
273
 
274
274
  def _create_detailed_summary(
275
- self, sentences: List[str], content: str, max_length: int
275
+ self, sentences: list[str], content: str, max_length: int
276
276
  ) -> str:
277
277
  """Create a detailed summary preserving document structure."""
278
278
  import re
@@ -303,7 +303,7 @@ class SimpleMCPServer:
303
303
  return " ".join(words) + ("..." if len(result.split()) > max_length else "")
304
304
 
305
305
  def _create_bullet_summary(
306
- self, sentences: List[str], content: str, max_length: int
306
+ self, sentences: list[str], content: str, max_length: int
307
307
  ) -> str:
308
308
  """Extract key points as a bullet list."""
309
309
  import re
@@ -347,7 +347,7 @@ class SimpleMCPServer:
347
347
  return "\n".join(result_lines)
348
348
 
349
349
  def _create_executive_summary(
350
- self, sentences: List[str], content: str, max_length: int
350
+ self, sentences: list[str], content: str, max_length: int
351
351
  ) -> str:
352
352
  """Create an executive summary with overview, findings, and recommendations."""
353
353
  # Allocate words across sections
@@ -436,32 +436,9 @@ class SimpleMCPServer:
436
436
  # NOTE: Defer initialization to avoid event loop issues
437
437
  self.unified_ticket_tool = None
438
438
  self._ticket_tool_initialized = False
439
-
440
- async def _initialize_ticket_tool(self):
441
- """
442
- Initialize the unified ticket tool asynchronously.
443
-
444
- This is called lazily when the tool is first needed,
445
- ensuring an event loop is available.
446
- """
447
- if self._ticket_tool_initialized or not TICKET_TOOLS_AVAILABLE:
448
- return
449
439
 
450
- try:
451
- self.logger.info("Initializing unified ticket tool...")
452
- self.unified_ticket_tool = UnifiedTicketTool()
453
- # If the tool has an async init method, call it
454
- if hasattr(self.unified_ticket_tool, 'initialize'):
455
- await self.unified_ticket_tool.initialize()
456
- self._ticket_tool_initialized = True
457
- self.logger.info("Unified ticket tool initialized successfully")
458
- except Exception as e:
459
- self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
460
- self.unified_ticket_tool = None
461
- self._ticket_tool_initialized = True # Mark as attempted
462
-
463
440
  @self.server.list_tools()
464
- async def handle_list_tools() -> List[Tool]:
441
+ async def handle_list_tools() -> list[Tool]:
465
442
  """List available tools."""
466
443
  # Initialize ticket tool lazily if needed
467
444
  if not self._ticket_tool_initialized and TICKET_TOOLS_AVAILABLE:
@@ -469,69 +446,22 @@ class SimpleMCPServer:
469
446
 
470
447
  tools = [
471
448
  Tool(
472
- name="echo",
473
- description="Echo back the provided message",
474
- inputSchema={
475
- "type": "object",
476
- "properties": {
477
- "message": {
478
- "type": "string",
479
- "description": "Message to echo",
480
- }
481
- },
482
- "required": ["message"],
483
- },
484
- ),
485
- Tool(
486
- name="calculator",
487
- description="Perform basic arithmetic calculations",
488
- inputSchema={
489
- "type": "object",
490
- "properties": {
491
- "expression": {
492
- "type": "string",
493
- "description": "Mathematical expression to evaluate",
494
- }
495
- },
496
- "required": ["expression"],
497
- },
498
- ),
499
- Tool(
500
- name="system_info",
501
- description="Get system information",
449
+ name="status",
450
+ description="Get system and service status information",
502
451
  inputSchema={
503
452
  "type": "object",
504
453
  "properties": {
505
454
  "info_type": {
506
455
  "type": "string",
507
- "enum": ["platform", "python_version", "cwd"],
508
- "description": "Type of system information to retrieve",
456
+ "enum": ["platform", "python_version", "cwd", "all"],
457
+ "description": "Type of status information to retrieve (default: all)",
458
+ "default": "all",
509
459
  }
510
460
  },
511
- "required": ["info_type"],
512
- },
513
- ),
514
- Tool(
515
- name="run_command",
516
- description="Execute a shell command",
517
- inputSchema={
518
- "type": "object",
519
- "properties": {
520
- "command": {
521
- "type": "string",
522
- "description": "Shell command to execute",
523
- },
524
- "timeout": {
525
- "type": "number",
526
- "description": "Command timeout in seconds",
527
- "default": 30,
528
- },
529
- },
530
- "required": ["command"],
531
461
  },
532
462
  ),
533
463
  Tool(
534
- name="summarize_document",
464
+ name="document_summarizer",
535
465
  description="Summarize documents or text content",
536
466
  inputSchema={
537
467
  "type": "object",
@@ -576,60 +506,17 @@ class SimpleMCPServer:
576
506
  self.logger.info(f"Listing {len(tools)} available tools")
577
507
  return tools
578
508
 
509
+
579
510
  @self.server.call_tool()
580
511
  async def handle_call_tool(
581
512
  name: str, arguments: Dict[str, Any]
582
- ) -> List[TextContent]:
513
+ ) -> list[TextContent]:
583
514
  """Handle tool invocation."""
584
515
  self.logger.info(f"Invoking tool: {name} with arguments: {arguments}")
585
516
 
586
517
  try:
587
- if name == "echo":
588
- message = arguments.get("message", "")
589
- result = f"Echo: {message}"
590
-
591
- elif name == "calculator":
592
- expression = arguments.get("expression", "")
593
- try:
594
- # Safe evaluation of mathematical expressions
595
- import ast
596
- import operator as op
597
-
598
- # Supported operators
599
- ops = {
600
- ast.Add: op.add,
601
- ast.Sub: op.sub,
602
- ast.Mult: op.mul,
603
- ast.Div: op.truediv,
604
- ast.Pow: op.pow,
605
- ast.Mod: op.mod,
606
- ast.USub: op.neg,
607
- }
608
-
609
- def eval_expr(expr):
610
- """Safely evaluate mathematical expression."""
611
-
612
- def _eval(node):
613
- if isinstance(node, ast.Constant):
614
- return node.value
615
- elif isinstance(node, ast.BinOp):
616
- return ops[type(node.op)](
617
- _eval(node.left), _eval(node.right)
618
- )
619
- elif isinstance(node, ast.UnaryOp):
620
- return ops[type(node.op)](_eval(node.operand))
621
- else:
622
- raise TypeError(f"Unsupported operation: {node}")
623
-
624
- return _eval(ast.parse(expr, mode="eval").body)
625
-
626
- result_value = eval_expr(expression)
627
- result = f"{expression} = {result_value}"
628
- except Exception as e:
629
- result = f"Error evaluating expression: {str(e)}"
630
-
631
- elif name == "system_info":
632
- info_type = arguments.get("info_type", "platform")
518
+ if name == "status":
519
+ info_type = arguments.get("info_type", "all")
633
520
 
634
521
  if info_type == "platform":
635
522
  import platform
@@ -643,49 +530,25 @@ class SimpleMCPServer:
643
530
  import os
644
531
 
645
532
  result = f"Working Directory: {os.getcwd()}"
533
+ elif info_type == "all":
534
+ import platform
535
+ import sys
536
+ import os
537
+ import datetime
538
+
539
+ result = (
540
+ f"=== System Status ===\n"
541
+ f"Platform: {platform.system()} {platform.release()}\n"
542
+ f"Python: {sys.version.split()[0]}\n"
543
+ f"Working Directory: {os.getcwd()}\n"
544
+ f"Server: {self.name} v{self.version}\n"
545
+ f"Timestamp: {datetime.datetime.now().isoformat()}\n"
546
+ f"Tools Available: status, document_summarizer{', ticket' if self.unified_ticket_tool else ''}"
547
+ )
646
548
  else:
647
549
  result = f"Unknown info type: {info_type}"
648
550
 
649
- elif name == "run_command":
650
- command = arguments.get("command", "")
651
- timeout = arguments.get("timeout", 30)
652
-
653
- import shlex
654
- import subprocess
655
-
656
- try:
657
- # Split command string into a list to avoid shell injection
658
- command_parts = shlex.split(command)
659
-
660
- # Use create_subprocess_exec instead of create_subprocess_shell
661
- # to prevent command injection vulnerabilities
662
- proc = await asyncio.create_subprocess_exec(
663
- *command_parts,
664
- stdout=subprocess.PIPE,
665
- stderr=subprocess.PIPE,
666
- )
667
-
668
- stdout, stderr = await asyncio.wait_for(
669
- proc.communicate(), timeout=timeout
670
- )
671
-
672
- if proc.returncode == 0:
673
- result = (
674
- stdout.decode()
675
- if stdout
676
- else "Command completed successfully"
677
- )
678
- else:
679
- result = f"Command failed with code {proc.returncode}: {stderr.decode()}"
680
- except asyncio.TimeoutError:
681
- result = f"Command timed out after {timeout} seconds"
682
- except ValueError as e:
683
- # Handle shlex parsing errors (e.g., unmatched quotes)
684
- result = f"Invalid command syntax: {str(e)}"
685
- except Exception as e:
686
- result = f"Error running command: {str(e)}"
687
-
688
- elif name == "summarize_document":
551
+ elif name == "document_summarizer":
689
552
  content = arguments.get("content", "")
690
553
  style = arguments.get("style", "brief")
691
554
  max_length = arguments.get("max_length", 150)
@@ -734,6 +597,29 @@ class SimpleMCPServer:
734
597
  return [TextContent(type="text", text=error_msg)]
735
598
 
736
599
 
600
+ async def _initialize_ticket_tool(self):
601
+ """
602
+ Initialize the unified ticket tool asynchronously.
603
+
604
+ This is called lazily when the tool is first needed,
605
+ ensuring an event loop is available.
606
+ """
607
+ if self._ticket_tool_initialized or not TICKET_TOOLS_AVAILABLE:
608
+ return
609
+
610
+ try:
611
+ self.logger.info("Initializing unified ticket tool...")
612
+ self.unified_ticket_tool = UnifiedTicketTool()
613
+ # If the tool has an async init method, call it
614
+ if hasattr(self.unified_ticket_tool, 'initialize'):
615
+ await self.unified_ticket_tool.initialize()
616
+ self._ticket_tool_initialized = True
617
+ self.logger.info("Unified ticket tool initialized successfully")
618
+ except Exception as e:
619
+ self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
620
+ self.unified_ticket_tool = None
621
+ self._ticket_tool_initialized = True # Mark as attempted
622
+
737
623
  async def run(self):
738
624
  """
739
625
  Run the MCP server using stdio communication with backward compatibility.
@@ -799,9 +685,15 @@ async def main():
799
685
 
800
686
  def main_sync():
801
687
  """Synchronous entry point for use as a console script."""
688
+ import os
689
+ # Disable telemetry by default
690
+ os.environ.setdefault('DISABLE_TELEMETRY', '1')
802
691
  asyncio.run(main())
803
692
 
804
693
 
805
694
  if __name__ == "__main__":
695
+ import os
696
+ # Disable telemetry by default
697
+ os.environ.setdefault('DISABLE_TELEMETRY', '1')
806
698
  # Run the async main function
807
699
  main_sync()
@@ -143,11 +143,69 @@ class MemoryHookService(BaseService, MemoryHookInterface):
143
143
  HookResult with success status and any modifications
144
144
  """
145
145
  try:
146
- # This would integrate with a memory service to save new memories
147
- # For now, this is a placeholder for future memory integration
148
- self.logger.debug("Saving new memories from interaction")
149
-
150
146
  from claude_mpm.hooks.base_hook import HookResult
147
+
148
+ # Extract agent_id and response from context
149
+ agent_id = None
150
+ response_text = None
151
+
152
+ # Try to get agent_id from various possible locations in context
153
+ if hasattr(context, 'data') and context.data:
154
+ data = context.data
155
+
156
+ # Check for agent_id in various locations
157
+ if isinstance(data, dict):
158
+ # Try direct agent_id field
159
+ agent_id = data.get('agent_id')
160
+
161
+ # Try agent_type field
162
+ if not agent_id:
163
+ agent_id = data.get('agent_type')
164
+
165
+ # Try subagent_type (for Task delegations)
166
+ if not agent_id:
167
+ agent_id = data.get('subagent_type')
168
+
169
+ # Try tool_parameters for Task delegations
170
+ if not agent_id and 'tool_parameters' in data:
171
+ params = data.get('tool_parameters', {})
172
+ if isinstance(params, dict):
173
+ agent_id = params.get('subagent_type')
174
+
175
+ # Extract response text
176
+ response_text = data.get('response') or data.get('result') or data.get('output')
177
+
178
+ # If response_text is a dict, try to get text from it
179
+ if isinstance(response_text, dict):
180
+ response_text = response_text.get('text') or response_text.get('content') or str(response_text)
181
+
182
+ # Default to PM if no agent_id found
183
+ if not agent_id:
184
+ agent_id = "PM"
185
+ self.logger.debug("No agent_id found in context, defaulting to PM")
186
+
187
+ # Only process if we have response text
188
+ if response_text and isinstance(response_text, str):
189
+ self.logger.debug(f"Processing memory extraction for agent: {agent_id}")
190
+
191
+ # Import and use the memory manager
192
+ from claude_mpm.services.agents.memory.agent_memory_manager import get_memory_manager
193
+
194
+ try:
195
+ memory_manager = get_memory_manager()
196
+
197
+ # Extract and update memory
198
+ success = memory_manager.extract_and_update_memory(agent_id, response_text)
199
+
200
+ if success:
201
+ self.logger.info(f"Successfully extracted and saved memories for {agent_id}")
202
+ else:
203
+ self.logger.debug(f"No memories found to extract for {agent_id}")
204
+
205
+ except Exception as mem_error:
206
+ self.logger.warning(f"Failed to extract/save memories for {agent_id}: {mem_error}")
207
+ else:
208
+ self.logger.debug("No response text found in context for memory extraction")
151
209
 
152
210
  return HookResult(success=True, data=context.data, modified=False)
153
211
 
@@ -72,11 +72,12 @@ class RunnerConfigurationService(BaseService, RunnerConfigurationInterface):
72
72
  Loaded configuration dictionary
73
73
  """
74
74
  try:
75
+ # Use singleton Config instance to prevent duplicate loading
75
76
  if config_path:
76
- # Load from specific path if provided
77
- config = Config(config_path)
77
+ # Only pass config_path if it's different from what might already be loaded
78
+ config = Config({}, config_path)
78
79
  else:
79
- # Load from default location
80
+ # Use existing singleton instance
80
81
  config = Config()
81
82
 
82
83
  return {
@@ -162,14 +163,9 @@ class RunnerConfigurationService(BaseService, RunnerConfigurationInterface):
162
163
  "websocket_port": kwargs.get("websocket_port", 8765),
163
164
  }
164
165
 
165
- # Initialize main configuration
166
+ # Initialize main configuration (singleton will prevent duplicate loading)
166
167
  try:
167
168
  config = Config()
168
- except FileNotFoundError as e:
169
- self.logger.warning(
170
- "Configuration file not found, using defaults", extra={"error": str(e)}
171
- )
172
- config = Config() # Will use defaults
173
169
  except Exception as e:
174
170
  self.logger.error("Failed to load configuration", exc_info=True)
175
171
  raise RuntimeError(f"Configuration initialization failed: {e}") from e
@@ -184,10 +184,41 @@ class SocketIOEventBroadcaster:
184
184
 
185
185
  WHY: Failed broadcasts need to be retried automatically
186
186
  to ensure reliable event delivery.
187
+
188
+ IMPORTANT: This method must handle being called from a different thread
189
+ than the one running the event loop.
187
190
  """
188
191
  if self.loop and not self.retry_task:
192
+ try:
193
+ # Check if the loop is running in the current thread
194
+ try:
195
+ running_loop = asyncio.get_running_loop()
196
+ if running_loop == self.loop:
197
+ # Same thread, can use create_task directly
198
+ self.retry_task = asyncio.create_task(self._process_retry_queue())
199
+ self.logger.info("🔄 Started retry queue processor (same thread)")
200
+ else:
201
+ # Different thread, need to schedule in the target loop
202
+ self._start_retry_in_loop()
203
+ except RuntimeError:
204
+ # No running loop in current thread, schedule in target loop
205
+ self._start_retry_in_loop()
206
+ except Exception as e:
207
+ self.logger.error(f"Failed to start retry processor: {e}")
208
+
209
+ def _start_retry_in_loop(self):
210
+ """Helper to start retry processor from a different thread."""
211
+ async def _create_retry_task():
189
212
  self.retry_task = asyncio.create_task(self._process_retry_queue())
190
- self.logger.info("🔄 Started retry queue processor")
213
+ self.logger.info("🔄 Started retry queue processor (cross-thread)")
214
+
215
+ # Schedule the task creation in the target loop
216
+ future = asyncio.run_coroutine_threadsafe(_create_retry_task(), self.loop)
217
+ try:
218
+ # Wait briefly to ensure it's scheduled
219
+ future.result(timeout=1.0)
220
+ except Exception as e:
221
+ self.logger.error(f"Failed to schedule retry processor: {e}")
191
222
 
192
223
  def stop_retry_processor(self):
193
224
  """Stop the background retry processor."""
@@ -138,8 +138,12 @@ class SocketIOServerCore:
138
138
  """Run the server event loop."""
139
139
  try:
140
140
  # Create new event loop for this thread
141
+ # WHY: We create and assign the loop immediately to minimize the race
142
+ # condition window where other threads might try to access it.
141
143
  self.loop = asyncio.new_event_loop()
142
144
  asyncio.set_event_loop(self.loop)
145
+
146
+ self.logger.debug("Event loop created and set for background thread")
143
147
 
144
148
  # Run the server
145
149
  self.loop.run_until_complete(self._start_server())
@@ -117,11 +117,30 @@ class SocketIOServer(SocketIOServiceInterface):
117
117
  server=self, # Pass server reference for event history access
118
118
  )
119
119
 
120
- # Set the loop reference for broadcaster
121
- self.broadcaster.loop = self.core.loop
120
+ # Wait for the event loop to be initialized in the background thread
121
+ # WHY: The core server starts in a background thread and creates the event
122
+ # loop asynchronously. We must wait for it to be ready before using it.
123
+ max_wait = 5.0 # Maximum wait time in seconds
124
+ wait_interval = 0.1 # Check interval
125
+ waited = 0.0
122
126
 
123
- # Start the retry processor for resilient event delivery
124
- self.broadcaster.start_retry_processor()
127
+ while self.core.loop is None and waited < max_wait:
128
+ time.sleep(wait_interval)
129
+ waited += wait_interval
130
+
131
+ if self.core.loop is None:
132
+ self.logger.warning(
133
+ f"Event loop not initialized after {max_wait}s wait. "
134
+ "Retry processor may not function correctly."
135
+ )
136
+ else:
137
+ self.logger.debug(f"Event loop ready after {waited:.1f}s")
138
+
139
+ # Set the loop reference for broadcaster
140
+ self.broadcaster.loop = self.core.loop
141
+
142
+ # Start the retry processor for resilient event delivery
143
+ self.broadcaster.start_retry_processor()
125
144
 
126
145
  # Register events
127
146
  self._register_events()
@@ -313,4 +313,9 @@ class SubprocessLauncherService(BaseService, SubprocessLauncherInterface):
313
313
  env = os.environ.copy()
314
314
  if base_env:
315
315
  env.update(base_env)
316
+
317
+ # Disable telemetry for Claude Code subprocesses
318
+ # This ensures Claude Code doesn't send telemetry data during runtime
319
+ env["DISABLE_TELEMETRY"] = "1"
320
+
316
321
  return env
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.0.19
3
+ Version: 4.0.22
4
4
  Summary: Claude Multi-agent Project Manager - Clean orchestration with ticket management
5
5
  Home-page: https://github.com/bobmatnyc/claude-mpm
6
6
  Author: Claude MPM Team