claude-mpm 5.6.10__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 (117) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +174 -4
  3. claude_mpm/cli/parsers/commander_parser.py +43 -10
  4. claude_mpm/cli/startup.py +140 -20
  5. claude_mpm/cli/startup_display.py +2 -1
  6. claude_mpm/commander/__init__.py +6 -0
  7. claude_mpm/commander/adapters/__init__.py +32 -3
  8. claude_mpm/commander/adapters/auggie.py +260 -0
  9. claude_mpm/commander/adapters/base.py +98 -1
  10. claude_mpm/commander/adapters/claude_code.py +32 -1
  11. claude_mpm/commander/adapters/codex.py +237 -0
  12. claude_mpm/commander/adapters/example_usage.py +310 -0
  13. claude_mpm/commander/adapters/mpm.py +389 -0
  14. claude_mpm/commander/adapters/registry.py +204 -0
  15. claude_mpm/commander/api/app.py +32 -16
  16. claude_mpm/commander/api/routes/messages.py +11 -11
  17. claude_mpm/commander/api/routes/projects.py +20 -20
  18. claude_mpm/commander/api/routes/sessions.py +19 -21
  19. claude_mpm/commander/api/routes/work.py +86 -50
  20. claude_mpm/commander/api/schemas.py +4 -0
  21. claude_mpm/commander/chat/cli.py +42 -3
  22. claude_mpm/commander/config.py +5 -3
  23. claude_mpm/commander/core/__init__.py +10 -0
  24. claude_mpm/commander/core/block_manager.py +325 -0
  25. claude_mpm/commander/core/response_manager.py +323 -0
  26. claude_mpm/commander/daemon.py +215 -10
  27. claude_mpm/commander/env_loader.py +59 -0
  28. claude_mpm/commander/frameworks/base.py +4 -1
  29. claude_mpm/commander/instance_manager.py +124 -11
  30. claude_mpm/commander/memory/__init__.py +45 -0
  31. claude_mpm/commander/memory/compression.py +347 -0
  32. claude_mpm/commander/memory/embeddings.py +230 -0
  33. claude_mpm/commander/memory/entities.py +310 -0
  34. claude_mpm/commander/memory/example_usage.py +290 -0
  35. claude_mpm/commander/memory/integration.py +325 -0
  36. claude_mpm/commander/memory/search.py +381 -0
  37. claude_mpm/commander/memory/store.py +657 -0
  38. claude_mpm/commander/registry.py +10 -4
  39. claude_mpm/commander/runtime/monitor.py +32 -2
  40. claude_mpm/commander/work/executor.py +38 -20
  41. claude_mpm/commander/workflow/event_handler.py +25 -3
  42. claude_mpm/core/claude_runner.py +152 -0
  43. claude_mpm/core/config.py +3 -3
  44. claude_mpm/core/config_constants.py +74 -9
  45. claude_mpm/core/constants.py +56 -12
  46. claude_mpm/core/interactive_session.py +5 -4
  47. claude_mpm/core/logging_utils.py +4 -2
  48. claude_mpm/core/network_config.py +148 -0
  49. claude_mpm/core/oneshot_session.py +7 -6
  50. claude_mpm/core/output_style_manager.py +37 -7
  51. claude_mpm/core/socketio_pool.py +13 -5
  52. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  53. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  58. claude_mpm/hooks/claude_hooks/event_handlers.py +284 -89
  59. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
  60. claude_mpm/hooks/claude_hooks/installer.py +90 -28
  61. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  62. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  63. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  64. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  71. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  72. claude_mpm/hooks/claude_hooks/services/container.py +310 -0
  73. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  74. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  75. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  76. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  77. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  78. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  79. claude_mpm/services/command_deployment_service.py +44 -26
  80. claude_mpm/services/hook_installer_service.py +77 -8
  81. claude_mpm/services/pm_skills_deployer.py +3 -2
  82. claude_mpm/skills/__init__.py +2 -1
  83. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  84. claude_mpm/skills/registry.py +295 -90
  85. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +5 -3
  86. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +91 -94
  87. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  88. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  89. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  90. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  91. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  92. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  97. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  98. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  99. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  100. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  101. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  102. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  103. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  104. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  105. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  106. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  107. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  108. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  109. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  110. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  111. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  112. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  113. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
  114. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
  115. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
  116. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  117. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/top_level.txt +0 -0
@@ -6,13 +6,16 @@ and detects events using OutputParser.
6
6
 
7
7
  import asyncio
8
8
  import logging
9
- from typing import Dict, List, Optional
9
+ from typing import TYPE_CHECKING, Dict, List, Optional
10
10
 
11
11
  from ..events.manager import EventManager
12
12
  from ..models.events import Event
13
13
  from ..parsing.output_parser import OutputParser
14
14
  from ..tmux_orchestrator import TmuxOrchestrator
15
15
 
16
+ if TYPE_CHECKING:
17
+ from ..core.block_manager import BlockManager
18
+
16
19
  logger = logging.getLogger(__name__)
17
20
 
18
21
 
@@ -44,6 +47,7 @@ class RuntimeMonitor:
44
47
  event_manager: EventManager,
45
48
  poll_interval: float = 2.0,
46
49
  capture_lines: int = 1000,
50
+ block_manager: Optional["BlockManager"] = None,
47
51
  ):
48
52
  """Initialize runtime monitor.
49
53
 
@@ -53,6 +57,7 @@ class RuntimeMonitor:
53
57
  event_manager: EventManager for emitting events
54
58
  poll_interval: Seconds between polls (default: 2.0)
55
59
  capture_lines: Number of lines to capture (default: 1000)
60
+ block_manager: Optional BlockManager for automatic work blocking
56
61
 
57
62
  Raises:
58
63
  ValueError: If any required parameter is None
@@ -69,15 +74,17 @@ class RuntimeMonitor:
69
74
  self.event_manager = event_manager
70
75
  self.poll_interval = poll_interval
71
76
  self.capture_lines = capture_lines
77
+ self.block_manager = block_manager
72
78
 
73
79
  # Track active monitors: pane_target -> (project_id, task, last_output_hash)
74
80
  self._monitors: Dict[str, tuple[str, Optional[asyncio.Task], int]] = {}
75
81
  self._running = False
76
82
 
77
83
  logger.debug(
78
- "RuntimeMonitor initialized (interval: %.2fs, lines: %d)",
84
+ "RuntimeMonitor initialized (interval: %.2fs, lines: %d, block_manager: %s)",
79
85
  poll_interval,
80
86
  capture_lines,
87
+ "enabled" if block_manager else "disabled",
81
88
  )
82
89
 
83
90
  async def start_monitoring(self, pane_target: str, project_id: str) -> None:
@@ -284,6 +291,29 @@ class RuntimeMonitor:
284
291
  pane_target,
285
292
  )
286
293
 
294
+ # Automatically block work for blocking events
295
+ if self.block_manager:
296
+ for parse_result in parse_results:
297
+ # Get the created event from EventManager
298
+ # Events are created with matching titles, so find by title
299
+ pending_events = self.event_manager.get_pending(project_id)
300
+ for event in pending_events:
301
+ if (
302
+ event.title == parse_result.title
303
+ and event.is_blocking
304
+ ):
305
+ blocked_work = (
306
+ await self.block_manager.check_and_block(event)
307
+ )
308
+ if blocked_work:
309
+ logger.info(
310
+ "Event %s blocked %d work items: %s",
311
+ event.id,
312
+ len(blocked_work),
313
+ blocked_work,
314
+ )
315
+ break
316
+
287
317
  except Exception as e:
288
318
  logger.error(
289
319
  "Error in monitor loop for pane %s: %s",
@@ -51,16 +51,19 @@ class WorkExecutor:
51
51
 
52
52
  logger.debug(f"Initialized WorkExecutor for project {queue.project_id}")
53
53
 
54
- async def execute_next(self) -> bool:
54
+ async def execute_next(self, pane_target: Optional[str] = None) -> bool:
55
55
  """Execute next available work item.
56
56
 
57
57
  Gets next work from queue, starts it, and executes via RuntimeExecutor.
58
58
 
59
+ Args:
60
+ pane_target: Optional tmux pane target for execution
61
+
59
62
  Returns:
60
63
  True if work was executed, False if queue empty/blocked
61
64
 
62
65
  Example:
63
- >>> executed = await executor.execute_next()
66
+ >>> executed = await executor.execute_next("%5")
64
67
  >>> if not executed:
65
68
  ... print("No work available")
66
69
  """
@@ -71,10 +74,12 @@ class WorkExecutor:
71
74
  return False
72
75
 
73
76
  # Execute the work item
74
- await self.execute(work_item)
77
+ await self.execute(work_item, pane_target)
75
78
  return True
76
79
 
77
- async def execute(self, work_item: WorkItem) -> None:
80
+ async def execute(
81
+ self, work_item: WorkItem, pane_target: Optional[str] = None
82
+ ) -> None:
78
83
  """Execute a specific work item.
79
84
 
80
85
  Marks work as IN_PROGRESS and sends to RuntimeExecutor.
@@ -83,12 +88,13 @@ class WorkExecutor:
83
88
 
84
89
  Args:
85
90
  work_item: WorkItem to execute
91
+ pane_target: Optional tmux pane target for execution
86
92
 
87
93
  Raises:
88
94
  RuntimeError: If execution fails
89
95
 
90
96
  Example:
91
- >>> await executor.execute(work_item)
97
+ >>> await executor.execute(work_item, "%5")
92
98
  """
93
99
  # Mark as in progress
94
100
  if not self.queue.start(work_item.id):
@@ -103,17 +109,21 @@ class WorkExecutor:
103
109
  )
104
110
 
105
111
  try:
106
- # Send work content to runtime
107
- # Note: In actual implementation, this would integrate with
108
- # ProjectSession which manages the pane_target
109
- # For now, we assume runtime has active session
110
- # This will be properly integrated when wiring with ProjectSession
112
+ # Send work content to runtime if pane target provided
113
+ if pane_target:
114
+ await self.runtime.send_message(pane_target, work_item.content)
115
+ logger.info(
116
+ f"Work item {work_item.id} sent to pane {pane_target} for execution"
117
+ )
118
+ else:
119
+ logger.warning(
120
+ f"No pane target provided for work item {work_item.id}, "
121
+ f"work marked as in-progress but not sent to runtime"
122
+ )
111
123
 
112
124
  # Store work item ID in metadata for callback tracking
113
125
  work_item.metadata["execution_started"] = True
114
126
 
115
- logger.info(f"Work item {work_item.id} sent to runtime for execution")
116
-
117
127
  except Exception as e:
118
128
  logger.error(f"Failed to execute work item {work_item.id}: {e}")
119
129
  await self.handle_failure(work_item.id, str(e))
@@ -155,7 +165,7 @@ class WorkExecutor:
155
165
  else:
156
166
  logger.warning(f"Failed to mark work item {work_id} as failed")
157
167
 
158
- async def handle_block(self, work_id: str, reason: str) -> None:
168
+ async def handle_block(self, work_id: str, reason: str) -> bool:
159
169
  """Handle work being blocked by an event.
160
170
 
161
171
  Called when RuntimeMonitor detects a blocking event.
@@ -164,15 +174,19 @@ class WorkExecutor:
164
174
  work_id: Work item ID that is blocked
165
175
  reason: Reason for blocking (e.g., "Waiting for approval")
166
176
 
177
+ Returns:
178
+ True if work was successfully blocked, False otherwise
179
+
167
180
  Example:
168
- >>> await executor.handle_block("work-123", "Decision needed")
181
+ >>> success = await executor.handle_block("work-123", "Decision needed")
169
182
  """
170
183
  if self.queue.block(work_id, reason):
171
184
  logger.info(f"Work item {work_id} blocked: {reason}")
172
- else:
173
- logger.warning(f"Failed to mark work item {work_id} as blocked")
185
+ return True
186
+ logger.warning(f"Failed to mark work item {work_id} as blocked")
187
+ return False
174
188
 
175
- async def handle_unblock(self, work_id: str) -> None:
189
+ async def handle_unblock(self, work_id: str) -> bool:
176
190
  """Handle work being unblocked after event resolution.
177
191
 
178
192
  Called when EventHandler resolves a blocking event.
@@ -180,10 +194,14 @@ class WorkExecutor:
180
194
  Args:
181
195
  work_id: Work item ID to unblock
182
196
 
197
+ Returns:
198
+ True if work was successfully unblocked, False otherwise
199
+
183
200
  Example:
184
- >>> await executor.handle_unblock("work-123")
201
+ >>> success = await executor.handle_unblock("work-123")
185
202
  """
186
203
  if self.queue.unblock(work_id):
187
204
  logger.info(f"Work item {work_id} unblocked, resuming execution")
188
- else:
189
- logger.warning(f"Failed to unblock work item {work_id}")
205
+ return True
206
+ logger.warning(f"Failed to unblock work item {work_id}")
207
+ return False
@@ -5,12 +5,15 @@ user input and coordinates session pause/resume.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Dict, List, Optional
8
+ from typing import TYPE_CHECKING, Dict, List, Optional
9
9
 
10
10
  from ..inbox import Inbox
11
11
  from ..models.events import BLOCKING_EVENTS, Event, EventStatus
12
12
  from ..project_session import ProjectSession
13
13
 
14
+ if TYPE_CHECKING:
15
+ from ..core.block_manager import BlockManager
16
+
14
17
  logger = logging.getLogger(__name__)
15
18
 
16
19
 
@@ -32,13 +35,17 @@ class EventHandler:
32
35
  """
33
36
 
34
37
  def __init__(
35
- self, inbox: Inbox, session_manager: Dict[str, ProjectSession]
38
+ self,
39
+ inbox: Inbox,
40
+ session_manager: Dict[str, ProjectSession],
41
+ block_manager: Optional["BlockManager"] = None,
36
42
  ) -> None:
37
43
  """Initialize event handler.
38
44
 
39
45
  Args:
40
46
  inbox: Inbox instance for event access
41
47
  session_manager: Dict mapping project_id -> ProjectSession
48
+ block_manager: Optional BlockManager for automatic work unblocking
42
49
 
43
50
  Raises:
44
51
  ValueError: If inbox or session_manager is None
@@ -51,8 +58,12 @@ class EventHandler:
51
58
  self.inbox = inbox
52
59
  self.session_manager = session_manager
53
60
  self._event_manager = inbox.events
61
+ self.block_manager = block_manager
54
62
 
55
- logger.debug("EventHandler initialized")
63
+ logger.debug(
64
+ "EventHandler initialized (block_manager: %s)",
65
+ "enabled" if block_manager else "disabled",
66
+ )
56
67
 
57
68
  async def process_event(self, event: Event) -> None:
58
69
  """Process an event - pause session if blocking.
@@ -137,6 +148,17 @@ class EventHandler:
137
148
  # Mark event as resolved
138
149
  self._event_manager.respond(event_id, response)
139
150
 
151
+ # Automatically unblock work items if BlockManager is available
152
+ if self.block_manager and was_blocking:
153
+ unblocked_work = await self.block_manager.check_and_unblock(event_id)
154
+ if unblocked_work:
155
+ logger.info(
156
+ "Event %s resolution unblocked %d work items: %s",
157
+ event_id,
158
+ len(unblocked_work),
159
+ unblocked_work,
160
+ )
161
+
140
162
  # If event was NOT blocking, no need to resume
141
163
  if not was_blocking:
142
164
  logger.debug("Event %s was non-blocking, no resume needed", event_id)
@@ -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
@@ -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
@@ -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")