claude-mpm 5.6.17__py3-none-any.whl → 5.6.33__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (84) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +7 -7
  3. claude_mpm/cli/parsers/commander_parser.py +2 -2
  4. claude_mpm/cli/startup.py +36 -19
  5. claude_mpm/commander/chat/cli.py +38 -3
  6. claude_mpm/commander/config.py +5 -3
  7. claude_mpm/commander/daemon.py +9 -0
  8. claude_mpm/commander/frameworks/base.py +4 -1
  9. claude_mpm/commander/instance_manager.py +124 -11
  10. claude_mpm/core/claude_runner.py +22 -13
  11. claude_mpm/core/config.py +3 -3
  12. claude_mpm/core/config_constants.py +74 -9
  13. claude_mpm/core/constants.py +56 -12
  14. claude_mpm/core/interactive_session.py +5 -4
  15. claude_mpm/core/logging_utils.py +4 -2
  16. claude_mpm/core/network_config.py +148 -0
  17. claude_mpm/core/oneshot_session.py +7 -6
  18. claude_mpm/core/output_style_manager.py +5 -2
  19. claude_mpm/core/socketio_pool.py +13 -5
  20. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  26. claude_mpm/hooks/claude_hooks/event_handlers.py +262 -89
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
  28. claude_mpm/hooks/claude_hooks/installer.py +90 -28
  29. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  30. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  31. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  33. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  34. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  35. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  36. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  37. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  38. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  39. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  40. claude_mpm/hooks/claude_hooks/services/container.py +310 -0
  41. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  42. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  43. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  44. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  45. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  46. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  47. claude_mpm/scripts/start_activity_logging.py +0 -0
  48. claude_mpm/services/command_deployment_service.py +44 -26
  49. claude_mpm/services/hook_installer_service.py +77 -8
  50. claude_mpm/services/pm_skills_deployer.py +3 -2
  51. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +1 -1
  52. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +56 -78
  53. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  76. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  80. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
  81. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
  82. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
  83. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  84. {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION CHANGED
@@ -1 +1 @@
1
- 5.6.17
1
+ 5.6.33
@@ -43,12 +43,12 @@ def display_commander_banner():
43
43
 
44
44
  # Commander ASCII art banner
45
45
  banner = f"""
46
- {CYAN}╭{'' * (width - 2)}╮{RESET}
47
- {CYAN}│{RESET}{BOLD} ⚡ MPM Commander {RESET}{DIM}v{version}{RESET}{' ' * (width - 24 - len(version))}│
48
- {CYAN}│{RESET}{DIM} Multi-Project AI Orchestration{RESET}{' ' * (width - 36)}│
49
- {CYAN}├{'' * (width - 2)}┤{RESET}
50
- {CYAN}│{RESET} {YELLOW}ALPHA{RESET} - APIs may change {' ' * (width - 55)}│
51
- {CYAN}╰{'' * (width - 2)}╯{RESET}
46
+ {CYAN}╭{"" * (width - 2)}╮{RESET}
47
+ {CYAN}│{RESET}{BOLD} ⚡ MPM Commander {RESET}{DIM}v{version}{RESET}{" " * (width - 24 - len(version))}│
48
+ {CYAN}│{RESET}{DIM} Multi-Project AI Orchestration{RESET}{" " * (width - 36)}│
49
+ {CYAN}├{"" * (width - 2)}┤{RESET}
50
+ {CYAN}│{RESET} {YELLOW}ALPHA{RESET} - APIs may change {" " * (width - 55)}│
51
+ {CYAN}╰{"" * (width - 2)}╯{RESET}
52
52
  """
53
53
  print(banner)
54
54
 
@@ -142,7 +142,7 @@ def handle_commander_command(args) -> int:
142
142
  print() # Blank line after loading
143
143
 
144
144
  # Get arguments
145
- port = getattr(args, "port", 8765)
145
+ port = getattr(args, "port", 8766) # NetworkPorts.COMMANDER_DEFAULT
146
146
  host = getattr(args, "host", "127.0.0.1")
147
147
  state_dir = getattr(args, "state_dir", None)
148
148
  no_chat = getattr(args, "no_chat", False) or getattr(args, "daemon_only", False)
@@ -77,8 +77,8 @@ Examples:
77
77
  commander_parser.add_argument(
78
78
  "--port",
79
79
  type=int,
80
- default=8765,
81
- help="Port for internal services (default: 8765)",
80
+ default=8766, # NetworkPorts.COMMANDER_DEFAULT
81
+ help="Port for internal services (default: 8766)",
82
82
  )
83
83
 
84
84
  # Optional: State directory
claude_mpm/cli/startup.py CHANGED
@@ -34,13 +34,13 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
34
34
  installer = HookInstaller()
35
35
 
36
36
  # Show brief status (hooks sync is fast)
37
- if not quiet:
37
+ if not quiet and sys.stdout.isatty():
38
38
  print("Syncing Claude Code hooks...", end=" ", flush=True)
39
39
 
40
40
  # Reinstall hooks (force=True ensures update)
41
41
  success = installer.install_hooks(force=True)
42
42
 
43
- if not quiet:
43
+ if not quiet and sys.stdout.isatty():
44
44
  if success:
45
45
  print("✓")
46
46
  else:
@@ -49,7 +49,7 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
49
49
  return success
50
50
 
51
51
  except Exception as e:
52
- if not quiet:
52
+ if not quiet and sys.stdout.isatty():
53
53
  print("(error)")
54
54
  # Log but don't fail startup
55
55
  from ..core.logger import get_logger
@@ -280,11 +280,13 @@ def deploy_bundled_skills():
280
280
  if deployment_result.get("deployed"):
281
281
  # Show simple feedback for deployed skills
282
282
  deployed_count = len(deployment_result["deployed"])
283
- print(f"✓ Bundled skills ready ({deployed_count} deployed)", flush=True)
283
+ if sys.stdout.isatty():
284
+ print(f"✓ Bundled skills ready ({deployed_count} deployed)", flush=True)
284
285
  logger.info(f"Skills: Deployed {deployed_count} skill(s)")
285
286
  elif not deployment_result.get("errors"):
286
287
  # No deployment needed, skills already present
287
- print("✓ Bundled skills ready", flush=True)
288
+ if sys.stdout.isatty():
289
+ print("✓ Bundled skills ready", flush=True)
288
290
 
289
291
  if deployment_result.get("errors"):
290
292
  logger.warning(
@@ -318,7 +320,8 @@ def discover_and_link_runtime_skills():
318
320
 
319
321
  discover_skills()
320
322
  # Show simple success feedback
321
- print("✓ Runtime skills linked", flush=True)
323
+ if sys.stdout.isatty():
324
+ print("✓ Runtime skills linked", flush=True)
322
325
  except Exception as e:
323
326
  # Import logger here to avoid circular imports
324
327
  from ..core.logger import get_logger
@@ -373,7 +376,8 @@ def deploy_output_style_on_startup():
373
376
 
374
377
  if all_up_to_date:
375
378
  # Show feedback that output styles are ready
376
- print("✓ Output styles ready", flush=True)
379
+ if sys.stdout.isatty():
380
+ print("✓ Output styles ready", flush=True)
377
381
  return
378
382
 
379
383
  # Deploy all styles using the manager
@@ -383,7 +387,8 @@ def deploy_output_style_on_startup():
383
387
  deployed_count = sum(1 for success in results.values() if success)
384
388
 
385
389
  if deployed_count > 0:
386
- print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
390
+ if sys.stdout.isatty():
391
+ print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
387
392
  else:
388
393
  # Deployment failed - log but don't fail startup
389
394
  from ..core.logger import get_logger
@@ -1290,9 +1295,11 @@ def verify_and_show_pm_skills():
1290
1295
  if result.verified:
1291
1296
  # Show verified status with count
1292
1297
  total_required = len(REQUIRED_PM_SKILLS)
1293
- print(
1294
- f"✓ PM skills: {total_required}/{total_required} verified", flush=True
1295
- )
1298
+ if sys.stdout.isatty():
1299
+ print(
1300
+ f"✓ PM skills: {total_required}/{total_required} verified",
1301
+ flush=True,
1302
+ )
1296
1303
  else:
1297
1304
  # Show warning with details
1298
1305
  missing_count = len(result.missing_skills)
@@ -1311,13 +1318,15 @@ def verify_and_show_pm_skills():
1311
1318
  if "Auto-repaired" in result.message:
1312
1319
  # Auto-repair succeeded
1313
1320
  total_required = len(REQUIRED_PM_SKILLS)
1314
- print(
1315
- f"✓ PM skills: {total_required}/{total_required} verified (auto-repaired)",
1316
- flush=True,
1317
- )
1321
+ if sys.stdout.isatty():
1322
+ print(
1323
+ f"✓ PM skills: {total_required}/{total_required} verified (auto-repaired)",
1324
+ flush=True,
1325
+ )
1318
1326
  else:
1319
1327
  # Auto-repair failed or not attempted
1320
- print(f"⚠ PM skills: {status}", flush=True)
1328
+ if sys.stdout.isatty():
1329
+ print(f"⚠ PM skills: {status}", flush=True)
1321
1330
 
1322
1331
  # Log warnings for debugging
1323
1332
  from ..core.logger import get_logger
@@ -1516,7 +1525,9 @@ def check_mcp_auto_configuration():
1516
1525
  from ..services.mcp_gateway.auto_configure import check_and_configure_mcp
1517
1526
 
1518
1527
  # Show progress feedback - this operation can take 10+ seconds
1519
- print("Checking MCP configuration...", end="", flush=True)
1528
+ # Only show progress message in TTY mode to avoid interfering with Claude Code's status display
1529
+ if sys.stdout.isatty():
1530
+ print("Checking MCP configuration...", end="", flush=True)
1520
1531
 
1521
1532
  # This function handles all the logic:
1522
1533
  # - Checks if already configured
@@ -1527,11 +1538,17 @@ def check_mcp_auto_configuration():
1527
1538
  check_and_configure_mcp()
1528
1539
 
1529
1540
  # Clear the "Checking..." message by overwriting with spaces
1530
- print("\r" + " " * 30 + "\r", end="", flush=True)
1541
+ # Only use carriage return clearing if stdout is a real TTY
1542
+ if sys.stdout.isatty():
1543
+ print("\r" + " " * 30 + "\r", end="", flush=True)
1544
+ # In non-TTY mode, don't print anything - the "Checking..." message will just remain on its line
1531
1545
 
1532
1546
  except Exception as e:
1533
1547
  # Clear progress message on error
1534
- print("\r" + " " * 30 + "\r", end="", flush=True)
1548
+ # Only use carriage return clearing if stdout is a real TTY
1549
+ if sys.stdout.isatty():
1550
+ print("\r" + " " * 30 + "\r", end="", flush=True)
1551
+ # In non-TTY mode, don't print anything - the "Checking..." message will just remain on its line
1535
1552
 
1536
1553
  # Non-critical - log but don't fail
1537
1554
  from ..core.logger import get_logger
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ from dataclasses import dataclass
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
@@ -26,20 +27,47 @@ load_env()
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
 
30
+ @dataclass
31
+ class CommanderCLIConfig:
32
+ """Configuration for Commander CLI mode.
33
+
34
+ Attributes:
35
+ summarize_responses: Whether to use LLM to summarize instance responses
36
+ port: Port for internal services (reserved for future use)
37
+ state_dir: Directory for state persistence (optional)
38
+
39
+ Example:
40
+ >>> config = CommanderCLIConfig(summarize_responses=False)
41
+ """
42
+
43
+ summarize_responses: bool = True
44
+ port: int = 8765
45
+ state_dir: Optional[Path] = None
46
+
47
+
29
48
  async def run_commander(
30
49
  port: int = 8765,
31
50
  state_dir: Optional[Path] = None,
51
+ config: Optional[CommanderCLIConfig] = None,
32
52
  ) -> None:
33
53
  """Run Commander in interactive mode.
34
54
 
35
55
  Args:
36
56
  port: Port for internal services (unused currently).
37
57
  state_dir: Directory for state persistence (optional).
58
+ config: Commander CLI configuration (optional, uses defaults if None).
38
59
 
39
60
  Example:
40
61
  >>> asyncio.run(run_commander())
41
62
  # Starts interactive Commander REPL
63
+ >>> config = CommanderCLIConfig(summarize_responses=False)
64
+ >>> asyncio.run(run_commander(config=config))
65
+ # Starts Commander without response summarization
42
66
  """
67
+ # Use default config if not provided
68
+ if config is None:
69
+ config = CommanderCLIConfig(port=port, state_dir=state_dir)
70
+
43
71
  # Setup logging
44
72
  logging.basicConfig(
45
73
  level=logging.INFO,
@@ -61,8 +89,8 @@ async def run_commander(
61
89
  # Try to initialize LLM client (optional)
62
90
  llm_client: Optional[OpenRouterClient] = None
63
91
  try:
64
- config = OpenRouterConfig()
65
- llm_client = OpenRouterClient(config)
92
+ llm_config = OpenRouterConfig()
93
+ llm_client = OpenRouterClient(llm_config)
66
94
  logger.info("LLM client initialized")
67
95
  except ValueError as e:
68
96
  logger.warning(f"LLM client not available: {e}")
@@ -72,7 +100,14 @@ async def run_commander(
72
100
  output_relay: Optional[OutputRelay] = None
73
101
  if llm_client:
74
102
  try:
75
- summarizer = OutputSummarizer(llm_client)
103
+ # Only create summarizer if summarize_responses is enabled
104
+ summarizer = None
105
+ if config.summarize_responses:
106
+ summarizer = OutputSummarizer(llm_client)
107
+ logger.info("Response summarization enabled")
108
+ else:
109
+ logger.info("Response summarization disabled")
110
+
76
111
  handler = OutputHandler(orchestrator, summarizer)
77
112
  formatter = OutputFormatter()
78
113
  output_relay = OutputRelay(handler, formatter)
@@ -14,28 +14,30 @@ class DaemonConfig:
14
14
 
15
15
  Attributes:
16
16
  host: API server bind address
17
- port: API server port
17
+ port: API server port (default: 8766 from NetworkPorts.COMMANDER_DEFAULT)
18
18
  log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
19
19
  state_dir: Directory for state persistence
20
20
  max_projects: Maximum concurrent projects
21
21
  healthcheck_interval: Healthcheck interval in seconds
22
22
  save_interval: State persistence interval in seconds
23
23
  poll_interval: Event polling interval in seconds
24
+ summarize_responses: Whether to use LLM to summarize instance responses
24
25
 
25
26
  Example:
26
- >>> config = DaemonConfig(port=8765, log_level="DEBUG")
27
+ >>> config = DaemonConfig(port=8766, log_level="DEBUG")
27
28
  >>> config.state_dir
28
29
  PosixPath('/Users/user/.claude-mpm/commander')
29
30
  """
30
31
 
31
32
  host: str = "127.0.0.1"
32
- port: int = 8765
33
+ port: int = 8766 # Default commander port (from network_config.NetworkPorts.COMMANDER_DEFAULT)
33
34
  log_level: str = "INFO"
34
35
  state_dir: Path = Path.home() / ".claude-mpm" / "commander"
35
36
  max_projects: int = 10
36
37
  healthcheck_interval: int = 30
37
38
  save_interval: int = 30
38
39
  poll_interval: float = 2.0
40
+ summarize_responses: bool = True
39
41
 
40
42
  def __post_init__(self):
41
43
  """Ensure state_dir is a Path object and create if needed."""
@@ -307,7 +307,16 @@ class CommanderDaemon:
307
307
 
308
308
  Registers handlers for SIGINT and SIGTERM that trigger
309
309
  daemon shutdown via asyncio event loop.
310
+
311
+ Note: Signal handlers can only be registered from the main thread.
312
+ If called from a background thread, registration is skipped.
310
313
  """
314
+ import threading
315
+
316
+ # Signal handlers can only be registered from the main thread
317
+ if threading.current_thread() is not threading.main_thread():
318
+ logger.info("Running in background thread - signal handlers skipped")
319
+ return
311
320
 
312
321
  def handle_signal(signum: int, frame) -> None:
313
322
  """Handle shutdown signal.
@@ -19,6 +19,7 @@ class InstanceInfo:
19
19
  pane_target: Tmux pane target (e.g., "%1")
20
20
  git_branch: Current git branch if project is a git repo
21
21
  git_status: Git status summary if project is a git repo
22
+ connected: Whether instance has an active adapter connection
22
23
 
23
24
  Example:
24
25
  >>> info = InstanceInfo(
@@ -28,7 +29,8 @@ class InstanceInfo:
28
29
  ... tmux_session="mpm-commander",
29
30
  ... pane_target="%1",
30
31
  ... git_branch="main",
31
- ... git_status="clean"
32
+ ... git_status="clean",
33
+ ... connected=True
32
34
  ... )
33
35
  """
34
36
 
@@ -39,6 +41,7 @@ class InstanceInfo:
39
41
  pane_target: str
40
42
  git_branch: Optional[str] = None
41
43
  git_status: Optional[str] = None
44
+ connected: bool = False
42
45
 
43
46
 
44
47
  class BaseFramework(ABC):
@@ -167,6 +167,20 @@ class InstanceManager:
167
167
  startup_cmd = framework_obj.get_startup_command(project_path)
168
168
  self.orchestrator.send_keys(pane_target, startup_cmd)
169
169
 
170
+ # Create communication adapter for the instance (only for Claude Code for now)
171
+ # Do this BEFORE creating InstanceInfo so we can set connected=True
172
+ has_adapter = False
173
+ if framework == "cc":
174
+ runtime_adapter = ClaudeCodeAdapter()
175
+ comm_adapter = ClaudeCodeCommunicationAdapter(
176
+ orchestrator=self.orchestrator,
177
+ pane_target=pane_target,
178
+ runtime_adapter=runtime_adapter,
179
+ )
180
+ self._adapters[name] = comm_adapter
181
+ has_adapter = True
182
+ logger.debug(f"Created communication adapter for instance '{name}'")
183
+
170
184
  # Create instance info
171
185
  instance = InstanceInfo(
172
186
  name=name,
@@ -176,22 +190,12 @@ class InstanceManager:
176
190
  pane_target=pane_target,
177
191
  git_branch=git_branch,
178
192
  git_status=git_status,
193
+ connected=has_adapter,
179
194
  )
180
195
 
181
196
  # Track instance
182
197
  self._instances[name] = instance
183
198
 
184
- # Create communication adapter for the instance (only for Claude Code for now)
185
- if framework == "cc":
186
- runtime_adapter = ClaudeCodeAdapter()
187
- comm_adapter = ClaudeCodeCommunicationAdapter(
188
- orchestrator=self.orchestrator,
189
- pane_target=pane_target,
190
- runtime_adapter=runtime_adapter,
191
- )
192
- self._adapters[name] = comm_adapter
193
- logger.debug(f"Created communication adapter for instance '{name}'")
194
-
195
199
  logger.info(
196
200
  f"Started instance '{name}' with framework '{framework}' at {project_path}"
197
201
  )
@@ -226,6 +230,7 @@ class InstanceManager:
226
230
  # Remove adapter if exists
227
231
  if name in self._adapters:
228
232
  del self._adapters[name]
233
+ instance.connected = False
229
234
  logger.debug(f"Removed adapter for instance '{name}'")
230
235
 
231
236
  # Remove from tracking
@@ -335,3 +340,111 @@ class InstanceManager:
335
340
  ... print(chunk, end='')
336
341
  """
337
342
  return self._adapters.get(name)
343
+
344
+ async def rename_instance(self, old_name: str, new_name: str) -> bool:
345
+ """Rename an instance.
346
+
347
+ Args:
348
+ old_name: Current instance name
349
+ new_name: New instance name
350
+
351
+ Returns:
352
+ True if renamed successfully
353
+
354
+ Raises:
355
+ InstanceNotFoundError: If old_name doesn't exist
356
+ InstanceAlreadyExistsError: If new_name already exists
357
+
358
+ Example:
359
+ >>> manager = InstanceManager(orchestrator)
360
+ >>> await manager.rename_instance("myapp", "myapp-v2")
361
+ True
362
+ """
363
+ # Validate old_name exists
364
+ if old_name not in self._instances:
365
+ raise InstanceNotFoundError(old_name)
366
+
367
+ # Validate new_name doesn't exist
368
+ if new_name in self._instances:
369
+ raise InstanceAlreadyExistsError(new_name)
370
+
371
+ # Get instance and update name
372
+ instance = self._instances[old_name]
373
+ instance.name = new_name
374
+
375
+ # Update _instances dict (remove old key, add new)
376
+ del self._instances[old_name]
377
+ self._instances[new_name] = instance
378
+
379
+ # Update _adapters dict if exists
380
+ if old_name in self._adapters:
381
+ adapter = self._adapters[old_name]
382
+ del self._adapters[old_name]
383
+ self._adapters[new_name] = adapter
384
+ logger.debug(f"Moved adapter from '{old_name}' to '{new_name}'")
385
+
386
+ logger.info(f"Renamed instance from '{old_name}' to '{new_name}'")
387
+
388
+ return True
389
+
390
+ async def close_instance(self, name: str) -> bool:
391
+ """Close and remove an instance.
392
+
393
+ Alias for stop_instance that provides clearer semantics for closing.
394
+
395
+ Args:
396
+ name: Instance name to close
397
+
398
+ Returns:
399
+ True if closed successfully
400
+
401
+ Raises:
402
+ InstanceNotFoundError: If instance not found
403
+
404
+ Example:
405
+ >>> manager = InstanceManager(orchestrator)
406
+ >>> await manager.close_instance("myapp")
407
+ True
408
+ """
409
+ return await self.stop_instance(name)
410
+
411
+ async def disconnect_instance(self, name: str) -> bool:
412
+ """Disconnect from an instance without closing it.
413
+
414
+ The instance keeps running but we stop communication.
415
+ Removes the adapter while keeping the instance tracked.
416
+
417
+ Args:
418
+ name: Instance name to disconnect from
419
+
420
+ Returns:
421
+ True if disconnected successfully
422
+
423
+ Raises:
424
+ InstanceNotFoundError: If instance not found
425
+
426
+ Example:
427
+ >>> manager = InstanceManager(orchestrator)
428
+ >>> await manager.disconnect_instance("myapp")
429
+ True
430
+ >>> # Instance still running, but no adapter connection
431
+ >>> adapter = manager.get_adapter("myapp")
432
+ >>> print(adapter)
433
+ None
434
+ """
435
+ # Validate instance exists
436
+ if name not in self._instances:
437
+ raise InstanceNotFoundError(name)
438
+
439
+ instance = self._instances[name]
440
+
441
+ # Remove adapter if exists (but keep instance)
442
+ if name in self._adapters:
443
+ # Could add cleanup here if adapter has resources to close
444
+ del self._adapters[name]
445
+ instance.connected = False
446
+ logger.info(f"Disconnected from instance '{name}' (instance still running)")
447
+ else:
448
+ logger.debug(f"No adapter to disconnect for instance '{name}'")
449
+
450
+ return True
@@ -214,8 +214,12 @@ class ClaudeRunner:
214
214
  )
215
215
 
216
216
  def _get_deployment_state_path(self) -> Path:
217
- """Get path to deployment state file."""
218
- return Path.cwd() / ".claude-mpm" / "deployment-state.json"
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"
219
223
 
220
224
  def _calculate_deployment_hash(self, agents_dir: Path) -> str:
221
225
  """Calculate hash of all agent files for change detection.
@@ -331,18 +335,23 @@ class ClaudeRunner:
331
335
  def setup_agents(self) -> bool:
332
336
  """Deploy native agents to .claude/agents/."""
333
337
  try:
334
- # Check if agents are already deployed and up-to-date
335
- if self._check_deployment_state():
336
- agents_dir = Path.cwd() / ".claude" / "agents"
337
- agent_count = len(list(agents_dir.glob("*.md")))
338
- print(f"✓ Agents: {agent_count} cached")
339
- if self.project_logger:
340
- self.project_logger.log_system(
341
- f"Agents already deployed: {agent_count} cached",
342
- level="INFO",
343
- component="deployment",
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"
344
347
  )
345
- return True
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
346
355
 
347
356
  if self.project_logger:
348
357
  self.project_logger.log_system(
claude_mpm/core/config.py CHANGED
@@ -499,7 +499,7 @@ class Config:
499
499
  # Socket.IO server health and recovery configuration
500
500
  "socketio_server": {
501
501
  "host": "localhost",
502
- "port": 8765,
502
+ "port": 8768, # Default SocketIO port (from network_config.NetworkPorts)
503
503
  "enable_health_monitoring": True,
504
504
  "enable_recovery": True,
505
505
  "health_monitoring": {
@@ -540,7 +540,7 @@ class Config:
540
540
  # Monitor server configuration (decoupled from dashboard)
541
541
  "monitor_server": {
542
542
  "host": "localhost",
543
- "port": 8765, # Default monitor port (shared with dashboard)
543
+ "port": 8765, # Default monitor port (from network_config.NetworkPorts.MONITOR_DEFAULT)
544
544
  "enable_health_monitoring": True,
545
545
  "auto_start": False, # Don't auto-start with dashboard by default
546
546
  "event_buffer_size": 2000, # Larger buffer for monitor server
@@ -549,7 +549,7 @@ class Config:
549
549
  # Dashboard server configuration (connects to monitor)
550
550
  "dashboard_server": {
551
551
  "host": "localhost",
552
- "port": 8765, # Dashboard UI port
552
+ "port": 8767, # Dashboard UI port (from network_config.NetworkPorts.DASHBOARD_DEFAULT)
553
553
  "monitor_host": "localhost", # Monitor server host to connect to
554
554
  "monitor_port": 8765, # Monitor server port to connect to
555
555
  "auto_connect_monitor": True, # Automatically connect to monitor