claude-mpm 4.0.29__py3-none-any.whl → 4.0.30__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/agents/BASE_AGENT_TEMPLATE.md +48 -3
- claude_mpm/agents/BASE_PM.md +20 -15
- claude_mpm/agents/INSTRUCTIONS.md +12 -2
- claude_mpm/agents/templates/documentation.json +16 -3
- claude_mpm/agents/templates/engineer.json +19 -5
- claude_mpm/agents/templates/ops.json +19 -5
- claude_mpm/agents/templates/qa.json +16 -3
- claude_mpm/agents/templates/refactoring_engineer.json +25 -7
- claude_mpm/agents/templates/research.json +19 -5
- claude_mpm/cli/__init__.py +2 -0
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_manager.py +10 -6
- claude_mpm/cli/commands/agents.py +2 -1
- claude_mpm/cli/commands/cleanup.py +1 -1
- claude_mpm/cli/commands/doctor.py +209 -0
- claude_mpm/cli/commands/mcp.py +3 -3
- claude_mpm/cli/commands/mcp_install_commands.py +12 -30
- claude_mpm/cli/commands/mcp_server_commands.py +9 -9
- claude_mpm/cli/commands/run.py +31 -2
- claude_mpm/cli/commands/run_config_checker.py +1 -1
- claude_mpm/cli/parsers/agent_manager_parser.py +3 -3
- claude_mpm/cli/parsers/base_parser.py +5 -1
- claude_mpm/cli/parsers/mcp_parser.py +1 -1
- claude_mpm/cli/parsers/run_parser.py +1 -1
- claude_mpm/cli/startup_logging.py +463 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +78 -0
- claude_mpm/core/framework_loader.py +45 -11
- claude_mpm/core/interactive_session.py +82 -3
- claude_mpm/core/output_style_manager.py +6 -6
- claude_mpm/core/unified_paths.py +128 -0
- claude_mpm/scripts/mcp_server.py +2 -2
- claude_mpm/services/agents/deployment/agent_validator.py +1 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +69 -1
- claude_mpm/services/diagnostics/__init__.py +18 -0
- claude_mpm/services/diagnostics/checks/__init__.py +30 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +319 -0
- claude_mpm/services/diagnostics/checks/base_check.py +64 -0
- claude_mpm/services/diagnostics/checks/claude_desktop_check.py +283 -0
- claude_mpm/services/diagnostics/checks/common_issues_check.py +354 -0
- claude_mpm/services/diagnostics/checks/configuration_check.py +300 -0
- claude_mpm/services/diagnostics/checks/filesystem_check.py +233 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +255 -0
- claude_mpm/services/diagnostics/checks/mcp_check.py +315 -0
- claude_mpm/services/diagnostics/checks/monitor_check.py +282 -0
- claude_mpm/services/diagnostics/checks/startup_log_check.py +322 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +247 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +283 -0
- claude_mpm/services/diagnostics/models.py +120 -0
- claude_mpm/services/mcp_gateway/core/interfaces.py +1 -1
- claude_mpm/services/mcp_gateway/main.py +1 -1
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +3 -3
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
- claude_mpm/services/mcp_gateway/server/stdio_server.py +3 -3
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +2 -2
- claude_mpm/services/socketio/handlers/registry.py +39 -7
- claude_mpm/services/socketio/server/core.py +72 -22
- claude_mpm/validation/frontmatter_validator.py +1 -1
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/METADATA +4 -1
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/RECORD +64 -47
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/top_level.txt +0 -0
|
@@ -172,7 +172,7 @@ def add_mcp_subparser(subparsers) -> argparse.ArgumentParser:
|
|
|
172
172
|
"--test", action="store_true", help="Run in test mode with debug output"
|
|
173
173
|
)
|
|
174
174
|
server_mcp_parser.add_argument(
|
|
175
|
-
"--instructions", action="store_true", help="Show setup instructions for Claude
|
|
175
|
+
"--instructions", action="store_true", help="Show setup instructions for Claude Code"
|
|
176
176
|
)
|
|
177
177
|
|
|
178
178
|
return mcp_parser
|
|
@@ -78,7 +78,7 @@ def add_run_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
78
78
|
run_group.add_argument(
|
|
79
79
|
"--resume",
|
|
80
80
|
action="store_true",
|
|
81
|
-
help="Pass --resume flag to Claude
|
|
81
|
+
help="Pass --resume flag to Claude Code to resume the last conversation",
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
# Dependency checking options
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Startup logging utilities for MCP server and monitor setup status.
|
|
3
|
+
|
|
4
|
+
WHY: This module provides detailed startup logging for better debugging
|
|
5
|
+
visibility. It logs MCP server installation/configuration status and
|
|
6
|
+
monitor service initialization status during the startup sequence.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Use consistent INFO log format with existing startup messages
|
|
10
|
+
- Gracefully handle missing dependencies or services
|
|
11
|
+
- Provide informative but concise status messages
|
|
12
|
+
- Include helpful context for debugging
|
|
13
|
+
- Ensure logging works in all deployment contexts (dev, pipx, pip)
|
|
14
|
+
- Capture all startup logs to timestamped files for analysis
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
from ..core.logger import get_logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StartupStatusLogger:
|
|
30
|
+
"""Logs MCP server and monitor setup status during startup."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, logger_name: str = "startup_status"):
|
|
33
|
+
"""Initialize the startup status logger."""
|
|
34
|
+
self.logger = get_logger(logger_name)
|
|
35
|
+
|
|
36
|
+
def log_mcp_server_status(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Log MCP server installation and configuration status.
|
|
39
|
+
|
|
40
|
+
Checks:
|
|
41
|
+
- MCP server executable availability
|
|
42
|
+
- MCP server version if available
|
|
43
|
+
- MCP configuration in ~/.claude.json
|
|
44
|
+
- MCP-related errors or warnings
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Check if MCP server executable is available
|
|
48
|
+
mcp_executable = self._find_mcp_executable()
|
|
49
|
+
if mcp_executable:
|
|
50
|
+
self.logger.info(f"MCP Server: Installed at {mcp_executable}")
|
|
51
|
+
|
|
52
|
+
# Try to get version
|
|
53
|
+
version = self._get_mcp_version(mcp_executable)
|
|
54
|
+
if version:
|
|
55
|
+
self.logger.info(f"MCP Server: Version {version}")
|
|
56
|
+
else:
|
|
57
|
+
self.logger.info("MCP Server: Version unknown")
|
|
58
|
+
else:
|
|
59
|
+
self.logger.info("MCP Server: Not found in PATH")
|
|
60
|
+
|
|
61
|
+
# Check MCP configuration in ~/.claude.json
|
|
62
|
+
config_status = self._check_mcp_configuration()
|
|
63
|
+
if config_status["found"]:
|
|
64
|
+
self.logger.info("MCP Server: Configuration found in ~/.claude.json")
|
|
65
|
+
if config_status["servers_count"] > 0:
|
|
66
|
+
self.logger.info(f"MCP Server: {config_status['servers_count']} server(s) configured")
|
|
67
|
+
else:
|
|
68
|
+
self.logger.info("MCP Server: No servers configured")
|
|
69
|
+
else:
|
|
70
|
+
self.logger.info("MCP Server: No configuration found in ~/.claude.json")
|
|
71
|
+
|
|
72
|
+
# Check for claude-mpm MCP gateway status
|
|
73
|
+
gateway_status = self._check_mcp_gateway_status()
|
|
74
|
+
if gateway_status["configured"]:
|
|
75
|
+
self.logger.info("MCP Gateway: Claude MPM gateway configured")
|
|
76
|
+
else:
|
|
77
|
+
self.logger.info("MCP Gateway: Claude MPM gateway not configured")
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.logger.warning(f"MCP Server: Status check failed - {e}")
|
|
81
|
+
|
|
82
|
+
def log_monitor_setup_status(self, monitor_mode: bool = False, websocket_port: int = 8765) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Log monitor service initialization status.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
monitor_mode: Whether monitor mode is enabled
|
|
88
|
+
websocket_port: WebSocket port for monitoring
|
|
89
|
+
|
|
90
|
+
Checks:
|
|
91
|
+
- Monitor service initialization status
|
|
92
|
+
- Which monitors are enabled/disabled
|
|
93
|
+
- Monitor configuration details
|
|
94
|
+
- Monitor-related errors or warnings
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
if monitor_mode:
|
|
98
|
+
self.logger.info("Monitor: Mode enabled")
|
|
99
|
+
|
|
100
|
+
# Check SocketIO dependencies
|
|
101
|
+
socketio_status = self._check_socketio_dependencies()
|
|
102
|
+
if socketio_status["available"]:
|
|
103
|
+
self.logger.info("Monitor: Socket.IO dependencies available")
|
|
104
|
+
else:
|
|
105
|
+
self.logger.info(f"Monitor: Socket.IO dependencies missing - {socketio_status['error']}")
|
|
106
|
+
|
|
107
|
+
# Check if server is running
|
|
108
|
+
server_running = self._check_socketio_server_running(websocket_port)
|
|
109
|
+
if server_running:
|
|
110
|
+
self.logger.info(f"Monitor: Socket.IO server running on port {websocket_port}")
|
|
111
|
+
else:
|
|
112
|
+
self.logger.info(f"Monitor: Socket.IO server will start on port {websocket_port}")
|
|
113
|
+
|
|
114
|
+
# Check response logging configuration
|
|
115
|
+
logging_config = self._check_response_logging_config()
|
|
116
|
+
if logging_config["enabled"]:
|
|
117
|
+
self.logger.info(f"Monitor: Response logging enabled to {logging_config['directory']}")
|
|
118
|
+
else:
|
|
119
|
+
self.logger.info("Monitor: Response logging disabled")
|
|
120
|
+
|
|
121
|
+
else:
|
|
122
|
+
self.logger.info("Monitor: Mode disabled")
|
|
123
|
+
|
|
124
|
+
# Still check if there's an existing server running
|
|
125
|
+
server_running = self._check_socketio_server_running(websocket_port)
|
|
126
|
+
if server_running:
|
|
127
|
+
self.logger.info(f"Monitor: Background Socket.IO server detected on port {websocket_port}")
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.logger.warning(f"Monitor: Status check failed - {e}")
|
|
131
|
+
|
|
132
|
+
def _find_mcp_executable(self) -> Optional[str]:
|
|
133
|
+
"""Find MCP server executable in PATH."""
|
|
134
|
+
# Common MCP executable names
|
|
135
|
+
executables = ["claude-mpm-mcp", "mcp", "claude-mcp"]
|
|
136
|
+
|
|
137
|
+
for exe_name in executables:
|
|
138
|
+
exe_path = shutil.which(exe_name)
|
|
139
|
+
if exe_path:
|
|
140
|
+
return exe_path
|
|
141
|
+
|
|
142
|
+
# Check if it's installed as a Python package
|
|
143
|
+
try:
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
[sys.executable, "-m", "claude_mpm.scripts.mcp_server", "--version"],
|
|
146
|
+
capture_output=True,
|
|
147
|
+
text=True,
|
|
148
|
+
timeout=5
|
|
149
|
+
)
|
|
150
|
+
if result.returncode == 0:
|
|
151
|
+
return f"{sys.executable} -m claude_mpm.scripts.mcp_server"
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _get_mcp_version(self, executable: str) -> Optional[str]:
|
|
158
|
+
"""Get MCP server version."""
|
|
159
|
+
try:
|
|
160
|
+
# Try --version flag
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
executable.split() + ["--version"],
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
timeout=5
|
|
166
|
+
)
|
|
167
|
+
if result.returncode == 0:
|
|
168
|
+
# Extract version from output
|
|
169
|
+
output = result.stdout.strip()
|
|
170
|
+
if output:
|
|
171
|
+
return output
|
|
172
|
+
|
|
173
|
+
# Try version command
|
|
174
|
+
result = subprocess.run(
|
|
175
|
+
executable.split() + ["version"],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
text=True,
|
|
178
|
+
timeout=5
|
|
179
|
+
)
|
|
180
|
+
if result.returncode == 0:
|
|
181
|
+
output = result.stdout.strip()
|
|
182
|
+
if output:
|
|
183
|
+
return output
|
|
184
|
+
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def _check_mcp_configuration(self) -> Dict[str, Any]:
|
|
191
|
+
"""Check MCP configuration in ~/.claude.json."""
|
|
192
|
+
claude_json_path = Path.home() / ".claude.json"
|
|
193
|
+
|
|
194
|
+
result = {
|
|
195
|
+
"found": False,
|
|
196
|
+
"servers_count": 0,
|
|
197
|
+
"error": None
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if not claude_json_path.exists():
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
import json
|
|
205
|
+
with open(claude_json_path, 'r') as f:
|
|
206
|
+
config = json.load(f)
|
|
207
|
+
|
|
208
|
+
result["found"] = True
|
|
209
|
+
|
|
210
|
+
# Check for MCP servers configuration
|
|
211
|
+
mcp_config = config.get("mcpServers", {})
|
|
212
|
+
result["servers_count"] = len(mcp_config)
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
result["error"] = str(e)
|
|
216
|
+
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
def _check_mcp_gateway_status(self) -> Dict[str, Any]:
|
|
220
|
+
"""Check Claude MPM MCP gateway configuration status."""
|
|
221
|
+
result = {
|
|
222
|
+
"configured": False,
|
|
223
|
+
"error": None
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
# Check if MCP gateway startup verification is available
|
|
228
|
+
from ..services.mcp_gateway.core.startup_verification import is_mcp_gateway_configured
|
|
229
|
+
result["configured"] = is_mcp_gateway_configured()
|
|
230
|
+
except ImportError:
|
|
231
|
+
# MCP gateway not available
|
|
232
|
+
pass
|
|
233
|
+
except Exception as e:
|
|
234
|
+
result["error"] = str(e)
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
def _check_socketio_dependencies(self) -> Dict[str, Any]:
|
|
239
|
+
"""Check if Socket.IO dependencies are available."""
|
|
240
|
+
result = {
|
|
241
|
+
"available": False,
|
|
242
|
+
"error": None
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
import socketio
|
|
247
|
+
import aiohttp
|
|
248
|
+
import engineio
|
|
249
|
+
result["available"] = True
|
|
250
|
+
except ImportError as e:
|
|
251
|
+
result["error"] = f"Missing dependencies: {e}"
|
|
252
|
+
except Exception as e:
|
|
253
|
+
result["error"] = str(e)
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
def _check_socketio_server_running(self, port: int) -> bool:
|
|
258
|
+
"""Check if Socket.IO server is running on specified port."""
|
|
259
|
+
try:
|
|
260
|
+
import socket
|
|
261
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
262
|
+
s.settimeout(1)
|
|
263
|
+
result = s.connect_ex(('localhost', port))
|
|
264
|
+
return result == 0
|
|
265
|
+
except Exception:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
def _check_response_logging_config(self) -> Dict[str, Any]:
|
|
269
|
+
"""Check response logging configuration."""
|
|
270
|
+
result = {
|
|
271
|
+
"enabled": False,
|
|
272
|
+
"directory": None,
|
|
273
|
+
"error": None
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
from ..core.shared.config_loader import ConfigLoader
|
|
278
|
+
|
|
279
|
+
config_loader = ConfigLoader()
|
|
280
|
+
config = config_loader.load_main_config()
|
|
281
|
+
|
|
282
|
+
# Check response logging configuration
|
|
283
|
+
response_logging = config.get("response_logging", {})
|
|
284
|
+
result["enabled"] = response_logging.get("enabled", False)
|
|
285
|
+
|
|
286
|
+
if result["enabled"]:
|
|
287
|
+
log_dir = response_logging.get("session_directory", ".claude-mpm/responses")
|
|
288
|
+
if not Path(log_dir).is_absolute():
|
|
289
|
+
log_dir = Path.cwd() / log_dir
|
|
290
|
+
result["directory"] = str(log_dir)
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
result["error"] = str(e)
|
|
294
|
+
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def setup_startup_logging(project_root: Optional[Path] = None) -> Path:
|
|
299
|
+
"""
|
|
300
|
+
Set up logging to both console and file for startup.
|
|
301
|
+
|
|
302
|
+
WHY: Capture all startup logs (INFO, WARNING, ERROR, DEBUG) to timestamped
|
|
303
|
+
files for later analysis by the doctor command. This helps diagnose
|
|
304
|
+
startup issues that users may not notice in the console output.
|
|
305
|
+
|
|
306
|
+
DESIGN DECISIONS:
|
|
307
|
+
- Use ISO-like timestamp format for easy sorting and reading
|
|
308
|
+
- Store in .claude-mpm/logs/startup/ directory
|
|
309
|
+
- Keep all historical startup logs for pattern analysis
|
|
310
|
+
- Add file handler to root logger to capture ALL module logs
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
project_root: Root directory for the project (defaults to cwd)
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Path to the created log file
|
|
317
|
+
"""
|
|
318
|
+
if project_root is None:
|
|
319
|
+
project_root = Path.cwd()
|
|
320
|
+
|
|
321
|
+
# Create log directory
|
|
322
|
+
log_dir = project_root / ".claude-mpm" / "logs" / "startup"
|
|
323
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
|
|
325
|
+
# Generate timestamp for log file
|
|
326
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
327
|
+
log_file = log_dir / f"startup-{timestamp}.log"
|
|
328
|
+
|
|
329
|
+
# Create file handler with detailed formatting
|
|
330
|
+
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
331
|
+
file_handler.setLevel(logging.DEBUG) # Capture all levels to file
|
|
332
|
+
|
|
333
|
+
# Format with timestamp, logger name, level, and message
|
|
334
|
+
formatter = logging.Formatter(
|
|
335
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
336
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
337
|
+
)
|
|
338
|
+
file_handler.setFormatter(formatter)
|
|
339
|
+
|
|
340
|
+
# Add to claude_mpm logger to capture all our logs
|
|
341
|
+
# (Don't add to root logger to avoid duplicates from propagation)
|
|
342
|
+
claude_logger = logging.getLogger("claude_mpm")
|
|
343
|
+
claude_logger.addHandler(file_handler)
|
|
344
|
+
claude_logger.setLevel(logging.DEBUG) # Ensure all levels are captured
|
|
345
|
+
|
|
346
|
+
# Log startup header
|
|
347
|
+
logger = get_logger("startup")
|
|
348
|
+
logger.info("="*60)
|
|
349
|
+
logger.info(f"Claude MPM Startup - {datetime.now().isoformat()}")
|
|
350
|
+
logger.info(f"Log file: {log_file}")
|
|
351
|
+
logger.info("="*60)
|
|
352
|
+
|
|
353
|
+
# Log system information
|
|
354
|
+
logger.info(f"Python: {sys.version}")
|
|
355
|
+
logger.info(f"Platform: {sys.platform}")
|
|
356
|
+
logger.info(f"CWD: {Path.cwd()}")
|
|
357
|
+
logger.info(f"Project root: {project_root}")
|
|
358
|
+
|
|
359
|
+
return log_file
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def cleanup_old_startup_logs(project_root: Optional[Path] = None,
|
|
363
|
+
keep_days: int = 7,
|
|
364
|
+
keep_min_count: int = 10) -> int:
|
|
365
|
+
"""
|
|
366
|
+
Clean up old startup log files.
|
|
367
|
+
|
|
368
|
+
WHY: Prevent unbounded growth of startup logs while keeping enough
|
|
369
|
+
history for debugging patterns.
|
|
370
|
+
|
|
371
|
+
DESIGN DECISIONS:
|
|
372
|
+
- Keep logs from last N days
|
|
373
|
+
- Always keep minimum count regardless of age
|
|
374
|
+
- Return count of deleted files for reporting
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
project_root: Root directory for the project
|
|
378
|
+
keep_days: Number of days to keep logs
|
|
379
|
+
keep_min_count: Minimum number of logs to keep regardless of age
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Number of log files deleted
|
|
383
|
+
"""
|
|
384
|
+
if project_root is None:
|
|
385
|
+
project_root = Path.cwd()
|
|
386
|
+
|
|
387
|
+
log_dir = project_root / ".claude-mpm" / "logs" / "startup"
|
|
388
|
+
|
|
389
|
+
if not log_dir.exists():
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
# Get all startup log files
|
|
393
|
+
log_files = sorted(log_dir.glob("startup-*.log"),
|
|
394
|
+
key=lambda p: p.stat().st_mtime,
|
|
395
|
+
reverse=True) # Newest first
|
|
396
|
+
|
|
397
|
+
if len(log_files) <= keep_min_count:
|
|
398
|
+
return 0 # Keep minimum count
|
|
399
|
+
|
|
400
|
+
# Calculate cutoff time
|
|
401
|
+
cutoff_time = datetime.now().timestamp() - (keep_days * 24 * 60 * 60)
|
|
402
|
+
|
|
403
|
+
deleted_count = 0
|
|
404
|
+
for log_file in log_files[keep_min_count:]: # Skip minimum count
|
|
405
|
+
if log_file.stat().st_mtime < cutoff_time:
|
|
406
|
+
try:
|
|
407
|
+
log_file.unlink()
|
|
408
|
+
deleted_count += 1
|
|
409
|
+
except Exception:
|
|
410
|
+
pass # Ignore deletion errors
|
|
411
|
+
|
|
412
|
+
return deleted_count
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def get_latest_startup_log(project_root: Optional[Path] = None) -> Optional[Path]:
|
|
416
|
+
"""
|
|
417
|
+
Get the path to the most recent startup log file.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
project_root: Root directory for the project
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Path to latest log file or None if no logs exist
|
|
424
|
+
"""
|
|
425
|
+
if project_root is None:
|
|
426
|
+
project_root = Path.cwd()
|
|
427
|
+
|
|
428
|
+
log_dir = project_root / ".claude-mpm" / "logs" / "startup"
|
|
429
|
+
|
|
430
|
+
if not log_dir.exists():
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
log_files = sorted(log_dir.glob("startup-*.log"),
|
|
434
|
+
key=lambda p: p.stat().st_mtime,
|
|
435
|
+
reverse=True)
|
|
436
|
+
|
|
437
|
+
return log_files[0] if log_files else None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def log_startup_status(monitor_mode: bool = False, websocket_port: int = 8765) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Log comprehensive startup status for MCP server and monitor setup.
|
|
443
|
+
|
|
444
|
+
This function should be called during application startup to provide
|
|
445
|
+
detailed information about MCP and monitor setup status.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
monitor_mode: Whether monitor mode is enabled
|
|
449
|
+
websocket_port: WebSocket port for monitoring
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
status_logger = StartupStatusLogger("cli")
|
|
453
|
+
|
|
454
|
+
# Log MCP server status
|
|
455
|
+
status_logger.log_mcp_server_status()
|
|
456
|
+
|
|
457
|
+
# Log monitor setup status
|
|
458
|
+
status_logger.log_monitor_setup_status(monitor_mode, websocket_port)
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
# Don't let logging failures prevent startup
|
|
462
|
+
logger = get_logger("cli")
|
|
463
|
+
logger.debug(f"Startup status logging failed: {e}")
|
claude_mpm/constants.py
CHANGED
claude_mpm/core/claude_runner.py
CHANGED
|
@@ -210,6 +210,10 @@ class ClaudeRunner:
|
|
|
210
210
|
else:
|
|
211
211
|
self.system_instructions = self._load_system_instructions()
|
|
212
212
|
|
|
213
|
+
# Deploy output style early (before Claude Code launches)
|
|
214
|
+
# This ensures the "Claude MPM" output style is active on startup
|
|
215
|
+
self._deploy_output_style()
|
|
216
|
+
|
|
213
217
|
# Create session log file using configuration service
|
|
214
218
|
self.session_log_file = self.configuration_service.create_session_log_file(
|
|
215
219
|
self.project_logger, self.log_level, config_data
|
|
@@ -732,6 +736,80 @@ Use these agents to delegate specialized work via the Task tool.
|
|
|
732
736
|
# Fallback if service not available
|
|
733
737
|
return "v0.0.0"
|
|
734
738
|
|
|
739
|
+
def _deploy_output_style(self) -> None:
|
|
740
|
+
"""Deploy the Claude MPM output style before Claude Code launches.
|
|
741
|
+
|
|
742
|
+
This method ensures the output style is set to "Claude MPM" on startup
|
|
743
|
+
by deploying the style file and updating Claude Code settings.
|
|
744
|
+
Only works for Claude Code >= 1.0.83.
|
|
745
|
+
"""
|
|
746
|
+
try:
|
|
747
|
+
from claude_mpm.core.output_style_manager import OutputStyleManager
|
|
748
|
+
|
|
749
|
+
# Create OutputStyleManager instance
|
|
750
|
+
output_style_manager = OutputStyleManager()
|
|
751
|
+
|
|
752
|
+
# Check if Claude Code supports output styles
|
|
753
|
+
if not output_style_manager.supports_output_styles():
|
|
754
|
+
self.logger.debug(
|
|
755
|
+
f"Claude Code version {output_style_manager.claude_version or 'unknown'} "
|
|
756
|
+
"does not support output styles (requires >= 1.0.83)"
|
|
757
|
+
)
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
# Check if output style is already deployed and active
|
|
761
|
+
settings_file = Path.home() / ".claude" / "settings.json"
|
|
762
|
+
if settings_file.exists():
|
|
763
|
+
try:
|
|
764
|
+
import json
|
|
765
|
+
settings = json.loads(settings_file.read_text())
|
|
766
|
+
if settings.get("activeOutputStyle") == "claude-mpm":
|
|
767
|
+
# Already active, check if file exists
|
|
768
|
+
output_style_file = Path.home() / ".claude" / "output-styles" / "claude-mpm.md"
|
|
769
|
+
if output_style_file.exists():
|
|
770
|
+
self.logger.debug("Output style 'Claude MPM' already deployed and active")
|
|
771
|
+
return
|
|
772
|
+
except Exception:
|
|
773
|
+
pass # Continue with deployment if we can't read settings
|
|
774
|
+
|
|
775
|
+
# Read the OUTPUT_STYLE.md content if it exists
|
|
776
|
+
output_style_path = Path(__file__).parent.parent / "agents" / "OUTPUT_STYLE.md"
|
|
777
|
+
|
|
778
|
+
if output_style_path.exists():
|
|
779
|
+
# Use existing OUTPUT_STYLE.md content
|
|
780
|
+
output_style_content = output_style_path.read_text()
|
|
781
|
+
self.logger.debug("Using existing OUTPUT_STYLE.md content")
|
|
782
|
+
else:
|
|
783
|
+
# Extract output style content from framework instructions
|
|
784
|
+
output_style_content = output_style_manager.extract_output_style_content()
|
|
785
|
+
self.logger.debug("Extracted output style from framework instructions")
|
|
786
|
+
|
|
787
|
+
# Deploy the output style
|
|
788
|
+
deployed = output_style_manager.deploy_output_style(output_style_content)
|
|
789
|
+
|
|
790
|
+
if deployed:
|
|
791
|
+
self.logger.info("✅ Output style 'Claude MPM' deployed and activated on startup")
|
|
792
|
+
if self.project_logger:
|
|
793
|
+
self.project_logger.log_system(
|
|
794
|
+
"Output style 'Claude MPM' deployed and activated on startup",
|
|
795
|
+
level="INFO",
|
|
796
|
+
component="output_style"
|
|
797
|
+
)
|
|
798
|
+
else:
|
|
799
|
+
self.logger.warning("Failed to deploy output style")
|
|
800
|
+
|
|
801
|
+
except ImportError as e:
|
|
802
|
+
self.logger.warning(f"Could not import OutputStyleManager: {e}")
|
|
803
|
+
except Exception as e:
|
|
804
|
+
# Don't fail startup if output style deployment fails
|
|
805
|
+
self.logger.warning(f"Error deploying output style: {e}")
|
|
806
|
+
if self.project_logger:
|
|
807
|
+
self.project_logger.log_system(
|
|
808
|
+
f"Output style deployment error: {e}",
|
|
809
|
+
level="WARNING",
|
|
810
|
+
component="output_style"
|
|
811
|
+
)
|
|
812
|
+
|
|
735
813
|
def _launch_subprocess_interactive(self, cmd: list, env: dict):
|
|
736
814
|
"""Launch Claude as a subprocess with PTY for interactive mode.
|
|
737
815
|
|
|
@@ -77,11 +77,11 @@ class FrameworkLoader:
|
|
|
77
77
|
output_style_content = self.output_style_manager.extract_output_style_content(framework_loader=self)
|
|
78
78
|
output_style_path = self.output_style_manager.save_output_style(output_style_content)
|
|
79
79
|
|
|
80
|
-
# Deploy to Claude
|
|
80
|
+
# Deploy to Claude Code if supported
|
|
81
81
|
deployed = self.output_style_manager.deploy_output_style(output_style_content)
|
|
82
82
|
|
|
83
83
|
if deployed:
|
|
84
|
-
self.logger.info("✅ Output style deployed to Claude
|
|
84
|
+
self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
|
|
85
85
|
else:
|
|
86
86
|
self.logger.info("📝 Output style will be injected into instructions for older Claude versions")
|
|
87
87
|
|
|
@@ -97,11 +97,11 @@ class FrameworkLoader:
|
|
|
97
97
|
# Claude version detection
|
|
98
98
|
claude_version = self.output_style_manager.claude_version
|
|
99
99
|
if claude_version:
|
|
100
|
-
self.logger.info(f"Claude
|
|
100
|
+
self.logger.info(f"Claude Code version detected: {claude_version}")
|
|
101
101
|
|
|
102
102
|
# Check if version supports output styles
|
|
103
103
|
if self.output_style_manager.supports_output_styles():
|
|
104
|
-
self.logger.info("✅ Claude
|
|
104
|
+
self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
|
|
105
105
|
|
|
106
106
|
# Check deployment status
|
|
107
107
|
output_style_path = self.output_style_manager.output_style_path
|
|
@@ -111,10 +111,10 @@ class FrameworkLoader:
|
|
|
111
111
|
self.logger.info(f"📝 Output style will be created at: {output_style_path}")
|
|
112
112
|
|
|
113
113
|
else:
|
|
114
|
-
self.logger.info(f"⚠️ Claude
|
|
114
|
+
self.logger.info(f"⚠️ Claude Code {claude_version} does not support output styles (< 1.0.83)")
|
|
115
115
|
self.logger.info("📝 Output style content will be injected into framework instructions")
|
|
116
116
|
else:
|
|
117
|
-
self.logger.info("⚠️ Claude
|
|
117
|
+
self.logger.info("⚠️ Claude Code not detected or version unknown")
|
|
118
118
|
self.logger.info("📝 Output style content will be injected into framework instructions as fallback")
|
|
119
119
|
|
|
120
120
|
def _detect_framework_path(self) -> Optional[Path]:
|
|
@@ -311,17 +311,41 @@ class FrameworkLoader:
|
|
|
311
311
|
|
|
312
312
|
def _load_instructions_file(self, content: Dict[str, Any]) -> None:
|
|
313
313
|
"""
|
|
314
|
-
Load INSTRUCTIONS.md
|
|
314
|
+
Load custom INSTRUCTIONS.md from .claude-mpm directories.
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
Precedence (highest to lowest):
|
|
317
|
+
1. Project-specific: ./.claude-mpm/INSTRUCTIONS.md
|
|
318
|
+
2. User-specific: ~/.claude-mpm/INSTRUCTIONS.md
|
|
319
|
+
|
|
320
|
+
NOTE: We do NOT load CLAUDE.md files since Claude Code already picks them up automatically.
|
|
317
321
|
This prevents duplication of instructions.
|
|
318
322
|
|
|
319
323
|
Args:
|
|
320
324
|
content: Dictionary to update with loaded instructions
|
|
321
325
|
"""
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
# Check for project-specific INSTRUCTIONS.md first
|
|
327
|
+
project_instructions_path = Path.cwd() / ".claude-mpm" / "INSTRUCTIONS.md"
|
|
328
|
+
if project_instructions_path.exists():
|
|
329
|
+
loaded_content = self._try_load_file(
|
|
330
|
+
project_instructions_path, "project-specific INSTRUCTIONS.md"
|
|
331
|
+
)
|
|
332
|
+
if loaded_content:
|
|
333
|
+
content["custom_instructions"] = loaded_content
|
|
334
|
+
content["custom_instructions_level"] = "project"
|
|
335
|
+
self.logger.info("Using project-specific PM instructions from .claude-mpm/INSTRUCTIONS.md")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Check for user-specific INSTRUCTIONS.md
|
|
339
|
+
user_instructions_path = Path.home() / ".claude-mpm" / "INSTRUCTIONS.md"
|
|
340
|
+
if user_instructions_path.exists():
|
|
341
|
+
loaded_content = self._try_load_file(
|
|
342
|
+
user_instructions_path, "user-specific INSTRUCTIONS.md"
|
|
343
|
+
)
|
|
344
|
+
if loaded_content:
|
|
345
|
+
content["custom_instructions"] = loaded_content
|
|
346
|
+
content["custom_instructions_level"] = "user"
|
|
347
|
+
self.logger.info("Using user-specific PM instructions from ~/.claude-mpm/INSTRUCTIONS.md")
|
|
348
|
+
return
|
|
325
349
|
|
|
326
350
|
def _load_workflow_instructions(self, content: Dict[str, Any]) -> None:
|
|
327
351
|
"""
|
|
@@ -1101,6 +1125,16 @@ class FrameworkLoader:
|
|
|
1101
1125
|
|
|
1102
1126
|
# Note: We don't add working directory CLAUDE.md here since Claude Code
|
|
1103
1127
|
# already picks it up automatically. This prevents duplication.
|
|
1128
|
+
|
|
1129
|
+
# Add custom INSTRUCTIONS.md if present (overrides or extends framework instructions)
|
|
1130
|
+
if self.framework_content.get("custom_instructions"):
|
|
1131
|
+
level = self.framework_content.get("custom_instructions_level", "unknown")
|
|
1132
|
+
instructions += f"\n\n## Custom PM Instructions ({level} level)\n\n"
|
|
1133
|
+
instructions += "**The following custom instructions override or extend the framework defaults:**\n\n"
|
|
1134
|
+
instructions += self._strip_metadata_comments(
|
|
1135
|
+
self.framework_content["custom_instructions"]
|
|
1136
|
+
)
|
|
1137
|
+
instructions += "\n"
|
|
1104
1138
|
|
|
1105
1139
|
# Add WORKFLOW.md after instructions
|
|
1106
1140
|
if self.framework_content.get("workflow_instructions"):
|