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.
Files changed (64) hide show
  1. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +48 -3
  2. claude_mpm/agents/BASE_PM.md +20 -15
  3. claude_mpm/agents/INSTRUCTIONS.md +12 -2
  4. claude_mpm/agents/templates/documentation.json +16 -3
  5. claude_mpm/agents/templates/engineer.json +19 -5
  6. claude_mpm/agents/templates/ops.json +19 -5
  7. claude_mpm/agents/templates/qa.json +16 -3
  8. claude_mpm/agents/templates/refactoring_engineer.json +25 -7
  9. claude_mpm/agents/templates/research.json +19 -5
  10. claude_mpm/cli/__init__.py +2 -0
  11. claude_mpm/cli/commands/__init__.py +2 -0
  12. claude_mpm/cli/commands/agent_manager.py +10 -6
  13. claude_mpm/cli/commands/agents.py +2 -1
  14. claude_mpm/cli/commands/cleanup.py +1 -1
  15. claude_mpm/cli/commands/doctor.py +209 -0
  16. claude_mpm/cli/commands/mcp.py +3 -3
  17. claude_mpm/cli/commands/mcp_install_commands.py +12 -30
  18. claude_mpm/cli/commands/mcp_server_commands.py +9 -9
  19. claude_mpm/cli/commands/run.py +31 -2
  20. claude_mpm/cli/commands/run_config_checker.py +1 -1
  21. claude_mpm/cli/parsers/agent_manager_parser.py +3 -3
  22. claude_mpm/cli/parsers/base_parser.py +5 -1
  23. claude_mpm/cli/parsers/mcp_parser.py +1 -1
  24. claude_mpm/cli/parsers/run_parser.py +1 -1
  25. claude_mpm/cli/startup_logging.py +463 -0
  26. claude_mpm/constants.py +1 -0
  27. claude_mpm/core/claude_runner.py +78 -0
  28. claude_mpm/core/framework_loader.py +45 -11
  29. claude_mpm/core/interactive_session.py +82 -3
  30. claude_mpm/core/output_style_manager.py +6 -6
  31. claude_mpm/core/unified_paths.py +128 -0
  32. claude_mpm/scripts/mcp_server.py +2 -2
  33. claude_mpm/services/agents/deployment/agent_validator.py +1 -0
  34. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +69 -1
  35. claude_mpm/services/diagnostics/__init__.py +18 -0
  36. claude_mpm/services/diagnostics/checks/__init__.py +30 -0
  37. claude_mpm/services/diagnostics/checks/agent_check.py +319 -0
  38. claude_mpm/services/diagnostics/checks/base_check.py +64 -0
  39. claude_mpm/services/diagnostics/checks/claude_desktop_check.py +283 -0
  40. claude_mpm/services/diagnostics/checks/common_issues_check.py +354 -0
  41. claude_mpm/services/diagnostics/checks/configuration_check.py +300 -0
  42. claude_mpm/services/diagnostics/checks/filesystem_check.py +233 -0
  43. claude_mpm/services/diagnostics/checks/installation_check.py +255 -0
  44. claude_mpm/services/diagnostics/checks/mcp_check.py +315 -0
  45. claude_mpm/services/diagnostics/checks/monitor_check.py +282 -0
  46. claude_mpm/services/diagnostics/checks/startup_log_check.py +322 -0
  47. claude_mpm/services/diagnostics/diagnostic_runner.py +247 -0
  48. claude_mpm/services/diagnostics/doctor_reporter.py +283 -0
  49. claude_mpm/services/diagnostics/models.py +120 -0
  50. claude_mpm/services/mcp_gateway/core/interfaces.py +1 -1
  51. claude_mpm/services/mcp_gateway/main.py +1 -1
  52. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +3 -3
  53. claude_mpm/services/mcp_gateway/server/stdio_handler.py +1 -1
  54. claude_mpm/services/mcp_gateway/server/stdio_server.py +3 -3
  55. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +2 -2
  56. claude_mpm/services/socketio/handlers/registry.py +39 -7
  57. claude_mpm/services/socketio/server/core.py +72 -22
  58. claude_mpm/validation/frontmatter_validator.py +1 -1
  59. {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/METADATA +4 -1
  60. {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/RECORD +64 -47
  61. {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/WHEEL +0 -0
  62. {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/entry_points.txt +0 -0
  63. {claude_mpm-4.0.29.dist-info → claude_mpm-4.0.30.dist-info}/licenses/LICENSE +0 -0
  64. {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 Desktop"
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 Desktop to resume the last conversation",
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
@@ -40,6 +40,7 @@ class CLICommands(str, Enum):
40
40
  AGGREGATE = "aggregate"
41
41
  CLEANUP = "cleanup-memory"
42
42
  MCP = "mcp"
43
+ DOCTOR = "doctor"
43
44
 
44
45
  def with_prefix(self, prefix: CLIPrefix = CLIPrefix.MPM) -> str:
45
46
  """Get command with prefix."""
@@ -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 Desktop if supported
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 Desktop >= 1.0.83")
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 Desktop version detected: {claude_version}")
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 Desktop supports output styles (>= 1.0.83)")
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 Desktop {claude_version} does not support output styles (< 1.0.83)")
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 Desktop not detected or version unknown")
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 or legacy CLAUDE.md from working directory.
314
+ Load custom INSTRUCTIONS.md from .claude-mpm directories.
315
315
 
316
- NOTE: We no longer load CLAUDE.md since Claude Code already picks it up automatically.
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
- # Disabled - Claude Code already reads CLAUDE.md automatically
323
- # We don't need to duplicate it in the PM instructions
324
- pass
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"):