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