claude-mpm 3.5.4__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 (68) hide show
  1. claude_mpm/.claude-mpm/logs/hooks_20250728.log +10 -0
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  4. claude_mpm/agents/BASE_PM.md +273 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +114 -102
  6. claude_mpm/agents/agent-template.yaml +83 -0
  7. claude_mpm/agents/agent_loader.py +36 -1
  8. claude_mpm/agents/async_agent_loader.py +421 -0
  9. claude_mpm/agents/templates/code_analyzer.json +81 -0
  10. claude_mpm/agents/templates/data_engineer.json +18 -3
  11. claude_mpm/agents/templates/documentation.json +18 -3
  12. claude_mpm/agents/templates/engineer.json +19 -4
  13. claude_mpm/agents/templates/ops.json +18 -3
  14. claude_mpm/agents/templates/qa.json +20 -4
  15. claude_mpm/agents/templates/research.json +20 -4
  16. claude_mpm/agents/templates/security.json +18 -3
  17. claude_mpm/agents/templates/version_control.json +16 -3
  18. claude_mpm/cli/README.md +108 -0
  19. claude_mpm/cli/__init__.py +5 -1
  20. claude_mpm/cli/commands/__init__.py +5 -1
  21. claude_mpm/cli/commands/agents.py +233 -6
  22. claude_mpm/cli/commands/aggregate.py +462 -0
  23. claude_mpm/cli/commands/config.py +277 -0
  24. claude_mpm/cli/commands/run.py +228 -47
  25. claude_mpm/cli/parser.py +176 -1
  26. claude_mpm/cli/utils.py +9 -1
  27. claude_mpm/cli_module/refactoring_guide.md +253 -0
  28. claude_mpm/config/async_logging_config.yaml +145 -0
  29. claude_mpm/constants.py +19 -0
  30. claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +34 -0
  31. claude_mpm/core/claude_runner.py +413 -76
  32. claude_mpm/core/config.py +161 -4
  33. claude_mpm/core/config_paths.py +0 -1
  34. claude_mpm/core/factories.py +9 -3
  35. claude_mpm/core/framework_loader.py +81 -0
  36. claude_mpm/dashboard/.claude-mpm/memories/README.md +36 -0
  37. claude_mpm/dashboard/README.md +121 -0
  38. claude_mpm/dashboard/static/js/dashboard.js.backup +1973 -0
  39. claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +36 -0
  40. claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +39 -0
  41. claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +38 -0
  42. claude_mpm/hooks/README.md +96 -0
  43. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  44. claude_mpm/init.py +123 -18
  45. claude_mpm/models/agent_session.py +511 -0
  46. claude_mpm/schemas/agent_schema.json +435 -0
  47. claude_mpm/scripts/__init__.py +15 -0
  48. claude_mpm/scripts/start_activity_logging.py +86 -0
  49. claude_mpm/services/agents/deployment/agent_deployment.py +326 -24
  50. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  51. claude_mpm/services/agents/management/agent_management_service.py +2 -1
  52. claude_mpm/services/event_aggregator.py +547 -0
  53. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  54. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +2 -2
  56. claude_mpm/services/version_control/VERSION +1 -0
  57. claude_mpm/utils/agent_dependency_loader.py +655 -0
  58. claude_mpm/utils/console.py +11 -0
  59. claude_mpm/utils/dependency_cache.py +376 -0
  60. claude_mpm/utils/dependency_strategies.py +343 -0
  61. claude_mpm/utils/environment_context.py +310 -0
  62. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +87 -1
  63. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +67 -37
  64. claude_mpm/agents/templates/pm.json +0 -122
  65. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
  66. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
  67. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
  68. {claude_mpm-3.5.4.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
@@ -281,17 +383,10 @@ def run_session(args):
281
383
  websocket_port=websocket_port
282
384
  )
283
385
 
284
- # Ensure project agents are available if we're in a project directory
285
- # This deploys system agents to .claude-mpm/agents/ for local customization
286
- if not hasattr(args, 'no_native_agents') or not args.no_native_agents:
287
- # Check if we're in a project directory (has .git or other markers)
288
- project_markers = ['.git', 'pyproject.toml', 'package.json', 'requirements.txt']
289
- cwd = Path.cwd()
290
- is_project = any((cwd / marker).exists() for marker in project_markers)
291
-
292
- if is_project:
293
- logger.debug("Detected project directory, ensuring agents are available locally")
294
- runner.ensure_project_agents()
386
+ # Agent deployment is handled by ClaudeRunner.setup_agents() and
387
+ # ClaudeRunner.deploy_project_agents_to_claude() which are called
388
+ # in both run_interactive() and run_oneshot() methods.
389
+ # No need for redundant deployment here.
295
390
 
296
391
  # Set browser opening flag for monitor mode
297
392
  if monitor_mode:
@@ -428,13 +523,17 @@ def launch_socketio_monitor(port, logger):
428
523
  print(f"✅ Socket.IO server started successfully")
429
524
  print(f"📊 Dashboard: {dashboard_url}")
430
525
 
431
- # Final verification that server is responsive
526
+ # Final verification that server is responsive using event-based checking
432
527
  final_check_passed = False
433
- 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:
434
532
  if _check_socketio_server_running(socketio_port, logger):
435
533
  final_check_passed = True
436
534
  break
437
- time.sleep(1)
535
+ # Use a very short sleep just to yield CPU
536
+ time.sleep(0.1) # 100ms polling interval
438
537
 
439
538
  if not final_check_passed:
440
539
  logger.warning("Server started but final connectivity check failed")
@@ -520,20 +619,26 @@ def _check_socketio_server_running(port, logger):
520
619
  else:
521
620
  logger.debug(f"⚠️ HTTP response code {response.getcode()} from port {port} (attempt {retry + 1})")
522
621
  if retry < max_retries - 1:
523
- 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)
524
625
 
525
626
  except urllib.error.HTTPError as e:
526
627
  logger.debug(f"⚠️ HTTP error {e.code} from server on port {port} (attempt {retry + 1})")
527
628
  if retry < max_retries - 1 and e.code in [404, 503]: # Server starting but not ready
528
629
  logger.debug("Server appears to be starting, retrying...")
529
- time.sleep(0.5)
630
+ # Use exponential backoff for retries
631
+ backoff = min(0.1 * (2 ** retry), 1.0)
632
+ time.sleep(backoff)
530
633
  continue
531
634
  return False
532
635
  except urllib.error.URLError as e:
533
636
  logger.debug(f"⚠️ URL error connecting to port {port} (attempt {retry + 1}): {e.reason}")
534
637
  if retry < max_retries - 1:
535
638
  logger.debug("Connection refused - server may still be initializing, retrying...")
536
- time.sleep(0.5)
639
+ # Use exponential backoff for retries
640
+ backoff = min(0.1 * (2 ** retry), 1.0)
641
+ time.sleep(backoff)
537
642
  continue
538
643
  return False
539
644
 
@@ -592,44 +697,46 @@ def _start_standalone_socketio_server(port, logger):
592
697
  logger.error(f"Failed to start Socket.IO daemon: {result.stderr}")
593
698
  return False
594
699
 
595
- # Wait for server to be ready with reasonable timeouts and progressive delays
596
- # WHY: Socket.IO server startup involves async initialization:
597
- # 1. Thread creation (~0.1s)
598
- # 2. Event loop setup (~0.5s)
599
- # 3. aiohttp server binding (~2-5s)
600
- # 4. Socket.IO service initialization (~1-3s)
601
- # Total: typically 2-5 seconds, up to 15 seconds max
602
- max_attempts = 12 # Reduced from 30 - provides ~15 second total timeout
603
- initial_delay = 0.75 # Reduced from 1.0s - balanced startup time
604
- max_delay = 2.0 # Reduced from 3.0s - sufficient for binding delays
605
-
606
- logger.info(f"Waiting up to ~15 seconds for server to be fully ready...")
607
-
608
- # Give the daemon initial time to fork and start before checking
609
- logger.debug("Allowing initial daemon startup time...")
610
- time.sleep(0.5)
611
-
612
- for attempt in range(max_attempts):
613
- # Progressive delay - start fast, then slow down for socket binding
614
- if attempt < 5:
615
- delay = initial_delay
616
- else:
617
- 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
716
+
717
+ logger.debug(f"Checking server readiness (attempt {attempt}, elapsed {elapsed:.1f}s)")
618
718
 
619
- logger.debug(f"Checking server readiness (attempt {attempt + 1}/{max_attempts}, waiting {delay}s)")
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
620
726
 
621
- # Give the daemon process time to initialize and bind to the socket
622
- time.sleep(delay)
727
+ time.sleep(poll_interval)
623
728
 
624
729
  # Check if the daemon server is accepting connections
625
730
  if _check_socketio_server_running(port, logger):
626
731
  logger.info(f"✅ Standalone Socket.IO server started successfully on port {port}")
627
- 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)")
628
733
  return True
629
734
  else:
630
- logger.debug(f"Server not yet accepting connections on attempt {attempt + 1}")
735
+ logger.debug(f"Server not yet accepting connections on attempt {attempt}")
631
736
 
632
- 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)")
633
740
  logger.warning(f"⏱️ Server may still be starting - try waiting a few more seconds")
634
741
  logger.warning(f"💡 The daemon process might be running but not yet accepting HTTP connections")
635
742
  logger.error(f"🔧 Troubleshooting steps:")
@@ -696,4 +803,78 @@ def open_in_browser_tab(url, logger):
696
803
  except Exception as e:
697
804
  logger.warning(f"Browser opening failed: {e}")
698
805
  # Final fallback
699
- 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")