claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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 (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
1
1
  """Claude runner with both exec and subprocess launch methods."""
2
2
 
3
+ import hashlib
4
+ import json
3
5
  import os
4
6
  from pathlib import Path
5
7
  from typing import Optional
@@ -211,9 +213,146 @@ class ClaudeRunner:
211
213
  }
212
214
  )
213
215
 
216
+ def _get_deployment_state_path(self) -> Path:
217
+ """Get path to deployment state file.
218
+
219
+ CRITICAL: Must match path used by startup.py::_save_deployment_state_after_reconciliation()
220
+ Located at: .claude-mpm/cache/deployment_state.json
221
+ """
222
+ return Path.cwd() / ".claude-mpm" / "cache" / "deployment_state.json"
223
+
224
+ def _calculate_deployment_hash(self, agents_dir: Path) -> str:
225
+ """Calculate hash of all agent files for change detection.
226
+
227
+ Args:
228
+ agents_dir: Directory containing agent .md files
229
+
230
+ Returns:
231
+ SHA256 hash of agent file contents
232
+ """
233
+ if not agents_dir.exists():
234
+ return ""
235
+
236
+ # Get all .md files sorted for consistent hashing
237
+ agent_files = sorted(agents_dir.glob("*.md"))
238
+
239
+ hash_obj = hashlib.sha256()
240
+ for agent_file in agent_files:
241
+ # Include filename and content in hash
242
+ hash_obj.update(agent_file.name.encode())
243
+ try:
244
+ hash_obj.update(agent_file.read_bytes())
245
+ except Exception as e:
246
+ self.logger.debug(f"Error reading {agent_file} for hash: {e}")
247
+
248
+ return hash_obj.hexdigest()
249
+
250
+ def _check_deployment_state(self) -> bool:
251
+ """Check if agents are already deployed and up-to-date.
252
+
253
+ Returns:
254
+ True if agents are already deployed and match current version, False otherwise
255
+ """
256
+ state_file = self._get_deployment_state_path()
257
+ agents_dir = Path.cwd() / ".claude" / "agents"
258
+
259
+ # If state file doesn't exist, need to deploy
260
+ if not state_file.exists():
261
+ return False
262
+
263
+ # If agents directory doesn't exist, need to deploy
264
+ if not agents_dir.exists():
265
+ return False
266
+
267
+ try:
268
+ # Load deployment state
269
+ state_data = json.loads(state_file.read_text())
270
+
271
+ # Get current version from package
272
+ from claude_mpm import __version__
273
+
274
+ # Check if version matches
275
+ if state_data.get("version") != __version__:
276
+ self.logger.debug(
277
+ f"Version mismatch: {state_data.get('version')} != {__version__}"
278
+ )
279
+ return False
280
+
281
+ # Check if agent count and hash match
282
+ current_hash = self._calculate_deployment_hash(agents_dir)
283
+ stored_hash = state_data.get("deployment_hash", "")
284
+
285
+ if current_hash != stored_hash:
286
+ self.logger.debug("Agent deployment hash mismatch")
287
+ return False
288
+
289
+ # All checks passed - agents are already deployed
290
+ agent_count = state_data.get("agent_count", 0)
291
+ self.logger.debug(
292
+ f"Agents already deployed: {agent_count} agents (v{__version__})"
293
+ )
294
+ return True
295
+
296
+ except Exception as e:
297
+ self.logger.debug(f"Error checking deployment state: {e}")
298
+ return False
299
+
300
+ def _save_deployment_state(self, agent_count: int) -> None:
301
+ """Save current deployment state.
302
+
303
+ Args:
304
+ agent_count: Number of agents deployed
305
+ """
306
+ state_file = self._get_deployment_state_path()
307
+ agents_dir = Path.cwd() / ".claude" / "agents"
308
+
309
+ try:
310
+ import time
311
+
312
+ from claude_mpm import __version__
313
+
314
+ # Calculate deployment hash
315
+ deployment_hash = self._calculate_deployment_hash(agents_dir)
316
+
317
+ # Create state data
318
+ state_data = {
319
+ "version": __version__,
320
+ "agent_count": agent_count,
321
+ "deployment_hash": deployment_hash,
322
+ "deployed_at": time.time(),
323
+ }
324
+
325
+ # Ensure directory exists
326
+ state_file.parent.mkdir(parents=True, exist_ok=True)
327
+
328
+ # Write state file
329
+ state_file.write_text(json.dumps(state_data, indent=2))
330
+ self.logger.debug(f"Saved deployment state: {agent_count} agents")
331
+
332
+ except Exception as e:
333
+ self.logger.debug(f"Error saving deployment state: {e}")
334
+
214
335
  def setup_agents(self) -> bool:
215
336
  """Deploy native agents to .claude/agents/."""
216
337
  try:
338
+ # SIMPLE CHECK: If agents already exist from reconciliation, skip deployment
339
+ # This ensures reconciliation's user-configured agents are never overwritten
340
+ agents_dir = Path.cwd() / ".claude" / "agents"
341
+ if agents_dir.exists():
342
+ existing_agents = list(agents_dir.glob("*.md"))
343
+ if len(existing_agents) > 0:
344
+ # Reconciliation already deployed agents - skip
345
+ self.logger.debug(
346
+ f"Skipping setup_agents: {len(existing_agents)} agents already deployed by reconciliation"
347
+ )
348
+ if self.project_logger:
349
+ self.project_logger.log_system(
350
+ f"Agents already deployed via reconciliation: {len(existing_agents)} agents",
351
+ level="DEBUG",
352
+ component="deployment",
353
+ )
354
+ return True
355
+
217
356
  if self.project_logger:
218
357
  self.project_logger.log_system(
219
358
  "Starting agent deployment", level="INFO", component="deployment"
@@ -239,6 +378,12 @@ class ClaudeRunner:
239
378
 
240
379
  # Set Claude environment
241
380
  self.deployment_service.set_claude_environment()
381
+
382
+ # Save deployment state for future runs
383
+ agents_dir = Path.cwd() / ".claude" / "agents"
384
+ total_agents = len(list(agents_dir.glob("*.md")))
385
+ self._save_deployment_state(total_agents)
386
+
242
387
  return True
243
388
  self.logger.info("All agents already up to date")
244
389
  if self.project_logger:
@@ -247,6 +392,13 @@ class ClaudeRunner:
247
392
  level="INFO",
248
393
  component="deployment",
249
394
  )
395
+
396
+ # Save deployment state even if no changes
397
+ agents_dir = Path.cwd() / ".claude" / "agents"
398
+ if agents_dir.exists():
399
+ total_agents = len(list(agents_dir.glob("*.md")))
400
+ self._save_deployment_state(total_agents)
401
+
250
402
  return True
251
403
 
252
404
  except PermissionError as e:
claude_mpm/core/config.py CHANGED
@@ -12,11 +12,10 @@ import threading
12
12
  from pathlib import Path
13
13
  from typing import Any, Dict, List, Optional, Tuple, Union
14
14
 
15
- import yaml
16
-
17
15
  from claude_mpm.core.logging_utils import get_logger
18
16
 
19
- from ..utils.config_manager import ConfigurationManager
17
+ # Lazy import ConfigurationManager to avoid importing yaml at module level
18
+ # This prevents hook errors when yaml isn't available in the execution environment
20
19
  from .exceptions import ConfigurationError, FileOperationError
21
20
  from .unified_paths import get_path_manager
22
21
 
@@ -104,6 +103,9 @@ class Config:
104
103
  Config._initialized = True
105
104
  logger.debug("Initializing Config singleton for the first time")
106
105
 
106
+ # Lazy import ConfigurationManager at runtime to avoid yaml import at module level
107
+ from ..utils.config_manager import ConfigurationManager
108
+
107
109
  # Initialize instance variables inside the lock to ensure thread safety
108
110
  self._config: Dict[str, Any] = {}
109
111
  self._env_prefix = env_prefix
@@ -224,21 +226,6 @@ class Config:
224
226
  f"Response logging format: {response_logging.get('format', 'json')}"
225
227
  )
226
228
 
227
- except yaml.YAMLError as e:
228
- logger.error(f"YAML syntax error in {file_path}: {e}")
229
- if hasattr(e, "problem_mark"):
230
- mark = e.problem_mark
231
- logger.error(f"Error at line {mark.line + 1}, column {mark.column + 1}")
232
- logger.info(
233
- "TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py"
234
- )
235
- logger.info(
236
- "TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/ /g' "
237
- + str(file_path)
238
- )
239
- # Store error for later retrieval
240
- self._config["_load_error"] = str(e)
241
-
242
229
  except json.JSONDecodeError as e:
243
230
  logger.error(f"JSON syntax error in {file_path}: {e}")
244
231
  logger.error(f"Error at line {e.lineno}, column {e.colno}")
@@ -255,7 +242,28 @@ class Config:
255
242
  },
256
243
  ) from e
257
244
  except Exception as e:
258
- # Catch any remaining unexpected errors and wrap them as configuration errors
245
+ # Handle YAML errors without importing yaml at module level
246
+ # ConfigurationManager.load_yaml raises yaml.YAMLError, but we don't want to import yaml
247
+ # Check if it's a YAML error by class name to avoid import
248
+ if e.__class__.__name__ == "YAMLError":
249
+ logger.error(f"YAML syntax error in {file_path}: {e}")
250
+ if hasattr(e, "problem_mark"):
251
+ mark = e.problem_mark
252
+ logger.error(
253
+ f"Error at line {mark.line + 1}, column {mark.column + 1}"
254
+ )
255
+ logger.info(
256
+ "TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py"
257
+ )
258
+ logger.info(
259
+ "TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/ /g' "
260
+ + str(file_path)
261
+ )
262
+ # Store error for later retrieval
263
+ self._config["_load_error"] = str(e)
264
+ return # Don't re-raise, we handled it
265
+
266
+ # Not a YAML error, wrap as configuration error
259
267
  raise ConfigurationError(
260
268
  f"Unexpected error loading configuration from {file_path}: {e}",
261
269
  context={
@@ -491,7 +499,7 @@ class Config:
491
499
  # Socket.IO server health and recovery configuration
492
500
  "socketio_server": {
493
501
  "host": "localhost",
494
- "port": 8765,
502
+ "port": 8768, # Default SocketIO port (from network_config.NetworkPorts)
495
503
  "enable_health_monitoring": True,
496
504
  "enable_recovery": True,
497
505
  "health_monitoring": {
@@ -532,7 +540,7 @@ class Config:
532
540
  # Monitor server configuration (decoupled from dashboard)
533
541
  "monitor_server": {
534
542
  "host": "localhost",
535
- "port": 8765, # Default monitor port (shared with dashboard)
543
+ "port": 8765, # Default monitor port (from network_config.NetworkPorts.MONITOR_DEFAULT)
536
544
  "enable_health_monitoring": True,
537
545
  "auto_start": False, # Don't auto-start with dashboard by default
538
546
  "event_buffer_size": 2000, # Larger buffer for monitor server
@@ -541,7 +549,7 @@ class Config:
541
549
  # Dashboard server configuration (connects to monitor)
542
550
  "dashboard_server": {
543
551
  "host": "localhost",
544
- "port": 8765, # Dashboard UI port
552
+ "port": 8767, # Dashboard UI port (from network_config.NetworkPorts.DASHBOARD_DEFAULT)
545
553
  "monitor_host": "localhost", # Monitor server host to connect to
546
554
  "monitor_port": 8765, # Monitor server port to connect to
547
555
  "auto_connect_monitor": True, # Automatically connect to monitor
@@ -44,11 +44,14 @@ class ConfigConstants:
44
44
  "startup": 60,
45
45
  "graceful_shutdown": 30,
46
46
  },
47
- # Ports
47
+ # Ports (updated to use network_config.NetworkPorts defaults)
48
48
  "ports": {
49
- "socketio_default": 8765,
50
- "socketio_range_start": 8765,
51
- "socketio_range_end": 8775,
49
+ "monitor_default": 8765, # NetworkPorts.MONITOR_DEFAULT
50
+ "commander_default": 8766, # NetworkPorts.COMMANDER_DEFAULT
51
+ "dashboard_default": 8767, # NetworkPorts.DASHBOARD_DEFAULT
52
+ "socketio_default": 8768, # NetworkPorts.SOCKETIO_DEFAULT
53
+ "socketio_range_start": 8765, # NetworkPorts.PORT_RANGE_START
54
+ "socketio_range_end": 8785, # NetworkPorts.PORT_RANGE_END
52
55
  },
53
56
  # Cache settings
54
57
  "cache": {
@@ -134,23 +137,70 @@ class ConfigConstants:
134
137
  Get port value by type.
135
138
 
136
139
  Args:
137
- port_type: Type of port (e.g., 'socketio_default')
140
+ port_type: Type of port (e.g., 'socketio_default', 'monitor_default')
138
141
 
139
142
  Returns:
140
143
  Port number
141
144
  """
142
145
  try:
146
+ # Try to get from unified config first
143
147
  config = cls._get_config_service().config
144
148
 
149
+ if port_type == "monitor_default":
150
+ return (
151
+ config.network.monitor_port
152
+ if hasattr(config.network, "monitor_port")
153
+ else 8765
154
+ )
155
+ if port_type == "commander_default":
156
+ return (
157
+ config.network.commander_port
158
+ if hasattr(config.network, "commander_port")
159
+ else 8766
160
+ )
161
+ if port_type == "dashboard_default":
162
+ return (
163
+ config.network.dashboard_port
164
+ if hasattr(config.network, "dashboard_port")
165
+ else 8767
166
+ )
145
167
  if port_type == "socketio_default":
146
- return config.network.socketio_port
168
+ return (
169
+ config.network.socketio_port
170
+ if hasattr(config.network, "socketio_port")
171
+ else 8768
172
+ )
147
173
  if port_type == "socketio_range_start":
148
- return config.network.socketio_port_range[0]
174
+ return (
175
+ config.network.socketio_port_range[0]
176
+ if hasattr(config.network, "socketio_port_range")
177
+ else 8765
178
+ )
149
179
  if port_type == "socketio_range_end":
150
- return config.network.socketio_port_range[1]
180
+ return (
181
+ config.network.socketio_port_range[1]
182
+ if hasattr(config.network, "socketio_port_range")
183
+ else 8785
184
+ )
151
185
  return cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
152
186
  except Exception:
153
- return cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
187
+ # Fallback to network_config.NetworkPorts or DEFAULT_VALUES
188
+ try:
189
+ from .network_config import NetworkPorts
190
+
191
+ port_map = {
192
+ "monitor_default": NetworkPorts.MONITOR_DEFAULT,
193
+ "commander_default": NetworkPorts.COMMANDER_DEFAULT,
194
+ "dashboard_default": NetworkPorts.DASHBOARD_DEFAULT,
195
+ "socketio_default": NetworkPorts.SOCKETIO_DEFAULT,
196
+ "socketio_range_start": NetworkPorts.PORT_RANGE_START,
197
+ "socketio_range_end": NetworkPorts.PORT_RANGE_END,
198
+ }
199
+ return port_map.get(
200
+ port_type, cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
201
+ )
202
+ except Exception:
203
+ return cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
154
204
 
155
205
  @classmethod
156
206
  def get_cache_setting(cls, setting_name: str) -> Any:
@@ -304,6 +354,21 @@ def get_socketio_port() -> int:
304
354
  return ConfigConstants.get_port("socketio_default")
305
355
 
306
356
 
357
+ def get_monitor_port() -> int:
358
+ """Get default monitor port."""
359
+ return ConfigConstants.get_port("monitor_default")
360
+
361
+
362
+ def get_commander_port() -> int:
363
+ """Get default commander port."""
364
+ return ConfigConstants.get_port("commander_default")
365
+
366
+
367
+ def get_dashboard_port() -> int:
368
+ """Get default dashboard port."""
369
+ return ConfigConstants.get_port("dashboard_default")
370
+
371
+
307
372
  def get_cache_size() -> float:
308
373
  """Get default cache size in MB."""
309
374
  return ConfigConstants.get_cache_setting("max_size_mb")
@@ -38,12 +38,36 @@ class SystemLimits:
38
38
 
39
39
 
40
40
  class NetworkConfig:
41
- """Network-related configuration constants."""
41
+ """Network-related configuration constants.
42
42
 
43
- # Port ranges
44
- SOCKETIO_PORT_RANGE: Tuple[int, int] = (8765, 8785)
45
- DEFAULT_SOCKETIO_PORT = 8765
46
- DEFAULT_DASHBOARD_PORT = 8765
43
+ NOTE: Port defaults are now centralized in network_config.NetworkPorts.
44
+ This class maintains backward compatibility but delegates to NetworkPorts.
45
+ """
46
+
47
+ # Import from network_config for single source of truth
48
+ # Lazy import to avoid circular dependencies
49
+ @property
50
+ def SOCKETIO_PORT_RANGE(self) -> Tuple[int, int]:
51
+ from .network_config import NetworkPorts
52
+
53
+ return (NetworkPorts.PORT_RANGE_START, NetworkPorts.PORT_RANGE_END)
54
+
55
+ @property
56
+ def DEFAULT_SOCKETIO_PORT(self) -> int:
57
+ from .network_config import NetworkPorts
58
+
59
+ return NetworkPorts.SOCKETIO_DEFAULT
60
+
61
+ @property
62
+ def DEFAULT_DASHBOARD_PORT(self) -> int:
63
+ from .network_config import NetworkPorts
64
+
65
+ return NetworkPorts.DASHBOARD_DEFAULT
66
+
67
+ # Port ranges (module-level for backward compatibility)
68
+ SOCKETIO_PORT_RANGE: Tuple[int, int] = (8765, 8785) # Will be updated at runtime
69
+ DEFAULT_SOCKETIO_PORT = 8768 # Updated to match new default
70
+ DEFAULT_DASHBOARD_PORT = 8767 # Updated to match new default
47
71
 
48
72
  # Connection timeouts (seconds)
49
73
  CONNECTION_TIMEOUT = 5.0
@@ -303,18 +327,38 @@ DEFAULT_TIMEOUT = TimeoutConfig.DEFAULT_TIMEOUT
303
327
 
304
328
 
305
329
  class NetworkPorts:
306
- """Network port configuration."""
330
+ """Network port configuration.
331
+
332
+ DEPRECATED: Use claude_mpm.core.network_config.NetworkPorts instead.
333
+ This class is maintained for backward compatibility.
334
+ """
335
+
336
+ # Import from network_config for single source of truth
337
+ @classmethod
338
+ def _get_config(cls):
339
+ from .network_config import NetworkPorts as NewNetworkPorts
340
+
341
+ return NewNetworkPorts
342
+
343
+ # Delegate to new NetworkPorts
344
+ @property
345
+ def DEFAULT_SOCKETIO(self) -> int:
346
+ return self._get_config().SOCKETIO_DEFAULT
347
+
348
+ @property
349
+ def DEFAULT_DASHBOARD(self) -> int:
350
+ return self._get_config().DASHBOARD_DEFAULT
307
351
 
308
- # Use existing values from NetworkConfig
309
- DEFAULT_SOCKETIO = NetworkConfig.DEFAULT_SOCKETIO_PORT
310
- DEFAULT_DASHBOARD = NetworkConfig.DEFAULT_DASHBOARD_PORT
311
- PORT_RANGE_START = NetworkConfig.SOCKETIO_PORT_RANGE[0]
312
- PORT_RANGE_END = NetworkConfig.SOCKETIO_PORT_RANGE[1]
352
+ # Keep class-level attributes for compatibility
353
+ DEFAULT_SOCKETIO = 8768 # Updated to match network_config
354
+ DEFAULT_DASHBOARD = 8767 # Updated to match network_config
355
+ PORT_RANGE_START = 8765
356
+ PORT_RANGE_END = 8785
313
357
 
314
358
  @classmethod
315
359
  def get_port_range(cls) -> range:
316
360
  """Get the valid port range."""
317
- return range(cls.PORT_RANGE_START, cls.PORT_RANGE_END + 1)
361
+ return cls._get_config().get_port_range()
318
362
 
319
363
 
320
364
  class ProjectPaths:
@@ -186,8 +186,9 @@ class HookManager:
186
186
  env["CLAUDE_MPM_HOOK_DEBUG"] = "true"
187
187
 
188
188
  # Execute with timeout in background thread
189
+ # Run as module to ensure proper package context for relative imports
189
190
  result = subprocess.run( # nosec B603 B607
190
- ["python", str(self.hook_handler_path)],
191
+ ["python", "-m", "claude_mpm.hooks.claude_hooks.hook_handler"],
191
192
  input=event_json,
192
193
  text=True,
193
194
  capture_output=True,
@@ -143,11 +143,12 @@ class InteractiveSession:
143
143
  Tuple of (success, environment_dict)
144
144
  """
145
145
  try:
146
- # Deploy system agents
147
- if not self.runner.setup_agents():
148
- print("Continuing without native agents...")
146
+ # NOTE: System agents are deployed via reconciliation during startup.
147
+ # The reconciliation process respects user configuration and handles
148
+ # both native and custom mode deployment. No need to call setup_agents() here.
149
149
 
150
- # Deploy project-specific agents
150
+ # Deploy project-specific agents from .claude-mpm/agents/
151
+ # This is separate from system agents and handles user-defined agents
151
152
  self.runner.deploy_project_agents_to_claude()
152
153
 
153
154
  # Build command
claude_mpm/core/logger.py CHANGED
@@ -391,8 +391,22 @@ def cleanup_old_mpm_logs(
391
391
 
392
392
 
393
393
  def get_logger(name: str) -> logging.Logger:
394
- """Get a logger instance."""
395
- return logging.getLogger(f"claude_mpm.{name}")
394
+ """Get a logger instance.
395
+
396
+ CRITICAL: Respects startup suppression mode (CRITICAL+1) to prevent
397
+ early INFO logs before setup_logging() is called.
398
+ """
399
+ logger = logging.getLogger(f"claude_mpm.{name}")
400
+
401
+ # Check if root logger is suppressed (startup.py sets CRITICAL+1)
402
+ root_logger = logging.getLogger()
403
+ if root_logger.level > logging.CRITICAL:
404
+ # Suppression active - ensure this logger is also suppressed
405
+ logger.setLevel(logging.CRITICAL + 1)
406
+ logger.handlers = []
407
+ logger.propagate = False
408
+
409
+ return logger
396
410
 
397
411
 
398
412
  def setup_streaming_logger(name: str, level: str = "INFO") -> logging.Logger:
@@ -103,23 +103,47 @@ class LoggerFactory:
103
103
 
104
104
  # Set up root logger
105
105
  root_logger = logging.getLogger()
106
- root_logger.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
107
-
108
- # Remove existing handlers
109
- root_logger.handlers = []
110
-
111
- # Console handler - MUST use stderr to avoid corrupting hook JSON output
112
- # WHY stderr: Hook handlers output JSON to stdout. Logging to stdout
113
- # corrupts this JSON and causes "hook error" messages from Claude Code.
114
- console_handler = logging.StreamHandler(sys.stderr)
115
- console_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
116
- console_formatter = logging.Formatter(
117
- log_format or LoggingConfig.DEFAULT_FORMAT,
118
- date_format or LoggingConfig.DATE_FORMAT,
106
+
107
+ # CRITICAL FIX: Respect existing root logger suppression
108
+ # If root logger is already set to CRITICAL+1 (suppressed by startup.py),
109
+ # don't override it. This prevents logging from appearing during startup
110
+ # before the CLI's setup_logging() runs.
111
+ current_level = root_logger.level
112
+ desired_level = LoggingConfig.LEVELS.get(cls._log_level, logging.INFO)
113
+
114
+ # Only set level if current is unset (0) or lower than desired
115
+ # CRITICAL+1 is 51, so this check preserves suppression
116
+ should_configure_logging = current_level == 0 or (
117
+ current_level < desired_level and current_level <= logging.CRITICAL
119
118
  )
120
- console_handler.setFormatter(console_formatter)
121
- root_logger.addHandler(console_handler)
122
- cls._handlers["console"] = console_handler
119
+
120
+ if should_configure_logging:
121
+ root_logger.setLevel(desired_level)
122
+ # else: root logger is suppressed (CRITICAL+1), keep it suppressed
123
+
124
+ # Preserve FileHandlers (e.g., hooks logging), only remove StreamHandlers
125
+ root_logger.handlers = [
126
+ h for h in root_logger.handlers if isinstance(h, logging.FileHandler)
127
+ ]
128
+
129
+ # CRITICAL FIX: Don't add handlers if logging is suppressed
130
+ # If root logger is at CRITICAL+1 (startup suppression), don't add any handlers
131
+ # This prevents early imports from logging before CLI setup_logging() runs
132
+ if should_configure_logging:
133
+ # Console handler - MUST use stderr to avoid corrupting hook JSON output
134
+ # WHY stderr: Hook handlers output JSON to stdout. Logging to stdout
135
+ # corrupts this JSON and causes "hook error" messages from Claude Code.
136
+ console_handler = logging.StreamHandler(sys.stderr)
137
+ console_handler.setLevel(
138
+ LoggingConfig.LEVELS.get(cls._log_level, logging.INFO)
139
+ )
140
+ console_formatter = logging.Formatter(
141
+ log_format or LoggingConfig.DEFAULT_FORMAT,
142
+ date_format or LoggingConfig.DATE_FORMAT,
143
+ )
144
+ console_handler.setFormatter(console_formatter)
145
+ root_logger.addHandler(console_handler)
146
+ cls._handlers["console"] = console_handler
123
147
 
124
148
  # File handler (optional)
125
149
  if log_to_file and cls._log_dir: