claude-mpm 3.4.26__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +182 -299
- claude_mpm/agents/agent_loader.py +283 -57
- claude_mpm/agents/agent_loader_integration.py +6 -9
- claude_mpm/agents/base_agent.json +2 -1
- claude_mpm/agents/base_agent_loader.py +1 -1
- claude_mpm/cli/__init__.py +6 -10
- claude_mpm/cli/commands/__init__.py +0 -2
- claude_mpm/cli/commands/agents.py +1 -1
- claude_mpm/cli/commands/memory.py +1 -1
- claude_mpm/cli/commands/run.py +12 -0
- claude_mpm/cli/parser.py +0 -13
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/config/__init__.py +44 -2
- claude_mpm/config/agent_config.py +348 -0
- claude_mpm/config/paths.py +322 -0
- claude_mpm/constants.py +0 -1
- claude_mpm/core/__init__.py +2 -5
- claude_mpm/core/agent_registry.py +63 -17
- claude_mpm/core/claude_runner.py +354 -43
- claude_mpm/core/config.py +7 -1
- claude_mpm/core/config_aliases.py +4 -3
- claude_mpm/core/config_paths.py +151 -0
- claude_mpm/core/factories.py +4 -50
- claude_mpm/core/logger.py +11 -13
- claude_mpm/core/service_registry.py +2 -2
- claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
- claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/init.py +37 -6
- claude_mpm/scripts/socketio_daemon.py +6 -2
- claude_mpm/services/__init__.py +71 -3
- claude_mpm/services/agents/__init__.py +85 -0
- claude_mpm/services/agents/deployment/__init__.py +21 -0
- claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
- claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
- claude_mpm/services/agents/loading/__init__.py +11 -0
- claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
- claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
- claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
- claude_mpm/services/agents/management/__init__.py +9 -0
- claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
- claude_mpm/services/agents/memory/__init__.py +21 -0
- claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
- claude_mpm/services/agents/registry/__init__.py +29 -0
- claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
- claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
- claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
- claude_mpm/services/async_session_logger.py +584 -0
- claude_mpm/services/claude_session_logger.py +299 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
- claude_mpm/services/framework_claude_md_generator.py +4 -2
- claude_mpm/services/memory/__init__.py +17 -0
- claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
- claude_mpm/services/memory/cache/__init__.py +14 -0
- claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
- claude_mpm/services/memory/cache/simple_cache.py +317 -0
- claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
- claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
- claude_mpm/services/optimized_hook_service.py +542 -0
- claude_mpm/services/project_registry.py +14 -8
- claude_mpm/services/response_tracker.py +237 -0
- claude_mpm/services/ticketing_service_original.py +4 -2
- claude_mpm/services/version_control/branch_strategy.py +3 -1
- claude_mpm/utils/paths.py +12 -10
- claude_mpm/utils/session_logging.py +114 -0
- claude_mpm/validation/agent_validator.py +2 -1
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
- claude_mpm/cli/commands/ui.py +0 -57
- claude_mpm/core/simple_runner.py +0 -1046
- claude_mpm/hooks/builtin/__init__.py +0 -1
- claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
- claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
- claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
- claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
- claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
- claude_mpm/orchestration/__init__.py +0 -6
- claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
- claude_mpm/orchestration/archive/factory.py +0 -215
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
- claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
- claude_mpm/orchestration/archive/orchestrator.py +0 -501
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
- claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
- claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
- claude_mpm/schemas/workflow_validator.py +0 -411
- claude_mpm/services/parent_directory_manager/__init__.py +0 -577
- claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
- claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
- claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
- claude_mpm/services/parent_directory_manager/operations.py +0 -186
- claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
- claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
- claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
- claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
- claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
- claude_mpm/ui/__init__.py +0 -1
- claude_mpm/ui/rich_terminal_ui.py +0 -295
- claude_mpm/ui/terminal_ui.py +0 -328
- /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
- /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
- /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.26.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.26.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()
|