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,826 +0,0 @@
1
- """
2
- Interactive Subprocess Orchestrator using pexpect for Claude CLI control.
3
-
4
- This orchestrator creates controlled subprocesses for agent delegations,
5
- monitoring their execution and resource usage while maintaining interactive
6
- control through pexpect.
7
- """
8
-
9
- import os
10
- import pexpect
11
- import subprocess
12
- import concurrent.futures
13
- import json
14
- import time
15
- import logging
16
- import psutil
17
- import threading
18
- import uuid
19
- from pathlib import Path
20
- from typing import Dict, List, Any, Optional, Tuple
21
- from datetime import datetime
22
- from dataclasses import dataclass
23
- from enum import Enum
24
- import re
25
-
26
- try:
27
- from ..core.logger import get_logger, setup_logging
28
- # TicketExtractor removed from project
29
- from ..core.framework_loader import FrameworkLoader
30
- from .agent_delegator import AgentDelegator
31
- except ImportError:
32
- from core.logger import get_logger, setup_logging
33
- # TicketExtractor removed from project
34
- from core.framework_loader import FrameworkLoader
35
- from orchestration.agent_delegator import AgentDelegator
36
-
37
-
38
- # ==============================================================================
39
- # Data Structures
40
- # ==============================================================================
41
-
42
- @dataclass
43
- class AgentExecutionResult:
44
- """Result of an agent subprocess execution."""
45
- success: bool
46
- agent_type: str
47
- task_description: str
48
- stdout: str
49
- stderr: str
50
- exit_code: int
51
- execution_time: float
52
- memory_usage: Dict[str, int]
53
- tickets_created: List[str]
54
- error: Optional[str] = None
55
- process_id: Optional[str] = None
56
-
57
-
58
- class ProcessStatus(Enum):
59
- """Status of a subprocess execution."""
60
- RUNNING = "running"
61
- COMPLETED = "completed"
62
- FAILED = "failed"
63
- TIMEOUT = "timeout"
64
- MEMORY_EXCEEDED = "memory_exceeded"
65
-
66
-
67
- # ==============================================================================
68
- # Memory Monitor
69
- # ==============================================================================
70
-
71
- class MemoryMonitor:
72
- """Monitor memory usage of subprocesses."""
73
-
74
- def __init__(self, warning_threshold_mb: int = 512,
75
- critical_threshold_mb: int = 1024,
76
- hard_limit_mb: int = 2048):
77
- """
78
- Initialize memory monitor.
79
-
80
- Args:
81
- warning_threshold_mb: Memory usage warning threshold in MB
82
- critical_threshold_mb: Critical memory usage threshold in MB
83
- hard_limit_mb: Hard limit that triggers process termination in MB
84
- """
85
- self.warning_threshold = warning_threshold_mb * 1024 * 1024
86
- self.critical_threshold = critical_threshold_mb * 1024 * 1024
87
- self.hard_limit = hard_limit_mb * 1024 * 1024
88
- self.monitoring = False
89
- self.logger = get_logger("memory_monitor")
90
-
91
- def start_monitoring(self, process_pid: int) -> threading.Thread:
92
- """Start memory monitoring in separate thread."""
93
- self.monitoring = True
94
-
95
- def monitor():
96
- try:
97
- ps_process = psutil.Process(process_pid)
98
- while self.monitoring:
99
- try:
100
- memory_info = ps_process.memory_info()
101
- rss = memory_info.rss
102
-
103
- if rss > self.hard_limit:
104
- self.logger.critical(
105
- f"Process {process_pid} exceeded hard limit "
106
- f"({rss/1024/1024:.1f}MB > {self.hard_limit/1024/1024:.1f}MB)"
107
- )
108
- ps_process.terminate()
109
- break
110
- elif rss > self.critical_threshold:
111
- self.logger.warning(
112
- f"Process {process_pid} critical memory usage: "
113
- f"{rss/1024/1024:.1f}MB"
114
- )
115
- elif rss > self.warning_threshold:
116
- self.logger.info(
117
- f"Process {process_pid} high memory usage: "
118
- f"{rss/1024/1024:.1f}MB"
119
- )
120
-
121
- time.sleep(2) # Check every 2 seconds
122
- except psutil.NoSuchProcess:
123
- break
124
- except Exception as e:
125
- self.logger.error(f"Memory monitoring error: {e}")
126
-
127
- thread = threading.Thread(target=monitor, daemon=True)
128
- thread.start()
129
- return thread
130
-
131
- def stop_monitoring(self):
132
- """Stop memory monitoring."""
133
- self.monitoring = False
134
-
135
- def get_memory_usage(self, process_pid: int) -> Dict[str, int]:
136
- """Get current memory usage statistics."""
137
- try:
138
- ps_process = psutil.Process(process_pid)
139
- memory_info = ps_process.memory_info()
140
- return {
141
- "rss_mb": memory_info.rss // (1024 * 1024),
142
- "vms_mb": memory_info.vms // (1024 * 1024),
143
- "percent": ps_process.memory_percent()
144
- }
145
- except psutil.NoSuchProcess:
146
- return {"rss_mb": 0, "vms_mb": 0, "percent": 0.0}
147
-
148
-
149
- # ==============================================================================
150
- # Process Manager
151
- # ==============================================================================
152
-
153
- class ProcessManager:
154
- """Manage subprocess lifecycles and resource usage."""
155
-
156
- def __init__(self):
157
- """Initialize process manager."""
158
- self.active_processes: Dict[str, pexpect.spawn] = {}
159
- self.memory_monitors: Dict[str, MemoryMonitor] = {}
160
- self.process_metadata: Dict[str, Dict[str, Any]] = {}
161
- self.logger = get_logger("process_manager")
162
-
163
- def create_interactive_process(self, command: List[str], env: Dict[str, str],
164
- memory_limit_mb: int = 1024,
165
- timeout: int = 300) -> Tuple[str, pexpect.spawn]:
166
- """
167
- Create and return managed interactive subprocess using pexpect.
168
-
169
- Args:
170
- command: Command and arguments to execute
171
- env: Environment variables
172
- memory_limit_mb: Memory limit in MB
173
- timeout: Timeout in seconds
174
-
175
- Returns:
176
- Tuple of (process_id, pexpect.spawn instance)
177
- """
178
- process_id = str(uuid.uuid4())
179
-
180
- try:
181
- # Create pexpect spawn instance
182
- process = pexpect.spawn(
183
- command[0],
184
- command[1:],
185
- encoding='utf-8',
186
- timeout=timeout,
187
- env=env,
188
- dimensions=(24, 80)
189
- )
190
-
191
- # Start memory monitoring
192
- memory_monitor = MemoryMonitor(
193
- warning_threshold_mb=memory_limit_mb // 2,
194
- critical_threshold_mb=int(memory_limit_mb * 0.8),
195
- hard_limit_mb=memory_limit_mb
196
- )
197
-
198
- self.active_processes[process_id] = process
199
- self.memory_monitors[process_id] = memory_monitor
200
- self.process_metadata[process_id] = {
201
- "command": command,
202
- "start_time": datetime.now(),
203
- "memory_limit_mb": memory_limit_mb,
204
- "timeout": timeout
205
- }
206
-
207
- # Start monitoring after process is registered
208
- memory_monitor.start_monitoring(process.pid)
209
-
210
- self.logger.info(f"Created interactive process {process_id} (PID: {process.pid})")
211
- return process_id, process
212
-
213
- except Exception as e:
214
- self.logger.error(f"Failed to create process: {e}")
215
- raise RuntimeError(f"Failed to create process: {e}")
216
-
217
- def send_to_process(self, process_id: str, input_data: str) -> bool:
218
- """
219
- Send input to an interactive process.
220
-
221
- Args:
222
- process_id: Process identifier
223
- input_data: Data to send
224
-
225
- Returns:
226
- True if successful, False otherwise
227
- """
228
- if process_id not in self.active_processes:
229
- self.logger.error(f"Process {process_id} not found")
230
- return False
231
-
232
- try:
233
- process = self.active_processes[process_id]
234
- process.sendline(input_data)
235
- return True
236
- except Exception as e:
237
- self.logger.error(f"Failed to send to process {process_id}: {e}")
238
- return False
239
-
240
- def read_from_process(self, process_id: str, pattern: str = None,
241
- timeout: int = 30) -> Optional[str]:
242
- """
243
- Read output from an interactive process.
244
-
245
- Args:
246
- process_id: Process identifier
247
- pattern: Pattern to expect (default: Claude's '>' prompt)
248
- timeout: Read timeout in seconds
249
-
250
- Returns:
251
- Output string or None if failed
252
- """
253
- if process_id not in self.active_processes:
254
- self.logger.error(f"Process {process_id} not found")
255
- return None
256
-
257
- try:
258
- process = self.active_processes[process_id]
259
-
260
- if pattern:
261
- process.expect(pattern, timeout=timeout)
262
- else:
263
- # Default to Claude's prompt
264
- process.expect('>', timeout=timeout)
265
-
266
- return process.before
267
- except pexpect.TIMEOUT:
268
- self.logger.warning(f"Timeout reading from process {process_id}")
269
- return None
270
- except Exception as e:
271
- self.logger.error(f"Failed to read from process {process_id}: {e}")
272
- return None
273
-
274
- def terminate_process(self, process_id: str) -> Dict[str, Any]:
275
- """
276
- Terminate a process and return its final statistics.
277
-
278
- Args:
279
- process_id: Process identifier
280
-
281
- Returns:
282
- Dictionary with process statistics
283
- """
284
- if process_id not in self.active_processes:
285
- return {"error": f"Process {process_id} not found"}
286
-
287
- process = self.active_processes[process_id]
288
- memory_monitor = self.memory_monitors.get(process_id)
289
- metadata = self.process_metadata.get(process_id, {})
290
-
291
- # Get final memory usage
292
- memory_usage = {"rss_mb": 0, "vms_mb": 0, "percent": 0.0}
293
- if memory_monitor and process.pid:
294
- memory_usage = memory_monitor.get_memory_usage(process.pid)
295
- memory_monitor.stop_monitoring()
296
-
297
- # Calculate execution time
298
- start_time = metadata.get("start_time", datetime.now())
299
- execution_time = (datetime.now() - start_time).total_seconds()
300
-
301
- # Terminate process
302
- try:
303
- process.close()
304
- exit_code = process.exitstatus or -1
305
- except Exception as e:
306
- self.logger.error(f"Error terminating process {process_id}: {e}")
307
- exit_code = -1
308
-
309
- # Clean up
310
- if process_id in self.active_processes:
311
- del self.active_processes[process_id]
312
- if process_id in self.memory_monitors:
313
- del self.memory_monitors[process_id]
314
- if process_id in self.process_metadata:
315
- del self.process_metadata[process_id]
316
-
317
- return {
318
- "process_id": process_id,
319
- "exit_code": exit_code,
320
- "execution_time": execution_time,
321
- "memory_usage": memory_usage
322
- }
323
-
324
- def get_active_processes(self) -> List[Dict[str, Any]]:
325
- """Get list of active processes with their metadata."""
326
- active = []
327
- for process_id, process in self.active_processes.items():
328
- metadata = self.process_metadata.get(process_id, {})
329
- memory_usage = {"rss_mb": 0, "vms_mb": 0, "percent": 0.0}
330
-
331
- if process.pid:
332
- monitor = self.memory_monitors.get(process_id)
333
- if monitor:
334
- memory_usage = monitor.get_memory_usage(process.pid)
335
-
336
- active.append({
337
- "process_id": process_id,
338
- "pid": process.pid,
339
- "command": metadata.get("command", []),
340
- "start_time": metadata.get("start_time", "").isoformat() if metadata.get("start_time") else "",
341
- "memory_usage": memory_usage,
342
- "memory_limit_mb": metadata.get("memory_limit_mb", 0)
343
- })
344
-
345
- return active
346
-
347
-
348
- # ==============================================================================
349
- # Interactive Subprocess Orchestrator
350
- # ==============================================================================
351
-
352
- class InteractiveSubprocessOrchestrator:
353
- """
354
- Orchestrator that creates controlled subprocesses for agent delegations
355
- using pexpect for interactive control.
356
- """
357
-
358
- def __init__(
359
- self,
360
- framework_path: Optional[Path] = None,
361
- agents_dir: Optional[Path] = None,
362
- log_level: str = "INFO",
363
- log_dir: Optional[Path] = None,
364
- hook_manager=None,
365
- ):
366
- """
367
- Initialize the interactive subprocess orchestrator.
368
-
369
- Args:
370
- framework_path: Path to framework directory
371
- agents_dir: Path to agents directory
372
- log_level: Logging level
373
- log_dir: Directory for log files
374
- hook_manager: Hook service manager instance
375
- """
376
- self.log_level = log_level
377
- self.log_dir = log_dir or (Path.home() / ".claude-mpm" / "logs")
378
- self.hook_manager = hook_manager
379
-
380
- # Set up logging
381
- self.logger = setup_logging(level=log_level, log_dir=log_dir)
382
- self.logger.info(f"Initializing Interactive Subprocess Orchestrator (log_level={log_level})")
383
- if hook_manager and hook_manager.is_available():
384
- self.logger.info(f"Hook service available on port {hook_manager.port}")
385
-
386
- # Components
387
- self.framework_loader = FrameworkLoader(framework_path, agents_dir)
388
- # TicketExtractor removed from project
389
- self.agent_delegator = AgentDelegator(self.framework_loader.agent_registry)
390
- self.process_manager = ProcessManager()
391
-
392
- # State
393
- self.session_start = datetime.now()
394
- # Ticket creation removed from project
395
- self.parallel_execution_enabled = True
396
- self.max_parallel_processes = 8
397
-
398
- def detect_delegations(self, response: str) -> List[Dict[str, str]]:
399
- """
400
- Detect delegation requests in PM response.
401
-
402
- Looks for patterns like:
403
- - **Engineer Agent**: Create a function...
404
- - **QA**: Write tests...
405
- - Task Tool → Documentation Agent: Generate changelog
406
-
407
- Args:
408
- response: PM response text
409
-
410
- Returns:
411
- List of delegations with agent and task
412
- """
413
- delegations = []
414
-
415
- # Pattern 1: **Agent Name**: task
416
- pattern1 = r'\*\*([^*]+?)(?:\s+Agent)?\*\*:\s*(.+?)(?=\n\n|\n\*\*|$)'
417
- for match in re.finditer(pattern1, response, re.MULTILINE | re.DOTALL):
418
- agent = match.group(1).strip()
419
- task = match.group(2).strip()
420
- delegations.append({
421
- 'agent': agent,
422
- 'task': task,
423
- 'format': 'markdown'
424
- })
425
-
426
- # Pattern 2: Task Tool → Agent: task
427
- pattern2 = r'Task Tool\s*→\s*([^:]+):\s*(.+?)(?=\n\n|\nTask Tool|$)'
428
- for match in re.finditer(pattern2, response, re.MULTILINE | re.DOTALL):
429
- agent = match.group(1).strip().replace(' Agent', '')
430
- task = match.group(2).strip()
431
- delegations.append({
432
- 'agent': agent,
433
- 'task': task,
434
- 'format': 'task_tool'
435
- })
436
-
437
- self.logger.info(f"Detected {len(delegations)} delegations")
438
- for d in delegations:
439
- self.logger.debug(f" {d['agent']}: {d['task'][:50]}...")
440
-
441
- return delegations
442
-
443
- def create_agent_prompt(self, agent: str, task: str, context: Dict[str, Any] = None) -> str:
444
- """
445
- Create a prompt for an agent subprocess.
446
-
447
- Args:
448
- agent: Agent name
449
- task: Task description
450
- context: Additional context for the agent
451
-
452
- Returns:
453
- Complete prompt including agent-specific framework
454
- """
455
- # Get agent-specific content
456
- agent_content = ""
457
- agent_key = agent.lower().replace(' ', '_') + '_agent'
458
-
459
- if agent_key in self.framework_loader.framework_content.get('agents', {}):
460
- agent_content = self.framework_loader.framework_content['agents'][agent_key]
461
-
462
- # Add temporal context
463
- temporal_context = f"Today is {datetime.now().strftime('%Y-%m-%d')}."
464
-
465
- # Build focused agent prompt
466
- prompt = f"""You are the {agent} Agent in the Claude PM Framework.
467
-
468
- {agent_content}
469
-
470
- TEMPORAL CONTEXT: {temporal_context}
471
-
472
- ## Current Task
473
- {task}
474
-
475
- ## Context
476
- {json.dumps(context, indent=2) if context else 'No additional context provided.'}
477
-
478
- ## Response Format
479
- Provide a clear, structured response that:
480
- 1. Confirms your role as {agent} Agent
481
- 2. Completes the requested task
482
- 3. Reports any issues or blockers
483
- 4. Summarizes deliverables
484
-
485
- Remember: You are an autonomous agent. Complete the task independently and report results."""
486
-
487
- return prompt
488
-
489
- def run_agent_subprocess(self, agent: str, task: str,
490
- context: Dict[str, Any] = None,
491
- timeout: int = 300,
492
- memory_limit_mb: int = 1024) -> AgentExecutionResult:
493
- """
494
- Run a single agent subprocess with interactive control.
495
-
496
- Args:
497
- agent: Agent name
498
- task: Task description
499
- context: Additional context
500
- timeout: Execution timeout in seconds
501
- memory_limit_mb: Memory limit in MB
502
-
503
- Returns:
504
- AgentExecutionResult with execution details
505
- """
506
- start_time = time.time()
507
-
508
- # Create agent prompt
509
- prompt = self.create_agent_prompt(agent, task, context)
510
-
511
- # Prepare environment
512
- env = os.environ.copy()
513
- env.update({
514
- 'CLAUDE_PM_ORCHESTRATED': 'true',
515
- 'CLAUDE_PM_AGENT': agent,
516
- 'CLAUDE_PM_SESSION_ID': str(uuid.uuid4()),
517
- 'CLAUDE_PM_FRAMEWORK_VERSION': '1.4.0'
518
- })
519
-
520
- try:
521
- # Create interactive subprocess
522
- command = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
523
- process_id, process = self.process_manager.create_interactive_process(
524
- command, env, memory_limit_mb, timeout
525
- )
526
-
527
- self.logger.info(f"Started subprocess {process_id} for {agent}")
528
-
529
- # Wait for initial prompt
530
- initial_output = self.process_manager.read_from_process(process_id, '>', timeout=10)
531
- if initial_output is None:
532
- raise RuntimeError("Failed to get initial prompt from Claude")
533
-
534
- # Send agent prompt
535
- if not self.process_manager.send_to_process(process_id, prompt):
536
- raise RuntimeError("Failed to send prompt to subprocess")
537
-
538
- # Read response
539
- response = self.process_manager.read_from_process(process_id, '>', timeout=timeout)
540
- if response is None:
541
- raise RuntimeError("Failed to read response from subprocess")
542
-
543
- # Get process statistics
544
- stats = self.process_manager.terminate_process(process_id)
545
-
546
- execution_time = time.time() - start_time
547
-
548
- # Ticket extraction removed from project
549
- ticket_ids = []
550
-
551
- return AgentExecutionResult(
552
- success=True,
553
- agent_type=agent,
554
- task_description=task,
555
- stdout=response,
556
- stderr="",
557
- exit_code=stats.get("exit_code", 0),
558
- execution_time=execution_time,
559
- memory_usage=stats.get("memory_usage", {}),
560
- tickets_created=ticket_ids,
561
- process_id=process_id
562
- )
563
-
564
- except Exception as e:
565
- self.logger.error(f"Subprocess execution failed for {agent}: {e}")
566
-
567
- # Clean up if process exists
568
- if 'process_id' in locals():
569
- stats = self.process_manager.terminate_process(process_id)
570
-
571
- execution_time = time.time() - start_time
572
-
573
- return AgentExecutionResult(
574
- success=False,
575
- agent_type=agent,
576
- task_description=task,
577
- stdout="",
578
- stderr=str(e),
579
- exit_code=-1,
580
- execution_time=execution_time,
581
- memory_usage={},
582
- tickets_created=[],
583
- error=str(e)
584
- )
585
-
586
- def run_parallel_delegations(self, delegations: List[Dict[str, str]]) -> List[AgentExecutionResult]:
587
- """
588
- Run multiple agent delegations in parallel.
589
-
590
- Args:
591
- delegations: List of delegation dicts with 'agent' and 'task'
592
-
593
- Returns:
594
- List of AgentExecutionResult objects
595
- """
596
- results = []
597
-
598
- if not self.parallel_execution_enabled:
599
- # Run sequentially
600
- for delegation in delegations:
601
- result = self.run_agent_subprocess(
602
- delegation['agent'],
603
- delegation['task']
604
- )
605
- results.append(result)
606
- else:
607
- # Run in parallel with ThreadPoolExecutor
608
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_parallel_processes) as executor:
609
- # Submit all tasks
610
- future_to_delegation = {}
611
- for delegation in delegations:
612
- future = executor.submit(
613
- self.run_agent_subprocess,
614
- delegation['agent'],
615
- delegation['task']
616
- )
617
- future_to_delegation[future] = delegation
618
-
619
- # Collect results as they complete
620
- for future in concurrent.futures.as_completed(future_to_delegation):
621
- delegation = future_to_delegation[future]
622
- try:
623
- result = future.result()
624
- results.append(result)
625
- except Exception as e:
626
- # Create error result
627
- results.append(AgentExecutionResult(
628
- success=False,
629
- agent_type=delegation['agent'],
630
- task_description=delegation['task'],
631
- stdout="",
632
- stderr=str(e),
633
- exit_code=-1,
634
- execution_time=0,
635
- memory_usage={},
636
- tickets_created=[],
637
- error=str(e)
638
- ))
639
-
640
- return results
641
-
642
- def format_execution_results(self, results: List[AgentExecutionResult]) -> str:
643
- """
644
- Format execution results in a readable format.
645
-
646
- Args:
647
- results: List of AgentExecutionResult objects
648
-
649
- Returns:
650
- Formatted string output
651
- """
652
- output = []
653
-
654
- # Summary
655
- successful = sum(1 for r in results if r.success)
656
- failed = len(results) - successful
657
- total_time = sum(r.execution_time for r in results)
658
-
659
- output.append("## Subprocess Execution Summary")
660
- output.append(f"- Total delegations: {len(results)}")
661
- output.append(f"- Successful: {successful}")
662
- output.append(f"- Failed: {failed}")
663
- output.append(f"- Total execution time: {total_time:.1f}s")
664
- output.append("")
665
-
666
- # Process list
667
- output.append("## Execution Details")
668
- for i, result in enumerate(results, 1):
669
- status = "✓" if result.success else "✗"
670
- mem_usage = result.memory_usage.get("rss_mb", 0)
671
-
672
- output.append(f"{i}. [{status}] {result.agent_type}: {result.task_description[:50]}...")
673
- output.append(f" - Execution time: {result.execution_time:.1f}s")
674
- output.append(f" - Memory usage: {mem_usage}MB")
675
- output.append(f" - Exit code: {result.exit_code}")
676
- if result.tickets_created:
677
- output.append(f" - Tickets created: {len(result.tickets_created)}")
678
- if result.error:
679
- output.append(f" - Error: {result.error}")
680
- output.append("")
681
-
682
- # Detailed responses
683
- output.append("## Agent Responses")
684
- for result in results:
685
- output.append(f"\n### {result.agent_type} Agent")
686
- output.append("-" * 50)
687
- if result.success:
688
- output.append(result.stdout)
689
- else:
690
- output.append(f"ERROR: {result.error}")
691
- if result.stderr:
692
- output.append(f"STDERR: {result.stderr}")
693
- output.append("")
694
-
695
- return "\n".join(output)
696
-
697
- def run_orchestrated_session(self, initial_prompt: str):
698
- """
699
- Run an orchestrated session with subprocess delegation.
700
-
701
- Args:
702
- initial_prompt: Initial user prompt to send to PM
703
- """
704
- self.logger.info("Starting orchestrated session")
705
-
706
- try:
707
- # Create PM subprocess
708
- env = os.environ.copy()
709
- command = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
710
-
711
- process_id, pm_process = self.process_manager.create_interactive_process(
712
- command, env, memory_limit_mb=2048, timeout=600
713
- )
714
-
715
- self.logger.info(f"Started PM subprocess {process_id}")
716
-
717
- # Wait for initial prompt
718
- initial_output = self.process_manager.read_from_process(process_id, '>', timeout=10)
719
- if initial_output is None:
720
- raise RuntimeError("Failed to get initial prompt from PM")
721
-
722
- # Send framework instructions
723
- framework = self.framework_loader.get_framework_instructions()
724
- if not self.process_manager.send_to_process(process_id, framework):
725
- raise RuntimeError("Failed to send framework to PM")
726
-
727
- # Read framework acknowledgment
728
- framework_response = self.process_manager.read_from_process(process_id, '>', timeout=60)
729
- if framework_response is None:
730
- raise RuntimeError("Failed to get framework acknowledgment")
731
-
732
- # Send user prompt
733
- if not self.process_manager.send_to_process(process_id, initial_prompt):
734
- raise RuntimeError("Failed to send user prompt to PM")
735
-
736
- # Read PM response
737
- pm_response = self.process_manager.read_from_process(process_id, '>', timeout=120)
738
- if pm_response is None:
739
- raise RuntimeError("Failed to get PM response")
740
-
741
- print("\n=== PM Response ===")
742
- print(pm_response)
743
- print("==================\n")
744
-
745
- # Detect delegations
746
- delegations = self.detect_delegations(pm_response)
747
-
748
- if delegations:
749
- print(f"\nDetected {len(delegations)} delegations. Running subprocesses...\n")
750
-
751
- # Run delegations
752
- results = self.run_parallel_delegations(delegations)
753
-
754
- # Format and display results
755
- formatted_results = self.format_execution_results(results)
756
- print(formatted_results)
757
-
758
- # Store tickets
759
- all_tickets = []
760
- for result in results:
761
- if result.tickets_created:
762
- for ticket_id in result.tickets_created:
763
- all_tickets.append({
764
- 'id': ticket_id,
765
- 'agent': result.agent_type,
766
- 'created_at': datetime.now().isoformat()
767
- })
768
-
769
- if all_tickets:
770
- print(f"\nTotal tickets created: {len(all_tickets)}")
771
- else:
772
- print("\nNo delegations detected in PM response.")
773
-
774
- # Terminate PM process
775
- self.process_manager.terminate_process(process_id)
776
-
777
- except Exception as e:
778
- self.logger.error(f"Orchestrated session error: {e}")
779
- print(f"\nError during orchestrated session: {e}")
780
-
781
- # Clean up any active processes
782
- for process_info in self.process_manager.get_active_processes():
783
- self.process_manager.terminate_process(process_info['process_id'])
784
-
785
- def get_status(self) -> Dict[str, Any]:
786
- """Get current orchestrator status."""
787
- return {
788
- "session_start": self.session_start.isoformat(),
789
- "active_processes": self.process_manager.get_active_processes(),
790
- "parallel_execution_enabled": self.parallel_execution_enabled,
791
- "max_parallel_processes": self.max_parallel_processes,
792
- # Ticket extraction removed from project
793
- }
794
-
795
-
796
- # ==============================================================================
797
- # CLI Integration
798
- # ==============================================================================
799
-
800
- def main():
801
- """Main entry point for testing."""
802
- import argparse
803
-
804
- parser = argparse.ArgumentParser(description="Interactive Subprocess Orchestrator")
805
- parser.add_argument("prompt", help="Initial prompt to send to PM")
806
- parser.add_argument("--log-level", default="INFO", help="Logging level")
807
- parser.add_argument("--no-parallel", action="store_true", help="Disable parallel execution")
808
-
809
- args = parser.parse_args()
810
-
811
- # Create orchestrator
812
- orchestrator = InteractiveSubprocessOrchestrator(log_level=args.log_level)
813
-
814
- if args.no_parallel:
815
- orchestrator.parallel_execution_enabled = False
816
-
817
- # Run orchestrated session
818
- orchestrator.run_orchestrated_session(args.prompt)
819
-
820
- # Display final status
821
- print("\n=== Session Status ===")
822
- print(json.dumps(orchestrator.get_status(), indent=2))
823
-
824
-
825
- if __name__ == "__main__":
826
- main()