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