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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/startup.py +140 -20
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +19 -21
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +42 -3
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +3 -3
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +13 -5
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +284 -89
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
- claude_mpm/hooks/claude_hooks/installer.py +90 -28
- claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
- claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
- claude_mpm/hooks/claude_hooks/services/container.py +310 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/pm_skills_deployer.py +3 -2
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +5 -3
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +91 -94
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {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(
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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) ->
|
|
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
|
-
|
|
173
|
-
|
|
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) ->
|
|
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
|
-
|
|
189
|
-
|
|
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,
|
|
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(
|
|
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)
|
claude_mpm/core/claude_runner.py
CHANGED
|
@@ -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":
|
|
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 (
|
|
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":
|
|
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
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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")
|