claude-mpm 3.7.8__py3-none-any.whl → 3.8.1__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 (93) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +0 -106
  3. claude_mpm/agents/INSTRUCTIONS.md +0 -96
  4. claude_mpm/agents/MEMORY.md +88 -0
  5. claude_mpm/agents/WORKFLOW.md +86 -0
  6. claude_mpm/agents/templates/code_analyzer.json +2 -2
  7. claude_mpm/agents/templates/data_engineer.json +1 -1
  8. claude_mpm/agents/templates/documentation.json +1 -1
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/ops.json +1 -1
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/agents/templates/security.json +1 -1
  14. claude_mpm/agents/templates/ticketing.json +2 -7
  15. claude_mpm/agents/templates/version_control.json +1 -1
  16. claude_mpm/agents/templates/web_qa.json +2 -2
  17. claude_mpm/agents/templates/web_ui.json +2 -2
  18. claude_mpm/cli/__init__.py +2 -2
  19. claude_mpm/cli/commands/__init__.py +2 -1
  20. claude_mpm/cli/commands/tickets.py +596 -19
  21. claude_mpm/cli/parser.py +217 -5
  22. claude_mpm/config/__init__.py +30 -39
  23. claude_mpm/config/socketio_config.py +8 -5
  24. claude_mpm/constants.py +13 -0
  25. claude_mpm/core/__init__.py +8 -18
  26. claude_mpm/core/cache.py +596 -0
  27. claude_mpm/core/claude_runner.py +166 -622
  28. claude_mpm/core/config.py +5 -1
  29. claude_mpm/core/constants.py +339 -0
  30. claude_mpm/core/container.py +461 -22
  31. claude_mpm/core/exceptions.py +392 -0
  32. claude_mpm/core/framework_loader.py +208 -94
  33. claude_mpm/core/interactive_session.py +432 -0
  34. claude_mpm/core/interfaces.py +424 -0
  35. claude_mpm/core/lazy.py +467 -0
  36. claude_mpm/core/logging_config.py +444 -0
  37. claude_mpm/core/oneshot_session.py +465 -0
  38. claude_mpm/core/optimized_agent_loader.py +485 -0
  39. claude_mpm/core/optimized_startup.py +490 -0
  40. claude_mpm/core/service_registry.py +52 -26
  41. claude_mpm/core/socketio_pool.py +162 -5
  42. claude_mpm/core/types.py +292 -0
  43. claude_mpm/core/typing_utils.py +477 -0
  44. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
  45. claude_mpm/init.py +2 -1
  46. claude_mpm/services/__init__.py +78 -14
  47. claude_mpm/services/agent/__init__.py +24 -0
  48. claude_mpm/services/agent/deployment.py +2548 -0
  49. claude_mpm/services/agent/management.py +598 -0
  50. claude_mpm/services/agent/registry.py +813 -0
  51. claude_mpm/services/agents/deployment/agent_deployment.py +587 -268
  52. claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
  53. claude_mpm/services/async_session_logger.py +8 -3
  54. claude_mpm/services/communication/__init__.py +21 -0
  55. claude_mpm/services/communication/socketio.py +1933 -0
  56. claude_mpm/services/communication/websocket.py +479 -0
  57. claude_mpm/services/core/__init__.py +123 -0
  58. claude_mpm/services/core/base.py +247 -0
  59. claude_mpm/services/core/interfaces.py +951 -0
  60. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
  61. claude_mpm/services/framework_claude_md_generator.py +3 -2
  62. claude_mpm/services/health_monitor.py +4 -3
  63. claude_mpm/services/hook_service.py +64 -4
  64. claude_mpm/services/infrastructure/__init__.py +21 -0
  65. claude_mpm/services/infrastructure/logging.py +202 -0
  66. claude_mpm/services/infrastructure/monitoring.py +893 -0
  67. claude_mpm/services/memory/indexed_memory.py +648 -0
  68. claude_mpm/services/project/__init__.py +21 -0
  69. claude_mpm/services/project/analyzer.py +864 -0
  70. claude_mpm/services/project/registry.py +608 -0
  71. claude_mpm/services/project_analyzer.py +95 -2
  72. claude_mpm/services/recovery_manager.py +15 -9
  73. claude_mpm/services/socketio/__init__.py +25 -0
  74. claude_mpm/services/socketio/handlers/__init__.py +25 -0
  75. claude_mpm/services/socketio/handlers/base.py +121 -0
  76. claude_mpm/services/socketio/handlers/connection.py +198 -0
  77. claude_mpm/services/socketio/handlers/file.py +213 -0
  78. claude_mpm/services/socketio/handlers/git.py +723 -0
  79. claude_mpm/services/socketio/handlers/memory.py +27 -0
  80. claude_mpm/services/socketio/handlers/project.py +25 -0
  81. claude_mpm/services/socketio/handlers/registry.py +145 -0
  82. claude_mpm/services/socketio_client_manager.py +12 -7
  83. claude_mpm/services/socketio_server.py +156 -30
  84. claude_mpm/services/ticket_manager.py +170 -7
  85. claude_mpm/utils/error_handler.py +1 -1
  86. claude_mpm/validation/agent_validator.py +27 -14
  87. claude_mpm/validation/frontmatter_validator.py +231 -0
  88. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +58 -21
  89. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +93 -53
  90. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
  91. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
  92. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
  93. {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,465 @@
1
+ """Oneshot session handler for Claude MPM.
2
+
3
+ This module encapsulates the logic for running one-time Claude commands,
4
+ breaking down the monolithic run_oneshot method into focused, testable components.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import time
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import Optional, Dict, Any, Tuple, List, TYPE_CHECKING
14
+ from logging import Logger
15
+
16
+ from claude_mpm.core.logger import get_logger
17
+ from claude_mpm.core.typing_utils import (
18
+ SessionId, SessionStatus, ErrorResult, SuccessResult,
19
+ SessionConfig, SessionResult, SessionEvent
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from claude_mpm.core.claude_runner import ClaudeRunner
24
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
25
+ from claude_mpm.services.response_logger import ResponseLogger
26
+ from claude_mpm.agents.memory.ticket_manager import TicketManager
27
+ from claude_mpm.core.logger import ProjectLogger
28
+
29
+
30
+ class OneshotSession:
31
+ """Manages a single oneshot Claude execution session.
32
+
33
+ WHY: This class extracts the complex oneshot logic from ClaudeRunner,
34
+ reducing cyclomatic complexity and improving maintainability.
35
+
36
+ DESIGN DECISION: Each method focuses on a single responsibility with
37
+ complexity < 10 and lines < 80, making the code easier to test and modify.
38
+ """
39
+
40
+ def __init__(self, runner):
41
+ """Initialize the oneshot session with a reference to the runner.
42
+
43
+ Args:
44
+ runner: The ClaudeRunner instance that owns this session
45
+ """
46
+ self.runner = runner
47
+ self.logger = get_logger("oneshot_session")
48
+ self.start_time = None
49
+ self.session_id = None
50
+ self.original_cwd = None
51
+
52
+ def initialize_session(self, prompt: str) -> Tuple[bool, Optional[str]]:
53
+ """Initialize the oneshot session.
54
+
55
+ Returns:
56
+ Tuple of (success, error_message)
57
+ """
58
+ self.start_time = time.time()
59
+ self.session_id = str(uuid.uuid4())
60
+
61
+ # Check for special MPM commands
62
+ if prompt.strip().startswith("/mpm:"):
63
+ result = self.runner._handle_mpm_command(prompt.strip())
64
+ return (result, None)
65
+
66
+ # Initialize WebSocket if enabled
67
+ if self.runner.enable_websocket:
68
+ self._setup_websocket()
69
+
70
+ # Log session start
71
+ if self.runner.project_logger:
72
+ self.runner.project_logger.log_system(
73
+ f"Starting non-interactive session with prompt: {prompt[:100]}",
74
+ level="INFO",
75
+ component="session"
76
+ )
77
+
78
+ return (True, None)
79
+
80
+ def deploy_agents(self) -> bool:
81
+ """Deploy system and project agents.
82
+
83
+ Returns:
84
+ True if successful, False otherwise
85
+ """
86
+ # Deploy system agents
87
+ if not self.runner.setup_agents():
88
+ print("Continuing without native agents...")
89
+
90
+ # Deploy project-specific agents
91
+ self.runner.deploy_project_agents_to_claude()
92
+
93
+ return True
94
+
95
+ def setup_infrastructure(self) -> Dict[str, Any]:
96
+ """Set up the execution environment and build the command.
97
+
98
+ Returns:
99
+ Dictionary containing command, environment, and other setup details
100
+ """
101
+ infrastructure = {
102
+ 'env': self._prepare_environment(),
103
+ 'cmd': self._build_command(),
104
+ 'working_dir_changed': False
105
+ }
106
+
107
+ # Change to user working directory if specified
108
+ if 'CLAUDE_MPM_USER_PWD' in infrastructure['env']:
109
+ user_pwd = infrastructure['env']['CLAUDE_MPM_USER_PWD']
110
+ infrastructure['env']['CLAUDE_WORKSPACE'] = user_pwd
111
+
112
+ try:
113
+ self.original_cwd = os.getcwd()
114
+ os.chdir(user_pwd)
115
+ infrastructure['working_dir_changed'] = True
116
+ self.logger.info(f"Changed working directory to: {user_pwd}")
117
+ except (PermissionError, FileNotFoundError, OSError) as e:
118
+ self.logger.warning(f"Could not change to directory {user_pwd}: {e}")
119
+ self.original_cwd = None
120
+
121
+ return infrastructure
122
+
123
+ def execute_command(self, prompt: str, context: Optional[str],
124
+ infrastructure: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
125
+ """Execute the Claude command with the given prompt.
126
+
127
+ Args:
128
+ prompt: The user's prompt
129
+ context: Optional context to prepend
130
+ infrastructure: Setup details from setup_infrastructure
131
+
132
+ Returns:
133
+ Tuple of (success, response_or_error)
134
+ """
135
+ # Build final command
136
+ cmd = self._build_final_command(prompt, context, infrastructure)
137
+
138
+ # Log and notify
139
+ self._notify_execution_start()
140
+
141
+ # Execute with proper error handling
142
+ return self._run_subprocess(cmd, infrastructure['env'], prompt)
143
+
144
+ def _build_final_command(self, prompt: str, context: Optional[str],
145
+ infrastructure: Dict[str, Any]) -> list:
146
+ """Build the final command with prompt and system instructions."""
147
+ full_prompt = f"{context}\n\n{prompt}" if context else prompt
148
+ cmd = infrastructure['cmd'] + ["--print", full_prompt]
149
+
150
+ # Add system instructions if available
151
+ system_prompt = self.runner._create_system_prompt()
152
+ if system_prompt and system_prompt != self._get_simple_context():
153
+ cmd.insert(-2, "--append-system-prompt")
154
+ cmd.insert(-2, system_prompt)
155
+
156
+ return cmd
157
+
158
+ def _notify_execution_start(self) -> None:
159
+ """Log and notify about execution start."""
160
+ if self.runner.project_logger:
161
+ self.runner.project_logger.log_system(
162
+ "Executing Claude subprocess",
163
+ level="INFO",
164
+ component="session"
165
+ )
166
+
167
+ if self.runner.websocket_server:
168
+ self.runner.websocket_server.claude_status_changed(
169
+ status="running",
170
+ message="Executing Claude oneshot command"
171
+ )
172
+
173
+ def _run_subprocess(self, cmd: list, env: dict, prompt: str) -> Tuple[bool, Optional[str]]:
174
+ """Run the subprocess and handle all exception types."""
175
+ try:
176
+ result = subprocess.run(cmd, capture_output=True, text=True, env=env)
177
+
178
+ if result.returncode == 0:
179
+ response = result.stdout.strip()
180
+ self._handle_successful_response(response, prompt)
181
+ return (True, response)
182
+ else:
183
+ error_msg = result.stderr or "Unknown error"
184
+ self._handle_error_response(error_msg, result.returncode)
185
+ return (False, error_msg)
186
+
187
+ except subprocess.TimeoutExpired as e:
188
+ return self._handle_timeout(e)
189
+ except FileNotFoundError:
190
+ return self._handle_claude_not_found()
191
+ except PermissionError as e:
192
+ return self._handle_permission_error(e)
193
+ except KeyboardInterrupt:
194
+ return self._handle_keyboard_interrupt()
195
+ except MemoryError as e:
196
+ return self._handle_memory_error(e)
197
+ except Exception as e:
198
+ return self._handle_unexpected_error(e)
199
+
200
+ def cleanup_session(self) -> None:
201
+ """Clean up the session and restore state."""
202
+ # Restore original working directory
203
+ if self.original_cwd:
204
+ try:
205
+ os.chdir(self.original_cwd)
206
+ except Exception:
207
+ pass
208
+
209
+ # Log session summary
210
+ if self.runner.project_logger:
211
+ try:
212
+ summary = self.runner.project_logger.get_session_summary()
213
+ self.runner.project_logger.log_system(
214
+ f"Session {summary['session_id']} completed",
215
+ level="INFO",
216
+ component="session"
217
+ )
218
+ except Exception as e:
219
+ self.logger.debug(f"Failed to log session summary: {e}")
220
+
221
+ # End WebSocket session
222
+ if self.runner.websocket_server:
223
+ self.runner.websocket_server.claude_status_changed(
224
+ status="stopped",
225
+ message="Session completed"
226
+ )
227
+ self.runner.websocket_server.session_ended()
228
+
229
+ # Private helper methods
230
+
231
+ def _setup_websocket(self) -> None:
232
+ """Initialize WebSocket connection."""
233
+ try:
234
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
235
+ self.runner.websocket_server = SocketIOClientProxy(
236
+ port=self.runner.websocket_port
237
+ )
238
+ self.runner.websocket_server.start()
239
+ self.logger.info("Connected to Socket.IO monitoring server")
240
+
241
+ # Notify session start
242
+ self.runner.websocket_server.session_started(
243
+ session_id=self.session_id,
244
+ launch_method="oneshot",
245
+ working_dir=os.getcwd()
246
+ )
247
+ except (ImportError, ConnectionError, Exception) as e:
248
+ self.logger.warning(f"Socket.IO connection failed: {e}")
249
+ self.runner.websocket_server = None
250
+
251
+ def _prepare_environment(self) -> Dict[str, str]:
252
+ """Prepare the execution environment."""
253
+ return os.environ.copy()
254
+
255
+ def _build_command(self) -> list:
256
+ """Build the base Claude command."""
257
+ cmd = [
258
+ "claude",
259
+ "--model", "opus",
260
+ "--dangerously-skip-permissions"
261
+ ]
262
+
263
+ # Add custom arguments
264
+ if self.runner.claude_args:
265
+ cmd.extend(self.runner.claude_args)
266
+
267
+ return cmd
268
+
269
+ def _handle_successful_response(self, response: str, prompt: str) -> None:
270
+ """Process a successful Claude response."""
271
+ print(response)
272
+
273
+ execution_time = time.time() - self.start_time
274
+
275
+ # Log response if enabled
276
+ if self.runner.response_logger and response:
277
+ response_summary = prompt[:200] + "..." if len(prompt) > 200 else prompt
278
+ self.runner.response_logger.log_response(
279
+ request_summary=response_summary,
280
+ response_content=response,
281
+ metadata={
282
+ "mode": "oneshot",
283
+ "model": "opus",
284
+ "exit_code": 0,
285
+ "execution_time": execution_time
286
+ },
287
+ agent="claude-direct"
288
+ )
289
+
290
+ # Broadcast to WebSocket
291
+ if self.runner.websocket_server and response:
292
+ self.runner.websocket_server.claude_output(response, "stdout")
293
+
294
+ # Check for delegation
295
+ if self.runner._contains_delegation(response):
296
+ agent_name = self.runner._extract_agent_from_response(response)
297
+ if agent_name:
298
+ self.runner.websocket_server.agent_delegated(
299
+ agent=agent_name,
300
+ task=prompt[:100],
301
+ status="detected"
302
+ )
303
+
304
+ # Log completion
305
+ if self.runner.project_logger:
306
+ self.runner.project_logger.log_system(
307
+ f"Non-interactive session completed successfully in {execution_time:.2f}s",
308
+ level="INFO",
309
+ component="session"
310
+ )
311
+
312
+ self.runner._log_session_event({
313
+ "event": "session_complete",
314
+ "success": True,
315
+ "execution_time": execution_time,
316
+ "response_length": len(response)
317
+ })
318
+
319
+ # Extract tickets if enabled
320
+ if self.runner.enable_tickets and self.runner.ticket_manager and response:
321
+ self.runner._extract_tickets(response)
322
+
323
+ def _handle_error_response(self, error_msg: str, return_code: int) -> None:
324
+ """Handle an error response from Claude."""
325
+ print(f"Error: {error_msg}")
326
+
327
+ # Broadcast error
328
+ if self.runner.websocket_server:
329
+ self.runner.websocket_server.claude_output(error_msg, "stderr")
330
+ self.runner.websocket_server.claude_status_changed(
331
+ status="error",
332
+ message=f"Command failed with code {return_code}"
333
+ )
334
+
335
+ # Log error
336
+ if self.runner.project_logger:
337
+ self.runner.project_logger.log_system(
338
+ f"Non-interactive session failed: {error_msg}",
339
+ level="ERROR",
340
+ component="session"
341
+ )
342
+ self.runner._log_session_event({
343
+ "event": "session_failed",
344
+ "success": False,
345
+ "error": error_msg,
346
+ "return_code": return_code
347
+ })
348
+
349
+ def _handle_timeout(self, e: subprocess.TimeoutExpired) -> Tuple[bool, str]:
350
+ """Handle command timeout."""
351
+ error_msg = f"Command timed out after {e.timeout} seconds"
352
+ print(f"⏱️ {error_msg}")
353
+
354
+ if self.runner.project_logger:
355
+ self.runner.project_logger.log_system(error_msg, level="ERROR", component="session")
356
+ self.runner._log_session_event({
357
+ "event": "session_timeout",
358
+ "success": False,
359
+ "timeout": e.timeout,
360
+ "exception_type": "TimeoutExpired"
361
+ })
362
+
363
+ return (False, error_msg)
364
+
365
+ def _handle_claude_not_found(self) -> Tuple[bool, str]:
366
+ """Handle Claude CLI not found error."""
367
+ error_msg = "Claude CLI not found. Please ensure 'claude' is installed and in your PATH"
368
+ print(f"❌ {error_msg}")
369
+ print("\n💡 To fix: Install Claude CLI with 'npm install -g @anthropic-ai/claude-ai'")
370
+
371
+ if self.runner.project_logger:
372
+ self.runner.project_logger.log_system(
373
+ f"{error_msg}",
374
+ level="ERROR",
375
+ component="session"
376
+ )
377
+ self.runner._log_session_event({
378
+ "event": "session_exception",
379
+ "success": False,
380
+ "exception": "FileNotFoundError",
381
+ "exception_type": "FileNotFoundError"
382
+ })
383
+
384
+ return (False, error_msg)
385
+
386
+ def _handle_permission_error(self, e: PermissionError) -> Tuple[bool, str]:
387
+ """Handle permission denied error."""
388
+ error_msg = f"Permission denied executing Claude CLI: {e}"
389
+ print(f"❌ {error_msg}")
390
+
391
+ if self.runner.project_logger:
392
+ self.runner.project_logger.log_system(error_msg, level="ERROR", component="session")
393
+ self.runner._log_session_event({
394
+ "event": "session_exception",
395
+ "success": False,
396
+ "exception": str(e),
397
+ "exception_type": "PermissionError"
398
+ })
399
+
400
+ return (False, error_msg)
401
+
402
+ def _handle_keyboard_interrupt(self) -> Tuple[bool, str]:
403
+ """Handle keyboard interrupt."""
404
+ print("\n⚠️ Command interrupted by user")
405
+
406
+ if self.runner.project_logger:
407
+ self.runner.project_logger.log_system(
408
+ "Session interrupted by user",
409
+ level="INFO",
410
+ component="session"
411
+ )
412
+ self.runner._log_session_event({
413
+ "event": "session_interrupted",
414
+ "success": False,
415
+ "reason": "user_interrupt"
416
+ })
417
+
418
+ return (False, "User interrupted")
419
+
420
+ def _handle_memory_error(self, e: MemoryError) -> Tuple[bool, str]:
421
+ """Handle out of memory error."""
422
+ error_msg = "Out of memory while processing command"
423
+ print(f"❌ {error_msg}")
424
+
425
+ if self.runner.project_logger:
426
+ self.runner.project_logger.log_system(
427
+ f"{error_msg}: {e}",
428
+ level="ERROR",
429
+ component="session"
430
+ )
431
+ self.runner._log_session_event({
432
+ "event": "session_exception",
433
+ "success": False,
434
+ "exception": str(e),
435
+ "exception_type": "MemoryError"
436
+ })
437
+
438
+ return (False, error_msg)
439
+
440
+ def _handle_unexpected_error(self, e: Exception) -> Tuple[bool, str]:
441
+ """Handle unexpected errors."""
442
+ error_msg = f"Unexpected error: {e}"
443
+ print(f"❌ {error_msg}")
444
+ print(f" Error type: {type(e).__name__}")
445
+
446
+ if self.runner.project_logger:
447
+ self.runner.project_logger.log_system(
448
+ f"Exception during non-interactive session: {e}",
449
+ level="ERROR",
450
+ component="session"
451
+ )
452
+ self.runner._log_session_event({
453
+ "event": "session_exception",
454
+ "success": False,
455
+ "exception": str(e),
456
+ "exception_type": type(e).__name__
457
+ })
458
+
459
+ return (False, error_msg)
460
+
461
+ def _get_simple_context(self) -> str:
462
+ """Get the simple context string for comparison."""
463
+ # Import here to avoid circular dependency
464
+ from claude_mpm.core.claude_runner import create_simple_context
465
+ return create_simple_context()