claude-mpm 3.4.27__py3-none-any.whl → 3.5.0__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 (123) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +182 -299
  3. claude_mpm/agents/agent_loader.py +283 -57
  4. claude_mpm/agents/agent_loader_integration.py +6 -9
  5. claude_mpm/agents/base_agent.json +2 -1
  6. claude_mpm/agents/base_agent_loader.py +1 -1
  7. claude_mpm/cli/__init__.py +5 -7
  8. claude_mpm/cli/commands/__init__.py +0 -2
  9. claude_mpm/cli/commands/agents.py +1 -1
  10. claude_mpm/cli/commands/memory.py +1 -1
  11. claude_mpm/cli/commands/run.py +12 -0
  12. claude_mpm/cli/parser.py +0 -13
  13. claude_mpm/cli/utils.py +1 -1
  14. claude_mpm/config/__init__.py +44 -2
  15. claude_mpm/config/agent_config.py +348 -0
  16. claude_mpm/config/paths.py +322 -0
  17. claude_mpm/constants.py +0 -1
  18. claude_mpm/core/__init__.py +2 -5
  19. claude_mpm/core/agent_registry.py +63 -17
  20. claude_mpm/core/claude_runner.py +354 -43
  21. claude_mpm/core/config.py +7 -1
  22. claude_mpm/core/config_aliases.py +4 -3
  23. claude_mpm/core/config_paths.py +151 -0
  24. claude_mpm/core/factories.py +4 -50
  25. claude_mpm/core/logger.py +11 -13
  26. claude_mpm/core/service_registry.py +2 -2
  27. claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
  28. claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
  30. claude_mpm/hooks/memory_integration_hook.py +1 -1
  31. claude_mpm/init.py +37 -6
  32. claude_mpm/scripts/socketio_daemon.py +6 -2
  33. claude_mpm/services/__init__.py +71 -3
  34. claude_mpm/services/agents/__init__.py +85 -0
  35. claude_mpm/services/agents/deployment/__init__.py +21 -0
  36. claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
  37. claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
  38. claude_mpm/services/agents/loading/__init__.py +11 -0
  39. claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
  40. claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
  41. claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
  42. claude_mpm/services/agents/management/__init__.py +9 -0
  43. claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
  44. claude_mpm/services/agents/memory/__init__.py +21 -0
  45. claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
  46. claude_mpm/services/agents/registry/__init__.py +29 -0
  47. claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
  48. claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
  49. claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
  50. claude_mpm/services/async_session_logger.py +584 -0
  51. claude_mpm/services/claude_session_logger.py +299 -0
  52. claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
  53. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
  54. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
  56. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
  57. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
  58. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
  59. claude_mpm/services/framework_claude_md_generator.py +4 -2
  60. claude_mpm/services/memory/__init__.py +17 -0
  61. claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
  62. claude_mpm/services/memory/cache/__init__.py +14 -0
  63. claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
  64. claude_mpm/services/memory/cache/simple_cache.py +317 -0
  65. claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
  66. claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
  67. claude_mpm/services/optimized_hook_service.py +542 -0
  68. claude_mpm/services/project_registry.py +14 -8
  69. claude_mpm/services/response_tracker.py +237 -0
  70. claude_mpm/services/ticketing_service_original.py +4 -2
  71. claude_mpm/services/version_control/branch_strategy.py +3 -1
  72. claude_mpm/utils/paths.py +12 -10
  73. claude_mpm/utils/session_logging.py +114 -0
  74. claude_mpm/validation/agent_validator.py +2 -1
  75. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
  76. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
  77. claude_mpm/cli/commands/ui.py +0 -57
  78. claude_mpm/core/simple_runner.py +0 -1046
  79. claude_mpm/hooks/builtin/__init__.py +0 -1
  80. claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
  81. claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
  82. claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
  83. claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
  84. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
  85. claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
  86. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
  87. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
  88. claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
  89. claude_mpm/orchestration/__init__.py +0 -6
  90. claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
  91. claude_mpm/orchestration/archive/factory.py +0 -215
  92. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
  93. claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
  94. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
  95. claude_mpm/orchestration/archive/orchestrator.py +0 -501
  96. claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
  97. claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
  98. claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
  99. claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
  100. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
  101. claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
  102. claude_mpm/schemas/workflow_validator.py +0 -411
  103. claude_mpm/services/parent_directory_manager/__init__.py +0 -577
  104. claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
  105. claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
  106. claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
  107. claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
  108. claude_mpm/services/parent_directory_manager/operations.py +0 -186
  109. claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
  110. claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
  111. claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
  112. claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
  113. claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
  114. claude_mpm/ui/__init__.py +0 -1
  115. claude_mpm/ui/rich_terminal_ui.py +0 -295
  116. claude_mpm/ui/terminal_ui.py +0 -328
  117. /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
  118. /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
  119. /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
  120. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
  121. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
  122. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
  123. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/top_level.txt +0 -0
@@ -1,1046 +0,0 @@
1
- """Claude runner with both exec and subprocess launch methods."""
2
-
3
- import json
4
- import os
5
- import subprocess
6
- import sys
7
- import time
8
- from datetime import datetime
9
- from pathlib import Path
10
- from typing import Optional
11
- import uuid
12
-
13
- try:
14
- from claude_mpm.services.agent_deployment import AgentDeploymentService
15
- from claude_mpm.services.ticket_manager import TicketManager
16
- from claude_mpm.services.hook_service import HookService
17
- from claude_mpm.core.config import Config
18
- from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
19
- except ImportError:
20
- from claude_mpm.services.agent_deployment import AgentDeploymentService
21
- from claude_mpm.services.ticket_manager import TicketManager
22
- from claude_mpm.services.hook_service import HookService
23
- from claude_mpm.core.config import Config
24
- from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
25
-
26
-
27
- class ClaudeRunner:
28
- """
29
- Claude runner that replaces the entire orchestrator system.
30
-
31
- This does exactly what we need:
32
- 1. Deploy native agents to .claude/agents/
33
- 2. Run Claude CLI with either exec or subprocess
34
- 3. Extract tickets if needed
35
- 4. Handle both interactive and non-interactive modes
36
-
37
- Supports two launch methods:
38
- - exec: Replace current process (default for backward compatibility)
39
- - subprocess: Launch as child process for more control
40
- """
41
-
42
- def __init__(
43
- self,
44
- enable_tickets: bool = True,
45
- log_level: str = "OFF",
46
- claude_args: Optional[list] = None,
47
- launch_method: str = "exec", # "exec" or "subprocess"
48
- enable_websocket: bool = False,
49
- websocket_port: int = 8765
50
- ):
51
- """Initialize the Claude runner."""
52
- self.enable_tickets = enable_tickets
53
- self.log_level = log_level
54
- self.logger = get_logger("claude_runner")
55
- self.claude_args = claude_args or []
56
- self.launch_method = launch_method
57
- self.enable_websocket = enable_websocket
58
- self.websocket_port = websocket_port
59
-
60
- # Initialize project logger for session logging
61
- self.project_logger = None
62
- if log_level != "OFF":
63
- try:
64
- self.project_logger = get_project_logger(log_level)
65
- self.project_logger.log_system(
66
- f"Initializing ClaudeRunner with {launch_method} launcher",
67
- level="INFO",
68
- component="runner"
69
- )
70
- except Exception as e:
71
- self.logger.warning(f"Failed to initialize project logger: {e}")
72
-
73
- # Initialize services
74
- self.deployment_service = AgentDeploymentService()
75
- if enable_tickets:
76
- try:
77
- self.ticket_manager = TicketManager()
78
- except (ImportError, TypeError, Exception) as e:
79
- self.logger.warning(f"Ticket manager not available: {e}")
80
- self.ticket_manager = None
81
- self.enable_tickets = False
82
-
83
- # Initialize hook service and register memory hooks
84
- self.config = Config()
85
- self.hook_service = HookService(self.config)
86
- self._register_memory_hooks()
87
-
88
- # Load system instructions
89
- self.system_instructions = self._load_system_instructions()
90
-
91
- # Track if we need to create session logs
92
- self.session_log_file = None
93
- if self.project_logger and log_level != "OFF":
94
- try:
95
- # Create a system.jsonl file in the session directory
96
- self.session_log_file = self.project_logger.session_dir / "system.jsonl"
97
- self._log_session_event({
98
- "event": "session_start",
99
- "runner": "ClaudeRunner",
100
- "enable_tickets": enable_tickets,
101
- "log_level": log_level,
102
- "launch_method": launch_method
103
- })
104
- except Exception as e:
105
- self.logger.debug(f"Failed to create session log file: {e}")
106
-
107
- # Initialize Socket.IO server reference
108
- self.websocket_server = None
109
-
110
- def setup_agents(self) -> bool:
111
- """Deploy native agents to .claude/agents/."""
112
- try:
113
- if self.project_logger:
114
- self.project_logger.log_system(
115
- "Starting agent deployment",
116
- level="INFO",
117
- component="deployment"
118
- )
119
-
120
- results = self.deployment_service.deploy_agents()
121
-
122
- if results["deployed"] or results.get("updated", []):
123
- deployed_count = len(results['deployed'])
124
- updated_count = len(results.get('updated', []))
125
-
126
- if deployed_count > 0:
127
- print(f"✓ Deployed {deployed_count} native agents")
128
- if updated_count > 0:
129
- print(f"✓ Updated {updated_count} agents")
130
-
131
- if self.project_logger:
132
- self.project_logger.log_system(
133
- f"Agent deployment successful: {deployed_count} deployed, {updated_count} updated",
134
- level="INFO",
135
- component="deployment"
136
- )
137
-
138
- # Set Claude environment
139
- self.deployment_service.set_claude_environment()
140
- return True
141
- else:
142
- self.logger.info("All agents already up to date")
143
- if self.project_logger:
144
- self.project_logger.log_system(
145
- "All agents already up to date",
146
- level="INFO",
147
- component="deployment"
148
- )
149
- return True
150
-
151
- except Exception as e:
152
- self.logger.error(f"Agent deployment failed: {e}")
153
- print(f"⚠️ Agent deployment failed: {e}")
154
- if self.project_logger:
155
- self.project_logger.log_system(
156
- f"Agent deployment failed: {e}",
157
- level="ERROR",
158
- component="deployment"
159
- )
160
- return False
161
-
162
- def run_interactive(self, initial_context: Optional[str] = None):
163
- """Run Claude in interactive mode."""
164
- # Connect to Socket.IO server if enabled
165
- if self.enable_websocket:
166
- try:
167
- # Use Socket.IO client proxy to connect to monitoring server
168
- from claude_mpm.services.socketio_server import SocketIOClientProxy
169
- self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
170
- self.websocket_server.start()
171
- self.logger.info("Connected to Socket.IO monitoring server")
172
-
173
- # Generate session ID
174
- session_id = str(uuid.uuid4())
175
- working_dir = os.getcwd()
176
-
177
- # Notify session start
178
- self.websocket_server.session_started(
179
- session_id=session_id,
180
- launch_method=self.launch_method,
181
- working_dir=working_dir
182
- )
183
- except Exception as e:
184
- self.logger.warning(f"Failed to connect to Socket.IO server: {e}")
185
- self.websocket_server = None
186
-
187
- # Get version with robust fallback mechanisms
188
- version_str = self._get_version()
189
-
190
- # Print styled welcome box
191
- print("\033[32m╭───────────────────────────────────────────────────╮\033[0m")
192
- print("\033[32m│\033[0m ✻ Claude MPM - Interactive Session \033[32m│\033[0m")
193
- print(f"\033[32m│\033[0m Version {version_str:<40}\033[32m│\033[0m")
194
- print("\033[32m│ │\033[0m")
195
- print("\033[32m│\033[0m Type '/agents' to see available agents \033[32m│\033[0m")
196
- print("\033[32m╰───────────────────────────────────────────────────╯\033[0m")
197
- print("") # Add blank line after box
198
-
199
- if self.project_logger:
200
- self.project_logger.log_system(
201
- "Starting interactive session",
202
- level="INFO",
203
- component="session"
204
- )
205
-
206
- # Setup agents
207
- if not self.setup_agents():
208
- print("Continuing without native agents...")
209
-
210
- # Build command with system instructions
211
- cmd = [
212
- "claude",
213
- "--model", "opus",
214
- "--dangerously-skip-permissions"
215
- ]
216
-
217
- # Add any custom Claude arguments
218
- if self.claude_args:
219
- cmd.extend(self.claude_args)
220
-
221
- # Add system instructions if available
222
- system_prompt = self._create_system_prompt()
223
- if system_prompt and system_prompt != create_simple_context():
224
- cmd.extend(["--append-system-prompt", system_prompt])
225
-
226
- # Run interactive Claude directly
227
- try:
228
- # Use execvp to replace the current process with Claude
229
- # This should avoid any subprocess issues
230
-
231
- # Clean environment
232
- clean_env = os.environ.copy()
233
- claude_vars_to_remove = [
234
- 'CLAUDE_CODE_ENTRYPOINT', 'CLAUDECODE', 'CLAUDE_CONFIG_DIR',
235
- 'CLAUDE_MAX_PARALLEL_SUBAGENTS', 'CLAUDE_TIMEOUT'
236
- ]
237
- for var in claude_vars_to_remove:
238
- clean_env.pop(var, None)
239
-
240
- # Set the correct working directory for Claude Code
241
- # If CLAUDE_MPM_USER_PWD is set, use that as the working directory
242
- if 'CLAUDE_MPM_USER_PWD' in clean_env:
243
- user_pwd = clean_env['CLAUDE_MPM_USER_PWD']
244
- clean_env['CLAUDE_WORKSPACE'] = user_pwd
245
- # Also change to that directory before launching Claude
246
- try:
247
- os.chdir(user_pwd)
248
- self.logger.info(f"Changed working directory to: {user_pwd}")
249
- except Exception as e:
250
- self.logger.warning(f"Could not change to user directory {user_pwd}: {e}")
251
-
252
- print("Launching Claude...")
253
-
254
- if self.project_logger:
255
- self.project_logger.log_system(
256
- f"Launching Claude interactive mode with {self.launch_method}",
257
- level="INFO",
258
- component="session"
259
- )
260
- self._log_session_event({
261
- "event": "launching_claude_interactive",
262
- "command": " ".join(cmd),
263
- "method": self.launch_method
264
- })
265
-
266
- # Notify WebSocket clients
267
- if self.websocket_server:
268
- self.websocket_server.claude_status_changed(
269
- status="starting",
270
- message="Launching Claude interactive session"
271
- )
272
-
273
- # Launch using selected method
274
- if self.launch_method == "subprocess":
275
- self._launch_subprocess_interactive(cmd, clean_env)
276
- else:
277
- # Default to exec for backward compatibility
278
- if self.websocket_server:
279
- # Notify before exec (we won't be able to after)
280
- self.websocket_server.claude_status_changed(
281
- status="running",
282
- message="Claude process started (exec mode)"
283
- )
284
- os.execvpe(cmd[0], cmd, clean_env)
285
-
286
- except Exception as e:
287
- print(f"Failed to launch Claude: {e}")
288
- if self.project_logger:
289
- self.project_logger.log_system(
290
- f"Failed to launch Claude: {e}",
291
- level="ERROR",
292
- component="session"
293
- )
294
- self._log_session_event({
295
- "event": "interactive_launch_failed",
296
- "error": str(e),
297
- "exception_type": type(e).__name__
298
- })
299
-
300
- # Notify WebSocket clients of error
301
- if self.websocket_server:
302
- self.websocket_server.claude_status_changed(
303
- status="error",
304
- message=f"Failed to launch Claude: {e}"
305
- )
306
- # Fallback to subprocess
307
- try:
308
- # Use the same clean_env we prepared earlier
309
- subprocess.run(cmd, stdin=None, stdout=None, stderr=None, env=clean_env)
310
- if self.project_logger:
311
- self.project_logger.log_system(
312
- "Interactive session completed (subprocess fallback)",
313
- level="INFO",
314
- component="session"
315
- )
316
- self._log_session_event({
317
- "event": "interactive_session_complete",
318
- "fallback": True
319
- })
320
- except Exception as fallback_error:
321
- print(f"Fallback also failed: {fallback_error}")
322
- if self.project_logger:
323
- self.project_logger.log_system(
324
- f"Fallback launch failed: {fallback_error}",
325
- level="ERROR",
326
- component="session"
327
- )
328
- self._log_session_event({
329
- "event": "interactive_fallback_failed",
330
- "error": str(fallback_error),
331
- "exception_type": type(fallback_error).__name__
332
- })
333
-
334
- def run_oneshot(self, prompt: str, context: Optional[str] = None) -> bool:
335
- """Run Claude with a single prompt and return success status."""
336
- start_time = time.time()
337
-
338
- # Connect to Socket.IO server if enabled
339
- if self.enable_websocket:
340
- try:
341
- # Use Socket.IO client proxy to connect to monitoring server
342
- from claude_mpm.services.socketio_server import SocketIOClientProxy
343
- self.websocket_server = SocketIOClientProxy(port=self.websocket_port)
344
- self.websocket_server.start()
345
- self.logger.info("Connected to Socket.IO monitoring server")
346
-
347
- # Generate session ID
348
- session_id = str(uuid.uuid4())
349
- working_dir = os.getcwd()
350
-
351
- # Notify session start
352
- self.websocket_server.session_started(
353
- session_id=session_id,
354
- launch_method="oneshot",
355
- working_dir=working_dir
356
- )
357
- except Exception as e:
358
- self.logger.warning(f"Failed to connect to Socket.IO server: {e}")
359
- self.websocket_server = None
360
-
361
- # Check for /mpm: commands
362
- if prompt.strip().startswith("/mpm:"):
363
- return self._handle_mpm_command(prompt.strip())
364
-
365
- if self.project_logger:
366
- self.project_logger.log_system(
367
- f"Starting non-interactive session with prompt: {prompt[:100]}",
368
- level="INFO",
369
- component="session"
370
- )
371
-
372
- # Setup agents
373
- if not self.setup_agents():
374
- print("Continuing without native agents...")
375
-
376
- # Combine context and prompt
377
- full_prompt = prompt
378
- if context:
379
- full_prompt = f"{context}\n\n{prompt}"
380
-
381
- # Build command with system instructions
382
- cmd = [
383
- "claude",
384
- "--model", "opus",
385
- "--dangerously-skip-permissions"
386
- ]
387
-
388
- # Add any custom Claude arguments
389
- if self.claude_args:
390
- cmd.extend(self.claude_args)
391
-
392
- # Add print and prompt
393
- cmd.extend(["--print", full_prompt])
394
-
395
- # Add system instructions if available
396
- system_prompt = self._create_system_prompt()
397
- if system_prompt and system_prompt != create_simple_context():
398
- # Insert system prompt before the user prompt
399
- cmd.insert(-2, "--append-system-prompt")
400
- cmd.insert(-2, system_prompt)
401
-
402
- try:
403
- # Set up environment with correct working directory
404
- env = os.environ.copy()
405
-
406
- # Set the correct working directory for Claude Code
407
- if 'CLAUDE_MPM_USER_PWD' in env:
408
- user_pwd = env['CLAUDE_MPM_USER_PWD']
409
- env['CLAUDE_WORKSPACE'] = user_pwd
410
- # Change to that directory before running Claude
411
- try:
412
- original_cwd = os.getcwd()
413
- os.chdir(user_pwd)
414
- self.logger.info(f"Changed working directory to: {user_pwd}")
415
- except Exception as e:
416
- self.logger.warning(f"Could not change to user directory {user_pwd}: {e}")
417
- original_cwd = None
418
- else:
419
- original_cwd = None
420
-
421
- # Run Claude
422
- if self.project_logger:
423
- self.project_logger.log_system(
424
- "Executing Claude subprocess",
425
- level="INFO",
426
- component="session"
427
- )
428
-
429
- # Notify WebSocket clients
430
- if self.websocket_server:
431
- self.websocket_server.claude_status_changed(
432
- status="running",
433
- message="Executing Claude oneshot command"
434
- )
435
-
436
- result = subprocess.run(cmd, capture_output=True, text=True, env=env)
437
-
438
- # Restore original directory if we changed it
439
- if original_cwd:
440
- try:
441
- os.chdir(original_cwd)
442
- except Exception:
443
- pass
444
- execution_time = time.time() - start_time
445
-
446
- if result.returncode == 0:
447
- response = result.stdout.strip()
448
- print(response)
449
-
450
- # Broadcast output to WebSocket clients
451
- if self.websocket_server and response:
452
- self.websocket_server.claude_output(response, "stdout")
453
-
454
- if self.project_logger:
455
- # Log successful completion
456
- self.project_logger.log_system(
457
- f"Non-interactive session completed successfully in {execution_time:.2f}s",
458
- level="INFO",
459
- component="session"
460
- )
461
-
462
- # Log session event
463
- self._log_session_event({
464
- "event": "session_complete",
465
- "success": True,
466
- "execution_time": execution_time,
467
- "response_length": len(response)
468
- })
469
-
470
- # Log agent invocation if we detect delegation patterns
471
- if self._contains_delegation(response):
472
- self.project_logger.log_system(
473
- "Detected potential agent delegation in response",
474
- level="INFO",
475
- component="delegation"
476
- )
477
- self._log_session_event({
478
- "event": "delegation_detected",
479
- "prompt": prompt[:200],
480
- "indicators": [p for p in ["Task(", "subagent_type=", "engineer agent", "qa agent"]
481
- if p.lower() in response.lower()]
482
- })
483
-
484
- # Notify WebSocket clients about delegation
485
- if self.websocket_server:
486
- # Try to extract agent name
487
- agent_name = self._extract_agent_from_response(response)
488
- if agent_name:
489
- self.websocket_server.agent_delegated(
490
- agent=agent_name,
491
- task=prompt[:100],
492
- status="detected"
493
- )
494
-
495
- # Extract tickets if enabled
496
- if self.enable_tickets and self.ticket_manager and response:
497
- self._extract_tickets(response)
498
-
499
- return True
500
- else:
501
- error_msg = result.stderr or "Unknown error"
502
- print(f"Error: {error_msg}")
503
-
504
- # Broadcast error to WebSocket clients
505
- if self.websocket_server:
506
- self.websocket_server.claude_output(error_msg, "stderr")
507
- self.websocket_server.claude_status_changed(
508
- status="error",
509
- message=f"Command failed with code {result.returncode}"
510
- )
511
-
512
- if self.project_logger:
513
- self.project_logger.log_system(
514
- f"Non-interactive session failed: {error_msg}",
515
- level="ERROR",
516
- component="session"
517
- )
518
- self._log_session_event({
519
- "event": "session_failed",
520
- "success": False,
521
- "error": error_msg,
522
- "return_code": result.returncode
523
- })
524
-
525
- return False
526
-
527
- except Exception as e:
528
- print(f"Error: {e}")
529
-
530
- if self.project_logger:
531
- self.project_logger.log_system(
532
- f"Exception during non-interactive session: {e}",
533
- level="ERROR",
534
- component="session"
535
- )
536
- self._log_session_event({
537
- "event": "session_exception",
538
- "success": False,
539
- "exception": str(e),
540
- "exception_type": type(e).__name__
541
- })
542
-
543
- return False
544
- finally:
545
- # Ensure logs are flushed
546
- if self.project_logger:
547
- try:
548
- # Log session summary
549
- summary = self.project_logger.get_session_summary()
550
- self.project_logger.log_system(
551
- f"Session {summary['session_id']} completed",
552
- level="INFO",
553
- component="session"
554
- )
555
- except Exception as e:
556
- self.logger.debug(f"Failed to log session summary: {e}")
557
-
558
- # End WebSocket session
559
- if self.websocket_server:
560
- self.websocket_server.claude_status_changed(
561
- status="stopped",
562
- message="Session completed"
563
- )
564
- self.websocket_server.session_ended()
565
-
566
- def _extract_tickets(self, text: str):
567
- """Extract tickets from Claude's response."""
568
- if not self.ticket_manager:
569
- return
570
-
571
- try:
572
- # Use the ticket manager's extraction logic if available
573
- if hasattr(self.ticket_manager, 'extract_tickets_from_text'):
574
- tickets = self.ticket_manager.extract_tickets_from_text(text)
575
- if tickets:
576
- print(f"\n📋 Extracted {len(tickets)} tickets")
577
- for ticket in tickets[:3]: # Show first 3
578
- print(f" - [{ticket.get('id', 'N/A')}] {ticket.get('title', 'No title')}")
579
- if len(tickets) > 3:
580
- print(f" ... and {len(tickets) - 3} more")
581
- else:
582
- self.logger.debug("Ticket extraction method not available")
583
- except Exception as e:
584
- self.logger.debug(f"Ticket extraction failed: {e}")
585
-
586
- def _load_system_instructions(self) -> Optional[str]:
587
- """Load and process system instructions from agents/INSTRUCTIONS.md.
588
-
589
- WHY: Process template variables like {{capabilities-list}} to include
590
- dynamic agent capabilities in the PM's system instructions.
591
- """
592
- try:
593
- # Find the INSTRUCTIONS.md file
594
- module_path = Path(__file__).parent.parent
595
- instructions_path = module_path / "agents" / "INSTRUCTIONS.md"
596
-
597
- if not instructions_path.exists():
598
- self.logger.warning(f"System instructions not found: {instructions_path}")
599
- return None
600
-
601
- # Read raw instructions
602
- raw_instructions = instructions_path.read_text()
603
-
604
- # Process template variables if ContentAssembler is available
605
- try:
606
- from claude_mpm.services.framework_claude_md_generator.content_assembler import ContentAssembler
607
- assembler = ContentAssembler()
608
- processed_instructions = assembler.apply_template_variables(raw_instructions)
609
- self.logger.info("Loaded and processed PM framework system instructions with dynamic capabilities")
610
- return processed_instructions
611
- except ImportError:
612
- self.logger.warning("ContentAssembler not available, using raw instructions")
613
- return raw_instructions
614
- except Exception as e:
615
- self.logger.warning(f"Failed to process template variables: {e}, using raw instructions")
616
- return raw_instructions
617
-
618
- except Exception as e:
619
- self.logger.error(f"Failed to load system instructions: {e}")
620
- return None
621
-
622
- def _create_system_prompt(self) -> str:
623
- """Create the complete system prompt including instructions."""
624
- if self.system_instructions:
625
- return self.system_instructions
626
- else:
627
- # Fallback to basic context
628
- return create_simple_context()
629
-
630
- def _contains_delegation(self, text: str) -> bool:
631
- """Check if text contains signs of agent delegation."""
632
- # Look for common delegation patterns
633
- delegation_patterns = [
634
- "Task(",
635
- "subagent_type=",
636
- "delegating to",
637
- "asking the",
638
- "engineer agent",
639
- "qa agent",
640
- "documentation agent",
641
- "research agent",
642
- "security agent",
643
- "ops agent",
644
- "version_control agent",
645
- "data_engineer agent"
646
- ]
647
-
648
- text_lower = text.lower()
649
- return any(pattern.lower() in text_lower for pattern in delegation_patterns)
650
-
651
- def _extract_agent_from_response(self, text: str) -> Optional[str]:
652
- """Try to extract agent name from delegation response."""
653
- # Look for common patterns
654
- import re
655
-
656
- # Pattern 1: subagent_type="agent_name"
657
- match = re.search(r'subagent_type=["\']([^"\']*)["\'\)]', text)
658
- if match:
659
- return match.group(1)
660
-
661
- # Pattern 2: "engineer agent" etc
662
- agent_names = [
663
- "engineer", "qa", "documentation", "research",
664
- "security", "ops", "version_control", "data_engineer"
665
- ]
666
- text_lower = text.lower()
667
- for agent in agent_names:
668
- if f"{agent} agent" in text_lower or f"agent: {agent}" in text_lower:
669
- return agent
670
-
671
- return None
672
-
673
- def _handle_mpm_command(self, prompt: str) -> bool:
674
- """Handle /mpm: commands directly without going to Claude."""
675
- try:
676
- # Extract command and arguments
677
- command_line = prompt[5:].strip() # Remove "/mpm:"
678
- parts = command_line.split()
679
-
680
- if not parts:
681
- print("No command specified. Available commands: test")
682
- return True
683
-
684
- command = parts[0]
685
- args = parts[1:]
686
-
687
- # Handle commands
688
- if command == "test":
689
- print("Hello World")
690
- if self.project_logger:
691
- self.project_logger.log_system(
692
- "Executed /mpm:test command",
693
- level="INFO",
694
- component="command"
695
- )
696
- return True
697
- elif command == "agents":
698
- # Handle agents command - display deployed agent versions
699
- # WHY: This provides users with a quick way to check deployed agent versions
700
- # directly from within Claude Code, maintaining consistency with CLI behavior
701
- try:
702
- from claude_mpm.cli import _get_agent_versions_display
703
- agent_versions = _get_agent_versions_display()
704
- if agent_versions:
705
- print(agent_versions)
706
- else:
707
- print("No deployed agents found")
708
- print("\nTo deploy agents, run: claude-mpm --mpm:agents deploy")
709
-
710
- if self.project_logger:
711
- self.project_logger.log_system(
712
- "Executed /mpm:agents command",
713
- level="INFO",
714
- component="command"
715
- )
716
- return True
717
- except Exception as e:
718
- print(f"Error getting agent versions: {e}")
719
- return False
720
- else:
721
- print(f"Unknown command: {command}")
722
- print("Available commands: test, agents")
723
- return True
724
-
725
- except Exception as e:
726
- print(f"Error executing command: {e}")
727
- if self.project_logger:
728
- self.project_logger.log_system(
729
- f"Failed to execute /mpm: command: {e}",
730
- level="ERROR",
731
- component="command"
732
- )
733
- return False
734
-
735
- def _log_session_event(self, event_data: dict):
736
- """Log an event to the session log file."""
737
- if self.session_log_file:
738
- try:
739
- log_entry = {
740
- "timestamp": datetime.now().isoformat(),
741
- **event_data
742
- }
743
-
744
- with open(self.session_log_file, 'a') as f:
745
- f.write(json.dumps(log_entry) + '\n')
746
- except Exception as e:
747
- self.logger.debug(f"Failed to log session event: {e}")
748
-
749
- def _get_version(self) -> str:
750
- """
751
- Robust version determination with multiple fallback mechanisms.
752
-
753
- WHY: The version display is critical for debugging and user experience.
754
- This implementation ensures we always show the correct version rather than
755
- defaulting to v0.0.0, even in edge cases where imports might fail.
756
-
757
- DESIGN DECISION: We try multiple methods in order of preference:
758
- 1. Package import (__version__) - fastest for normal installations
759
- 2. importlib.metadata - standard for installed packages
760
- 3. VERSION file reading - fallback for development environments
761
- 4. Only then default to v0.0.0 with detailed error logging
762
-
763
- Returns version string formatted as "vX.Y.Z"
764
- """
765
- version = "0.0.0"
766
- method_used = "default"
767
-
768
- # Method 1: Try package import (fastest, most common)
769
- try:
770
- from claude_mpm import __version__
771
- version = __version__
772
- method_used = "package_import"
773
- self.logger.debug(f"Version obtained via package import: {version}")
774
- except ImportError as e:
775
- self.logger.debug(f"Package import failed: {e}")
776
- except Exception as e:
777
- self.logger.warning(f"Unexpected error in package import: {e}")
778
-
779
- # Method 2: Try importlib.metadata (standard for installed packages)
780
- if version == "0.0.0":
781
- try:
782
- import importlib.metadata
783
- version = importlib.metadata.version('claude-mpm')
784
- method_used = "importlib_metadata"
785
- self.logger.debug(f"Version obtained via importlib.metadata: {version}")
786
- except importlib.metadata.PackageNotFoundError:
787
- self.logger.debug("Package not found in importlib.metadata (likely development install)")
788
- except ImportError:
789
- self.logger.debug("importlib.metadata not available (Python < 3.8)")
790
- except Exception as e:
791
- self.logger.warning(f"Unexpected error in importlib.metadata: {e}")
792
-
793
- # Method 3: Try reading VERSION file directly (development fallback)
794
- if version == "0.0.0":
795
- try:
796
- # Calculate path relative to this file
797
- version_file = Path(__file__).parent.parent.parent.parent / "VERSION"
798
- if version_file.exists():
799
- version = version_file.read_text().strip()
800
- method_used = "version_file"
801
- self.logger.debug(f"Version obtained via VERSION file: {version}")
802
- else:
803
- self.logger.debug(f"VERSION file not found at: {version_file}")
804
- except Exception as e:
805
- self.logger.warning(f"Failed to read VERSION file: {e}")
806
-
807
- # Log final result
808
- if version == "0.0.0":
809
- self.logger.error(
810
- "All version detection methods failed. This indicates a packaging or installation issue."
811
- )
812
- else:
813
- self.logger.debug(f"Final version: {version} (method: {method_used})")
814
-
815
- return f"v{version}"
816
-
817
- def _register_memory_hooks(self):
818
- """Register memory integration hooks with the hook service.
819
-
820
- WHY: This activates the memory system by registering hooks that automatically
821
- inject agent memory before delegation and extract learnings after delegation.
822
- This is the critical connection point between the memory system and the CLI.
823
-
824
- DESIGN DECISION: We register hooks here instead of in __init__ to ensure
825
- all services are initialized first. Hooks are only registered if the memory
826
- system is enabled in configuration.
827
- """
828
- try:
829
- # Only register if memory system is enabled
830
- if not self.config.get('memory.enabled', True):
831
- self.logger.debug("Memory system disabled - skipping hook registration")
832
- return
833
-
834
- # Import hook classes (lazy import to avoid circular dependencies)
835
- from claude_mpm.hooks.memory_integration_hook import (
836
- MemoryPreDelegationHook,
837
- MemoryPostDelegationHook
838
- )
839
-
840
- # Register pre-delegation hook for memory injection
841
- pre_hook = MemoryPreDelegationHook(self.config)
842
- success = self.hook_service.register_hook(pre_hook)
843
- if success:
844
- self.logger.info(f"✅ Registered memory pre-delegation hook (priority: {pre_hook.priority})")
845
- else:
846
- self.logger.warning("❌ Failed to register memory pre-delegation hook")
847
-
848
- # Register post-delegation hook if auto-learning is enabled
849
- if self.config.get('memory.auto_learning', True): # Default to True now
850
- post_hook = MemoryPostDelegationHook(self.config)
851
- success = self.hook_service.register_hook(post_hook)
852
- if success:
853
- self.logger.info(f"✅ Registered memory post-delegation hook (priority: {post_hook.priority})")
854
- else:
855
- self.logger.warning("❌ Failed to register memory post-delegation hook")
856
- else:
857
- self.logger.info("ℹ️ Auto-learning disabled - skipping post-delegation hook")
858
-
859
- # Log summary of registered hooks
860
- hooks = self.hook_service.list_hooks()
861
- pre_count = len(hooks.get('pre_delegation', []))
862
- post_count = len(hooks.get('post_delegation', []))
863
- self.logger.info(f"📋 Hook Service initialized: {pre_count} pre-delegation, {post_count} post-delegation hooks")
864
-
865
- except Exception as e:
866
- self.logger.error(f"❌ Failed to register memory hooks: {e}")
867
- # Don't fail the entire initialization - memory system is optional
868
-
869
- def _launch_subprocess_interactive(self, cmd: list, env: dict):
870
- """Launch Claude as a subprocess with PTY for interactive mode."""
871
- import pty
872
- import select
873
- import termios
874
- import tty
875
- import signal
876
-
877
- # Save original terminal settings
878
- original_tty = None
879
- if sys.stdin.isatty():
880
- original_tty = termios.tcgetattr(sys.stdin)
881
-
882
- # Create PTY
883
- master_fd, slave_fd = pty.openpty()
884
-
885
- try:
886
- # Start Claude process
887
- process = subprocess.Popen(
888
- cmd,
889
- stdin=slave_fd,
890
- stdout=slave_fd,
891
- stderr=slave_fd,
892
- env=env
893
- )
894
-
895
- # Close slave in parent
896
- os.close(slave_fd)
897
-
898
- if self.project_logger:
899
- self.project_logger.log_system(
900
- f"Claude subprocess started with PID {process.pid}",
901
- level="INFO",
902
- component="subprocess"
903
- )
904
-
905
- # Notify WebSocket clients
906
- if self.websocket_server:
907
- self.websocket_server.claude_status_changed(
908
- status="running",
909
- pid=process.pid,
910
- message="Claude subprocess started"
911
- )
912
-
913
- # Set terminal to raw mode for proper interaction
914
- if sys.stdin.isatty():
915
- tty.setraw(sys.stdin)
916
-
917
- # Handle Ctrl+C gracefully
918
- def signal_handler(signum, frame):
919
- if process.poll() is None:
920
- process.terminate()
921
- raise KeyboardInterrupt()
922
-
923
- signal.signal(signal.SIGINT, signal_handler)
924
-
925
- # I/O loop
926
- while True:
927
- # Check if process is still running
928
- if process.poll() is not None:
929
- break
930
-
931
- # Check for data from Claude or stdin
932
- r, _, _ = select.select([master_fd, sys.stdin], [], [], 0)
933
-
934
- if master_fd in r:
935
- try:
936
- data = os.read(master_fd, 4096)
937
- if data:
938
- os.write(sys.stdout.fileno(), data)
939
- # Broadcast output to WebSocket clients
940
- if self.websocket_server:
941
- try:
942
- # Decode and send
943
- output = data.decode('utf-8', errors='replace')
944
- self.websocket_server.claude_output(output, "stdout")
945
- except Exception as e:
946
- self.logger.debug(f"Failed to broadcast output: {e}")
947
- else:
948
- break # EOF
949
- except OSError:
950
- break
951
-
952
- if sys.stdin in r:
953
- try:
954
- data = os.read(sys.stdin.fileno(), 4096)
955
- if data:
956
- os.write(master_fd, data)
957
- except OSError:
958
- break
959
-
960
- # Wait for process to complete
961
- process.wait()
962
-
963
- if self.project_logger:
964
- self.project_logger.log_system(
965
- f"Claude subprocess exited with code {process.returncode}",
966
- level="INFO",
967
- component="subprocess"
968
- )
969
-
970
- # Notify WebSocket clients
971
- if self.websocket_server:
972
- self.websocket_server.claude_status_changed(
973
- status="stopped",
974
- message=f"Claude subprocess exited with code {process.returncode}"
975
- )
976
-
977
- finally:
978
- # Restore terminal
979
- if original_tty and sys.stdin.isatty():
980
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_tty)
981
-
982
- # Close PTY
983
- try:
984
- os.close(master_fd)
985
- except:
986
- pass
987
-
988
- # Ensure process is terminated
989
- if 'process' in locals() and process.poll() is None:
990
- process.terminate()
991
- try:
992
- process.wait(timeout=2)
993
- except subprocess.TimeoutExpired:
994
- process.kill()
995
- process.wait()
996
-
997
- # End WebSocket session if in subprocess mode
998
- if self.websocket_server:
999
- self.websocket_server.session_ended()
1000
-
1001
-
1002
- def create_simple_context() -> str:
1003
- """Create basic context for Claude."""
1004
- return """You are Claude Code running in Claude MPM (Multi-Agent Project Manager).
1005
-
1006
- You have access to native subagents via the Task tool with subagent_type parameter:
1007
- - engineer: For coding, implementation, and technical tasks
1008
- - qa: For testing, validation, and quality assurance
1009
- - documentation: For docs, guides, and explanations
1010
- - research: For investigation and analysis
1011
- - security: For security-related tasks
1012
- - ops: For deployment and infrastructure
1013
- - version-control: For git and version management
1014
- - data-engineer: For data processing and APIs
1015
-
1016
- Use these agents by calling: Task(description="task description", subagent_type="agent_name")
1017
-
1018
- IMPORTANT: The Task tool accepts both naming formats:
1019
- - Capitalized format: "Research", "Engineer", "QA", "Version Control", "Data Engineer"
1020
- - Lowercase format: "research", "engineer", "qa", "version-control", "data-engineer"
1021
-
1022
- Both formats work correctly. When you see capitalized names (matching TodoWrite prefixes),
1023
- automatically normalize them to lowercase-hyphenated format for the Task tool.
1024
-
1025
- Work efficiently and delegate appropriately to subagents when needed."""
1026
-
1027
-
1028
- # Backward compatibility alias
1029
- SimpleClaudeRunner = ClaudeRunner
1030
-
1031
-
1032
- # Convenience functions for backward compatibility
1033
- def run_claude_interactive(context: Optional[str] = None):
1034
- """Run Claude interactively with optional context."""
1035
- runner = ClaudeRunner()
1036
- if context is None:
1037
- context = create_simple_context()
1038
- runner.run_interactive(context)
1039
-
1040
-
1041
- def run_claude_oneshot(prompt: str, context: Optional[str] = None) -> bool:
1042
- """Run Claude with a single prompt."""
1043
- runner = ClaudeRunner()
1044
- if context is None:
1045
- context = create_simple_context()
1046
- return runner.run_oneshot(prompt, context)