claude-mpm 3.5.6__py3-none-any.whl → 3.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  3. claude_mpm/agents/BASE_PM.md +273 -0
  4. claude_mpm/agents/INSTRUCTIONS.md +114 -103
  5. claude_mpm/agents/agent_loader.py +36 -1
  6. claude_mpm/agents/async_agent_loader.py +421 -0
  7. claude_mpm/agents/templates/code_analyzer.json +81 -0
  8. claude_mpm/agents/templates/data_engineer.json +18 -3
  9. claude_mpm/agents/templates/documentation.json +18 -3
  10. claude_mpm/agents/templates/engineer.json +19 -4
  11. claude_mpm/agents/templates/ops.json +18 -3
  12. claude_mpm/agents/templates/qa.json +20 -4
  13. claude_mpm/agents/templates/research.json +20 -4
  14. claude_mpm/agents/templates/security.json +18 -3
  15. claude_mpm/agents/templates/version_control.json +16 -3
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/__init__.py +5 -1
  18. claude_mpm/cli/commands/agents.py +212 -3
  19. claude_mpm/cli/commands/aggregate.py +462 -0
  20. claude_mpm/cli/commands/config.py +277 -0
  21. claude_mpm/cli/commands/run.py +224 -36
  22. claude_mpm/cli/parser.py +176 -1
  23. claude_mpm/constants.py +19 -0
  24. claude_mpm/core/claude_runner.py +320 -44
  25. claude_mpm/core/config.py +161 -4
  26. claude_mpm/core/framework_loader.py +81 -0
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  28. claude_mpm/init.py +40 -5
  29. claude_mpm/models/agent_session.py +511 -0
  30. claude_mpm/scripts/__init__.py +15 -0
  31. claude_mpm/scripts/start_activity_logging.py +86 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
  33. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  34. claude_mpm/services/event_aggregator.py +547 -0
  35. claude_mpm/utils/agent_dependency_loader.py +655 -0
  36. claude_mpm/utils/console.py +11 -0
  37. claude_mpm/utils/dependency_cache.py +376 -0
  38. claude_mpm/utils/dependency_strategies.py +343 -0
  39. claude_mpm/utils/environment_context.py +310 -0
  40. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +45 -31
  42. claude_mpm/agents/templates/pm.json +0 -122
  43. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,277 @@
1
+ """
2
+ Configuration management commands for claude-mpm CLI.
3
+
4
+ WHY: Users need a simple way to validate and manage their configuration from
5
+ the command line. This module provides commands for configuration validation,
6
+ viewing, and troubleshooting.
7
+
8
+ DESIGN DECISIONS:
9
+ - Integrate with existing CLI structure
10
+ - Provide clear, actionable output
11
+ - Support both validation and viewing operations
12
+ - Use consistent error codes for CI/CD integration
13
+ """
14
+
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Optional
18
+ import json
19
+ import yaml
20
+
21
+ from ...core.config import Config
22
+ from ...core.logger import get_logger
23
+ from ...utils.console import console
24
+ from rich.table import Table
25
+ from rich.syntax import Syntax
26
+ from rich.panel import Panel
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ def manage_config(args) -> int:
32
+ """Main entry point for configuration management commands.
33
+
34
+ WHY: This dispatcher handles different configuration subcommands,
35
+ routing to the appropriate handler based on user input.
36
+
37
+ Args:
38
+ args: Parsed command line arguments
39
+
40
+ Returns:
41
+ Exit code (0 for success, non-zero for failure)
42
+ """
43
+ if args.config_command == 'validate':
44
+ return validate_config(args)
45
+ elif args.config_command == 'view':
46
+ return view_config(args)
47
+ elif args.config_command == 'status':
48
+ return show_config_status(args)
49
+ else:
50
+ console.print(f"[red]Unknown config command: {args.config_command}[/red]")
51
+ return 1
52
+
53
+
54
+ def validate_config(args) -> int:
55
+ """Validate configuration file.
56
+
57
+ WHY: Users need immediate feedback on configuration validity with
58
+ clear guidance on how to fix any issues found.
59
+
60
+ Args:
61
+ args: Command line arguments including config file path
62
+
63
+ Returns:
64
+ Exit code (0=valid, 1=errors, 2=warnings in strict mode)
65
+ """
66
+ config_file = args.config_file or Path(".claude-mpm/configuration.yaml")
67
+
68
+ console.print(f"\n[bold blue]Validating configuration: {config_file}[/bold blue]\n")
69
+
70
+ # Check if file exists
71
+ if not config_file.exists():
72
+ console.print(f"[red]✗ Configuration file not found: {config_file}[/red]")
73
+ console.print(f"[yellow]→ Create with: mkdir -p {config_file.parent} && touch {config_file}[/yellow]")
74
+ return 1
75
+
76
+ try:
77
+ # Try to load configuration
78
+ config = Config(config_file=config_file)
79
+
80
+ # Validate configuration
81
+ is_valid, errors, warnings = config.validate_configuration()
82
+
83
+ # Display results
84
+ if errors:
85
+ console.print("[bold red]ERRORS:[/bold red]")
86
+ for error in errors:
87
+ console.print(f" [red]✗ {error}[/red]")
88
+
89
+ if warnings:
90
+ console.print("\n[bold yellow]WARNINGS:[/bold yellow]")
91
+ for warning in warnings:
92
+ console.print(f" [yellow]⚠ {warning}[/yellow]")
93
+
94
+ # Show summary
95
+ if is_valid and not warnings:
96
+ console.print("\n[green]✓ Configuration is valid[/green]")
97
+ return 0
98
+ elif is_valid and warnings:
99
+ console.print(f"\n[green]✓ Configuration is valid with {len(warnings)} warning(s)[/green]")
100
+ return 2 if args.strict else 0
101
+ else:
102
+ console.print(f"\n[red]✗ Configuration validation failed with {len(errors)} error(s)[/red]")
103
+ console.print("\n[yellow]Run 'python scripts/validate_configuration.py' for detailed analysis[/yellow]")
104
+ return 1
105
+
106
+ except Exception as e:
107
+ console.print(f"[red]Failed to validate configuration: {e}[/red]")
108
+ logger.exception("Configuration validation error")
109
+ return 1
110
+
111
+
112
+ def view_config(args) -> int:
113
+ """View current configuration.
114
+
115
+ WHY: Users need to see their effective configuration including
116
+ defaults and environment variable overrides.
117
+
118
+ Args:
119
+ args: Command line arguments
120
+
121
+ Returns:
122
+ Exit code (0 for success)
123
+ """
124
+ try:
125
+ # Load configuration
126
+ config_file = args.config_file
127
+ config = Config(config_file=config_file)
128
+
129
+ # Get configuration as dictionary
130
+ config_dict = config.to_dict()
131
+
132
+ # Filter by section if specified
133
+ if args.section:
134
+ if args.section in config_dict:
135
+ config_dict = {args.section: config_dict[args.section]}
136
+ else:
137
+ console.print(f"[red]Section '{args.section}' not found in configuration[/red]")
138
+ return 1
139
+
140
+ # Format output
141
+ if args.format == 'json':
142
+ output = json.dumps(config_dict, indent=2)
143
+ syntax = Syntax(output, "json", theme="monokai", line_numbers=False)
144
+ console.print(syntax)
145
+ elif args.format == 'yaml':
146
+ output = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
147
+ syntax = Syntax(output, "yaml", theme="monokai", line_numbers=False)
148
+ console.print(syntax)
149
+ else: # table format
150
+ display_config_table(config_dict)
151
+
152
+ return 0
153
+
154
+ except Exception as e:
155
+ console.print(f"[red]Failed to view configuration: {e}[/red]")
156
+ logger.exception("Configuration view error")
157
+ return 1
158
+
159
+
160
+ def show_config_status(args) -> int:
161
+ """Show configuration status and health.
162
+
163
+ WHY: Users need a quick way to check if their configuration is
164
+ working correctly, especially for response logging.
165
+
166
+ Args:
167
+ args: Command line arguments
168
+
169
+ Returns:
170
+ Exit code (0 for success)
171
+ """
172
+ try:
173
+ # Load configuration
174
+ config = Config(config_file=args.config_file)
175
+
176
+ # Get status
177
+ status = config.get_configuration_status()
178
+
179
+ # Create status panel
180
+ panel_content = []
181
+
182
+ # Basic info
183
+ panel_content.append(f"[bold]Configuration Status[/bold]")
184
+ panel_content.append(f"Valid: {'✓' if status['valid'] else '✗'}")
185
+ panel_content.append(f"Loaded from: {status.get('loaded_from', 'defaults')}")
186
+ panel_content.append(f"Total keys: {status['key_count']}")
187
+
188
+ # Feature status
189
+ panel_content.append("\n[bold]Features:[/bold]")
190
+ panel_content.append(
191
+ f"Response Logging: {'✓ Enabled' if status['response_logging_enabled'] else '✗ Disabled'}"
192
+ )
193
+ panel_content.append(
194
+ f"Memory System: {'✓ Enabled' if status['memory_enabled'] else '✗ Disabled'}"
195
+ )
196
+
197
+ # Errors and warnings
198
+ if status['errors']:
199
+ panel_content.append(f"\n[red]Errors: {len(status['errors'])}[/red]")
200
+ if status['warnings']:
201
+ panel_content.append(f"\n[yellow]Warnings: {len(status['warnings'])}[/yellow]")
202
+
203
+ # Display panel
204
+ panel = Panel(
205
+ "\n".join(panel_content),
206
+ title="Configuration Status",
207
+ border_style="green" if status['valid'] else "red"
208
+ )
209
+ console.print(panel)
210
+
211
+ # Show detailed errors/warnings if verbose
212
+ if args.verbose:
213
+ if status['errors']:
214
+ console.print("\n[bold red]Errors:[/bold red]")
215
+ for error in status['errors']:
216
+ console.print(f" [red]• {error}[/red]")
217
+
218
+ if status['warnings']:
219
+ console.print("\n[bold yellow]Warnings:[/bold yellow]")
220
+ for warning in status['warnings']:
221
+ console.print(f" [yellow]• {warning}[/yellow]")
222
+
223
+ # Check response logging specifically
224
+ if args.check_response_logging:
225
+ console.print("\n[bold]Response Logging Configuration:[/bold]")
226
+ rl_config = config.get('response_logging', {})
227
+
228
+ table = Table(show_header=True)
229
+ table.add_column("Setting", style="cyan")
230
+ table.add_column("Value", style="white")
231
+
232
+ table.add_row("Enabled", str(rl_config.get('enabled', False)))
233
+ table.add_row("Format", rl_config.get('format', 'json'))
234
+ table.add_row("Use Async", str(rl_config.get('use_async', True)))
235
+ table.add_row("Session Directory", rl_config.get('session_directory', '.claude-mpm/responses'))
236
+ table.add_row("Compression", str(rl_config.get('enable_compression', False)))
237
+
238
+ console.print(table)
239
+
240
+ return 0 if status['valid'] else 1
241
+
242
+ except Exception as e:
243
+ console.print(f"[red]Failed to get configuration status: {e}[/red]")
244
+ logger.exception("Configuration status error")
245
+ return 1
246
+
247
+
248
+ def display_config_table(config_dict: dict, prefix: str = "") -> None:
249
+ """Display configuration as a formatted table.
250
+
251
+ Args:
252
+ config_dict: Configuration dictionary
253
+ prefix: Key prefix for nested values
254
+ """
255
+ table = Table(show_header=True, title="Configuration")
256
+ table.add_column("Key", style="cyan", no_wrap=True)
257
+ table.add_column("Value", style="white")
258
+ table.add_column("Type", style="dim")
259
+
260
+ def add_items(d: dict, prefix: str = ""):
261
+ for key, value in d.items():
262
+ full_key = f"{prefix}.{key}" if prefix else key
263
+
264
+ if isinstance(value, dict) and value:
265
+ # Add nested items
266
+ add_items(value, full_key)
267
+ else:
268
+ # Add leaf value
269
+ value_str = str(value)
270
+ if len(value_str) > 50:
271
+ value_str = value_str[:47] + "..."
272
+
273
+ type_str = type(value).__name__
274
+ table.add_row(full_key, value_str, type_str)
275
+
276
+ add_items(config_dict)
277
+ console.print(table)
@@ -10,10 +10,12 @@ import subprocess
10
10
  import sys
11
11
  import time
12
12
  import webbrowser
13
+ import logging
13
14
  from pathlib import Path
14
15
  from datetime import datetime
15
16
 
16
17
  from ...core.logger import get_logger
18
+ from ...core.config import Config
17
19
  from ...constants import LogLevel
18
20
  from ..utils import get_user_input, list_agent_versions_at_startup
19
21
  from ...utils.dependency_manager import ensure_socketio_dependencies
@@ -52,6 +54,11 @@ def filter_claude_mpm_args(claude_args):
52
54
  '--no-native-agents',
53
55
  '--launch-method',
54
56
  '--resume',
57
+ # Dependency checking flags (MPM-specific)
58
+ '--no-check-dependencies',
59
+ '--force-check-dependencies',
60
+ '--no-prompt',
61
+ '--force-prompt',
55
62
  # Input/output flags (these are MPM-specific, not Claude CLI flags)
56
63
  '--input',
57
64
  '--non-interactive',
@@ -172,6 +179,9 @@ def run_session(args):
172
179
  if args.logging != LogLevel.OFF.value:
173
180
  logger.info("Starting Claude MPM session")
174
181
 
182
+ # Perform startup configuration check
183
+ _check_configuration_health(logger)
184
+
175
185
  try:
176
186
  from ...core.claude_runner import ClaudeRunner, create_simple_context
177
187
  from ...core.session_manager import SessionManager
@@ -219,6 +229,98 @@ def run_session(args):
219
229
  else:
220
230
  # List deployed agent versions at startup
221
231
  list_agent_versions_at_startup()
232
+
233
+ # Smart dependency checking - only when needed
234
+ if getattr(args, 'check_dependencies', True): # Default to checking
235
+ try:
236
+ from ...utils.agent_dependency_loader import AgentDependencyLoader
237
+ from ...utils.dependency_cache import SmartDependencyChecker
238
+ from ...utils.environment_context import should_prompt_for_dependencies
239
+
240
+ # Initialize smart checker
241
+ smart_checker = SmartDependencyChecker()
242
+ loader = AgentDependencyLoader(auto_install=False)
243
+
244
+ # Check if agents have changed
245
+ has_changed, deployment_hash = loader.has_agents_changed()
246
+
247
+ # Determine if we should check dependencies
248
+ should_check, check_reason = smart_checker.should_check_dependencies(
249
+ force_check=getattr(args, 'force_check_dependencies', False),
250
+ deployment_hash=deployment_hash
251
+ )
252
+
253
+ if should_check:
254
+ # Check if we're in an environment where prompting makes sense
255
+ can_prompt, prompt_reason = should_prompt_for_dependencies(
256
+ force_prompt=getattr(args, 'force_prompt', False),
257
+ force_skip=getattr(args, 'no_prompt', False)
258
+ )
259
+
260
+ logger.debug(f"Dependency check needed: {check_reason}")
261
+ logger.debug(f"Interactive prompting: {can_prompt} ({prompt_reason})")
262
+
263
+ # Get or check dependencies
264
+ results, was_cached = smart_checker.get_or_check_dependencies(
265
+ loader=loader,
266
+ force_check=getattr(args, 'force_check_dependencies', False)
267
+ )
268
+
269
+ # Show summary if there are missing dependencies
270
+ if results['summary']['missing_python']:
271
+ missing_count = len(results['summary']['missing_python'])
272
+ print(f"⚠️ {missing_count} agent dependencies missing")
273
+
274
+ if can_prompt and missing_count > 0:
275
+ # Interactive prompt for installation
276
+ print(f"\n📦 Missing dependencies detected:")
277
+ for dep in results['summary']['missing_python'][:5]:
278
+ print(f" - {dep}")
279
+ if missing_count > 5:
280
+ print(f" ... and {missing_count - 5} more")
281
+
282
+ print("\nWould you like to install them now?")
283
+ print(" [y] Yes, install missing dependencies")
284
+ print(" [n] No, continue without installing")
285
+ print(" [q] Quit")
286
+
287
+ try:
288
+ response = input("\nChoice [y/n/q]: ").strip().lower()
289
+ if response == 'y':
290
+ print("\n🔧 Installing missing dependencies...")
291
+ loader.auto_install = True
292
+ success, error = loader.install_missing_dependencies(
293
+ results['summary']['missing_python']
294
+ )
295
+ if success:
296
+ print("✅ Dependencies installed successfully")
297
+ # Invalidate cache after installation
298
+ smart_checker.cache.invalidate(deployment_hash)
299
+ else:
300
+ print(f"❌ Installation failed: {error}")
301
+ elif response == 'q':
302
+ print("👋 Exiting...")
303
+ return
304
+ else:
305
+ print("⏩ Continuing without installing dependencies")
306
+ except (EOFError, KeyboardInterrupt):
307
+ print("\n⏩ Continuing without installing dependencies")
308
+ else:
309
+ # Non-interactive environment or prompting disabled
310
+ print(" Run 'pip install \"claude-mpm[agents]\"' to install all agent dependencies")
311
+ if not can_prompt:
312
+ logger.debug(f"Not prompting for installation: {prompt_reason}")
313
+ elif was_cached:
314
+ logger.debug("Dependencies satisfied (cached result)")
315
+ else:
316
+ logger.debug("All dependencies satisfied")
317
+ else:
318
+ logger.debug(f"Skipping dependency check: {check_reason}")
319
+
320
+ except Exception as e:
321
+ if args.logging != LogLevel.OFF.value:
322
+ logger.debug(f"Could not check agent dependencies: {e}")
323
+ # Continue anyway - don't block execution
222
324
 
223
325
  # Create simple runner
224
326
  enable_tickets = not args.no_tickets
@@ -421,13 +523,17 @@ def launch_socketio_monitor(port, logger):
421
523
  print(f"✅ Socket.IO server started successfully")
422
524
  print(f"📊 Dashboard: {dashboard_url}")
423
525
 
424
- # Final verification that server is responsive
526
+ # Final verification that server is responsive using event-based checking
425
527
  final_check_passed = False
426
- for i in range(3):
528
+ check_start = time.time()
529
+ max_wait = 3 # Maximum 3 seconds
530
+
531
+ while time.time() - check_start < max_wait:
427
532
  if _check_socketio_server_running(socketio_port, logger):
428
533
  final_check_passed = True
429
534
  break
430
- time.sleep(1)
535
+ # Use a very short sleep just to yield CPU
536
+ time.sleep(0.1) # 100ms polling interval
431
537
 
432
538
  if not final_check_passed:
433
539
  logger.warning("Server started but final connectivity check failed")
@@ -513,20 +619,26 @@ def _check_socketio_server_running(port, logger):
513
619
  else:
514
620
  logger.debug(f"⚠️ HTTP response code {response.getcode()} from port {port} (attempt {retry + 1})")
515
621
  if retry < max_retries - 1:
516
- time.sleep(0.5) # Brief pause before retry
622
+ # Use exponential backoff with shorter initial delay
623
+ backoff = min(0.1 * (2 ** retry), 1.0) # 0.1s, 0.2s, 0.4s...
624
+ time.sleep(backoff)
517
625
 
518
626
  except urllib.error.HTTPError as e:
519
627
  logger.debug(f"⚠️ HTTP error {e.code} from server on port {port} (attempt {retry + 1})")
520
628
  if retry < max_retries - 1 and e.code in [404, 503]: # Server starting but not ready
521
629
  logger.debug("Server appears to be starting, retrying...")
522
- time.sleep(0.5)
630
+ # Use exponential backoff for retries
631
+ backoff = min(0.1 * (2 ** retry), 1.0)
632
+ time.sleep(backoff)
523
633
  continue
524
634
  return False
525
635
  except urllib.error.URLError as e:
526
636
  logger.debug(f"⚠️ URL error connecting to port {port} (attempt {retry + 1}): {e.reason}")
527
637
  if retry < max_retries - 1:
528
638
  logger.debug("Connection refused - server may still be initializing, retrying...")
529
- time.sleep(0.5)
639
+ # Use exponential backoff for retries
640
+ backoff = min(0.1 * (2 ** retry), 1.0)
641
+ time.sleep(backoff)
530
642
  continue
531
643
  return False
532
644
 
@@ -585,44 +697,46 @@ def _start_standalone_socketio_server(port, logger):
585
697
  logger.error(f"Failed to start Socket.IO daemon: {result.stderr}")
586
698
  return False
587
699
 
588
- # Wait for server to be ready with reasonable timeouts and progressive delays
589
- # WHY: Socket.IO server startup involves async initialization:
590
- # 1. Thread creation (~0.1s)
591
- # 2. Event loop setup (~0.5s)
592
- # 3. aiohttp server binding (~2-5s)
593
- # 4. Socket.IO service initialization (~1-3s)
594
- # Total: typically 2-5 seconds, up to 15 seconds max
595
- max_attempts = 12 # Reduced from 30 - provides ~15 second total timeout
596
- initial_delay = 0.75 # Reduced from 1.0s - balanced startup time
597
- max_delay = 2.0 # Reduced from 3.0s - sufficient for binding delays
598
-
599
- logger.info(f"Waiting up to ~15 seconds for server to be fully ready...")
600
-
601
- # Give the daemon initial time to fork and start before checking
602
- logger.debug("Allowing initial daemon startup time...")
603
- time.sleep(0.5)
604
-
605
- for attempt in range(max_attempts):
606
- # Progressive delay - start fast, then slow down for socket binding
607
- if attempt < 5:
608
- delay = initial_delay
609
- else:
610
- delay = min(max_delay, initial_delay + (attempt - 5) * 0.2)
700
+ # Wait for server using event-based polling instead of fixed delays
701
+ # WHY: Replace fixed sleep delays with active polling for faster startup detection
702
+ max_wait_time = 15 # Maximum 15 seconds
703
+ poll_interval = 0.1 # Start with 100ms polling
704
+
705
+ logger.info(f"Waiting up to {max_wait_time} seconds for server to be ready...")
706
+
707
+ # Give daemon minimal time to fork
708
+ time.sleep(0.2) # Reduced from 0.5s
709
+
710
+ start_time = time.time()
711
+ attempt = 0
712
+
713
+ while time.time() - start_time < max_wait_time:
714
+ attempt += 1
715
+ elapsed = time.time() - start_time
611
716
 
612
- logger.debug(f"Checking server readiness (attempt {attempt + 1}/{max_attempts}, waiting {delay}s)")
717
+ logger.debug(f"Checking server readiness (attempt {attempt}, elapsed {elapsed:.1f}s)")
718
+
719
+ # Adaptive polling - start fast, slow down over time
720
+ if elapsed < 2:
721
+ poll_interval = 0.1 # 100ms for first 2 seconds
722
+ elif elapsed < 5:
723
+ poll_interval = 0.25 # 250ms for next 3 seconds
724
+ else:
725
+ poll_interval = 0.5 # 500ms after 5 seconds
613
726
 
614
- # Give the daemon process time to initialize and bind to the socket
615
- time.sleep(delay)
727
+ time.sleep(poll_interval)
616
728
 
617
729
  # Check if the daemon server is accepting connections
618
730
  if _check_socketio_server_running(port, logger):
619
731
  logger.info(f"✅ Standalone Socket.IO server started successfully on port {port}")
620
- logger.info(f"🕐 Server ready after {attempt + 1} attempts ({(attempt + 1) * delay:.1f}s)")
732
+ logger.info(f"🕐 Server ready after {attempt} attempts ({elapsed:.1f}s)")
621
733
  return True
622
734
  else:
623
- logger.debug(f"Server not yet accepting connections on attempt {attempt + 1}")
735
+ logger.debug(f"Server not yet accepting connections on attempt {attempt}")
624
736
 
625
- logger.error(f"❌ Socket.IO server health check failed after {max_attempts} attempts (~15s timeout)")
737
+ # Timeout reached
738
+ elapsed_total = time.time() - start_time
739
+ logger.error(f"❌ Socket.IO server health check failed after {max_wait_time}s timeout ({attempt} attempts)")
626
740
  logger.warning(f"⏱️ Server may still be starting - try waiting a few more seconds")
627
741
  logger.warning(f"💡 The daemon process might be running but not yet accepting HTTP connections")
628
742
  logger.error(f"🔧 Troubleshooting steps:")
@@ -689,4 +803,78 @@ def open_in_browser_tab(url, logger):
689
803
  except Exception as e:
690
804
  logger.warning(f"Browser opening failed: {e}")
691
805
  # Final fallback
692
- webbrowser.open(url)
806
+ webbrowser.open(url)
807
+
808
+
809
+ def _check_configuration_health(logger):
810
+ """Check configuration health at startup and warn about issues.
811
+
812
+ WHY: Configuration errors can cause silent failures, especially for response
813
+ logging. This function proactively checks configuration at startup and warns
814
+ users about any issues, providing actionable guidance.
815
+
816
+ DESIGN DECISIONS:
817
+ - Non-blocking: Issues are logged as warnings, not errors
818
+ - Actionable: Provides specific commands to fix issues
819
+ - Focused: Only checks critical configuration that affects runtime
820
+
821
+ Args:
822
+ logger: Logger instance for output
823
+ """
824
+ try:
825
+ # Load configuration
826
+ config = Config()
827
+
828
+ # Validate configuration
829
+ is_valid, errors, warnings = config.validate_configuration()
830
+
831
+ # Get configuration status for additional context
832
+ status = config.get_configuration_status()
833
+
834
+ # Report critical errors that will affect functionality
835
+ if errors:
836
+ logger.warning("⚠️ Configuration issues detected:")
837
+ for error in errors[:3]: # Show first 3 errors
838
+ logger.warning(f" • {error}")
839
+ if len(errors) > 3:
840
+ logger.warning(f" • ... and {len(errors) - 3} more")
841
+ logger.info("💡 Run 'claude-mpm config validate' to see all issues and fixes")
842
+
843
+ # Check response logging specifically since it's commonly misconfigured
844
+ response_logging_enabled = config.get('response_logging.enabled', False)
845
+ if not response_logging_enabled:
846
+ logger.debug("Response logging is disabled (response_logging.enabled=false)")
847
+ else:
848
+ # Check if session directory is writable
849
+ session_dir = Path(config.get('response_logging.session_directory', '.claude-mpm/responses'))
850
+ if not session_dir.is_absolute():
851
+ session_dir = Path.cwd() / session_dir
852
+
853
+ if not session_dir.exists():
854
+ try:
855
+ session_dir.mkdir(parents=True, exist_ok=True)
856
+ logger.debug(f"Created response logging directory: {session_dir}")
857
+ except Exception as e:
858
+ logger.warning(f"Cannot create response logging directory {session_dir}: {e}")
859
+ logger.info("💡 Fix with: mkdir -p " + str(session_dir))
860
+ elif not os.access(session_dir, os.W_OK):
861
+ logger.warning(f"Response logging directory is not writable: {session_dir}")
862
+ logger.info("💡 Fix with: chmod 755 " + str(session_dir))
863
+
864
+ # Report non-critical warnings (only in debug mode)
865
+ if warnings and logger.isEnabledFor(logging.DEBUG):
866
+ logger.debug("Configuration warnings:")
867
+ for warning in warnings:
868
+ logger.debug(f" • {warning}")
869
+
870
+ # Log loaded configuration source for debugging
871
+ if status.get('loaded_from') and status['loaded_from'] != 'defaults':
872
+ logger.debug(f"Configuration loaded from: {status['loaded_from']}")
873
+
874
+ except Exception as e:
875
+ # Don't let configuration check errors prevent startup
876
+ logger.debug(f"Configuration check failed (non-critical): {e}")
877
+ # Only show user-facing message if it's likely to affect them
878
+ if "yaml" in str(e).lower():
879
+ logger.warning("⚠️ Configuration file may have YAML syntax errors")
880
+ logger.info("💡 Validate with: claude-mpm config validate")