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
claude_mpm/commander/daemon.py
CHANGED
|
@@ -15,12 +15,23 @@ from .api.app import (
|
|
|
15
15
|
app,
|
|
16
16
|
)
|
|
17
17
|
from .config import DaemonConfig
|
|
18
|
+
from .core.block_manager import BlockManager
|
|
19
|
+
from .env_loader import load_env
|
|
18
20
|
from .events.manager import EventManager
|
|
19
21
|
from .inbox import Inbox
|
|
22
|
+
from .models.events import EventStatus
|
|
23
|
+
from .parsing.output_parser import OutputParser
|
|
20
24
|
from .persistence import EventStore, StateStore
|
|
21
25
|
from .project_session import ProjectSession, SessionState
|
|
22
26
|
from .registry import ProjectRegistry
|
|
27
|
+
from .runtime.monitor import RuntimeMonitor
|
|
23
28
|
from .tmux_orchestrator import TmuxOrchestrator
|
|
29
|
+
from .work.executor import WorkExecutor
|
|
30
|
+
from .work.queue import WorkQueue
|
|
31
|
+
from .workflow.event_handler import EventHandler
|
|
32
|
+
|
|
33
|
+
# Load environment variables at module import
|
|
34
|
+
load_env()
|
|
24
35
|
|
|
25
36
|
logger = logging.getLogger(__name__)
|
|
26
37
|
|
|
@@ -38,6 +49,11 @@ class CommanderDaemon:
|
|
|
38
49
|
event_manager: Event manager
|
|
39
50
|
inbox: Event inbox
|
|
40
51
|
sessions: Active project sessions by project_id
|
|
52
|
+
work_queues: Work queues by project_id
|
|
53
|
+
work_executors: Work executors by project_id
|
|
54
|
+
block_manager: Block manager for automatic work blocking
|
|
55
|
+
runtime_monitor: Runtime monitor for output monitoring
|
|
56
|
+
event_handler: Event handler for blocking event workflow
|
|
41
57
|
state_store: StateStore for project/session persistence
|
|
42
58
|
event_store: EventStore for event queue persistence
|
|
43
59
|
running: Whether daemon is currently running
|
|
@@ -68,6 +84,8 @@ class CommanderDaemon:
|
|
|
68
84
|
self.event_manager = EventManager()
|
|
69
85
|
self.inbox = Inbox(self.event_manager, self.registry)
|
|
70
86
|
self.sessions: Dict[str, ProjectSession] = {}
|
|
87
|
+
self.work_queues: Dict[str, WorkQueue] = {}
|
|
88
|
+
self.work_executors: Dict[str, WorkExecutor] = {}
|
|
71
89
|
self._running = False
|
|
72
90
|
self._server_task: Optional[asyncio.Task] = None
|
|
73
91
|
self._main_loop_task: Optional[asyncio.Task] = None
|
|
@@ -76,6 +94,30 @@ class CommanderDaemon:
|
|
|
76
94
|
self.state_store = StateStore(config.state_dir)
|
|
77
95
|
self.event_store = EventStore(config.state_dir)
|
|
78
96
|
|
|
97
|
+
# Initialize BlockManager with work queues and executors
|
|
98
|
+
self.block_manager = BlockManager(
|
|
99
|
+
event_manager=self.event_manager,
|
|
100
|
+
work_queues=self.work_queues,
|
|
101
|
+
work_executors=self.work_executors,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Initialize RuntimeMonitor with BlockManager
|
|
105
|
+
parser = OutputParser(self.event_manager)
|
|
106
|
+
self.runtime_monitor = RuntimeMonitor(
|
|
107
|
+
orchestrator=self.orchestrator,
|
|
108
|
+
parser=parser,
|
|
109
|
+
event_manager=self.event_manager,
|
|
110
|
+
poll_interval=config.poll_interval,
|
|
111
|
+
block_manager=self.block_manager,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Initialize EventHandler with BlockManager
|
|
115
|
+
self.event_handler = EventHandler(
|
|
116
|
+
inbox=self.inbox,
|
|
117
|
+
session_manager=self.sessions,
|
|
118
|
+
block_manager=self.block_manager,
|
|
119
|
+
)
|
|
120
|
+
|
|
79
121
|
# Configure logging
|
|
80
122
|
logging.basicConfig(
|
|
81
123
|
level=getattr(logging, config.log_level.upper()),
|
|
@@ -122,12 +164,16 @@ class CommanderDaemon:
|
|
|
122
164
|
# Set up signal handlers
|
|
123
165
|
self._setup_signal_handlers()
|
|
124
166
|
|
|
125
|
-
# Inject
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
# Inject daemon instances into API app.state (BEFORE lifespan runs)
|
|
168
|
+
app.state.registry = self.registry
|
|
169
|
+
app.state.tmux = self.orchestrator
|
|
170
|
+
app.state.event_manager = self.event_manager
|
|
171
|
+
app.state.inbox = self.inbox
|
|
172
|
+
app.state.work_queues = self.work_queues
|
|
173
|
+
app.state.daemon_instance = self
|
|
174
|
+
app.state.session_manager = self.sessions
|
|
175
|
+
app.state.event_handler = self.event_handler
|
|
176
|
+
logger.info(f"Injected work_queues dict id: {id(self.work_queues)}")
|
|
131
177
|
|
|
132
178
|
# Start API server in background
|
|
133
179
|
logger.info(f"Starting API server on {self.config.host}:{self.config.port}")
|
|
@@ -171,6 +217,16 @@ class CommanderDaemon:
|
|
|
171
217
|
except Exception as e:
|
|
172
218
|
logger.error(f"Error stopping session {project_id}: {e}")
|
|
173
219
|
|
|
220
|
+
# Clear BlockManager project mappings
|
|
221
|
+
for project_id in list(self.work_queues.keys()):
|
|
222
|
+
try:
|
|
223
|
+
removed = self.block_manager.clear_project_mappings(project_id)
|
|
224
|
+
logger.debug(
|
|
225
|
+
f"Cleared {removed} work mappings for project {project_id}"
|
|
226
|
+
)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"Error clearing mappings for {project_id}: {e}")
|
|
229
|
+
|
|
174
230
|
# Cancel main loop task
|
|
175
231
|
if self._main_loop_task and not self._main_loop_task.done():
|
|
176
232
|
self._main_loop_task.cancel()
|
|
@@ -210,9 +266,19 @@ class CommanderDaemon:
|
|
|
210
266
|
|
|
211
267
|
while self._running:
|
|
212
268
|
try:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
269
|
+
logger.info(f"🔄 Main loop iteration (running={self._running})")
|
|
270
|
+
logger.info(
|
|
271
|
+
f"work_queues dict id: {id(self.work_queues)}, keys: {list(self.work_queues.keys())}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Check for resolved events and resume sessions
|
|
275
|
+
await self._check_and_resume_sessions()
|
|
276
|
+
|
|
277
|
+
# Check each ProjectSession for runnable work
|
|
278
|
+
logger.info(
|
|
279
|
+
f"Checking for pending work across {len(self.work_queues)} queues"
|
|
280
|
+
)
|
|
281
|
+
await self._execute_pending_work()
|
|
216
282
|
|
|
217
283
|
# Periodic state persistence
|
|
218
284
|
current_time = asyncio.get_event_loop().time()
|
|
@@ -241,7 +307,16 @@ class CommanderDaemon:
|
|
|
241
307
|
|
|
242
308
|
Registers handlers for SIGINT and SIGTERM that trigger
|
|
243
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.
|
|
244
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
|
|
245
320
|
|
|
246
321
|
def handle_signal(signum: int, frame) -> None:
|
|
247
322
|
"""Handle shutdown signal.
|
|
@@ -282,7 +357,26 @@ class CommanderDaemon:
|
|
|
282
357
|
if project is None:
|
|
283
358
|
raise ValueError(f"Project not found: {project_id}")
|
|
284
359
|
|
|
285
|
-
|
|
360
|
+
# Create work queue for project if not exists
|
|
361
|
+
if project_id not in self.work_queues:
|
|
362
|
+
self.work_queues[project_id] = WorkQueue(project_id)
|
|
363
|
+
logger.debug(f"Created work queue for project {project_id}")
|
|
364
|
+
|
|
365
|
+
# Create work executor for project if not exists
|
|
366
|
+
if project_id not in self.work_executors:
|
|
367
|
+
from .runtime.executor import RuntimeExecutor
|
|
368
|
+
|
|
369
|
+
runtime_executor = RuntimeExecutor(self.orchestrator)
|
|
370
|
+
self.work_executors[project_id] = WorkExecutor(
|
|
371
|
+
runtime=runtime_executor, queue=self.work_queues[project_id]
|
|
372
|
+
)
|
|
373
|
+
logger.debug(f"Created work executor for project {project_id}")
|
|
374
|
+
|
|
375
|
+
session = ProjectSession(
|
|
376
|
+
project=project,
|
|
377
|
+
orchestrator=self.orchestrator,
|
|
378
|
+
monitor=self.runtime_monitor,
|
|
379
|
+
)
|
|
286
380
|
self.sessions[project_id] = session
|
|
287
381
|
|
|
288
382
|
logger.info(f"Created new session for project {project_id}")
|
|
@@ -363,6 +457,117 @@ class CommanderDaemon:
|
|
|
363
457
|
except Exception as e:
|
|
364
458
|
logger.error(f"Failed to save state: {e}", exc_info=True)
|
|
365
459
|
|
|
460
|
+
async def _check_and_resume_sessions(self) -> None:
|
|
461
|
+
"""Check for resolved events and resume paused sessions.
|
|
462
|
+
|
|
463
|
+
Iterates through all paused sessions, checks if their blocking events
|
|
464
|
+
have been resolved, and resumes execution if ready.
|
|
465
|
+
"""
|
|
466
|
+
for project_id, session in list(self.sessions.items()):
|
|
467
|
+
# Skip non-paused sessions
|
|
468
|
+
if session.state != SessionState.PAUSED:
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
# Check if pause reason (event ID) is resolved
|
|
472
|
+
if not session.pause_reason:
|
|
473
|
+
logger.warning(f"Session {project_id} paused with no reason, resuming")
|
|
474
|
+
await session.resume()
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# Check if event is resolved
|
|
478
|
+
event = self.event_manager.get(session.pause_reason)
|
|
479
|
+
if event and event.status == EventStatus.RESOLVED:
|
|
480
|
+
logger.info(
|
|
481
|
+
f"Event {event.id} resolved, resuming session for {project_id}"
|
|
482
|
+
)
|
|
483
|
+
await session.resume()
|
|
484
|
+
|
|
485
|
+
# Unblock any work items that were blocked by this event
|
|
486
|
+
if project_id in self.work_executors:
|
|
487
|
+
executor = self.work_executors[project_id]
|
|
488
|
+
queue = self.work_queues[project_id]
|
|
489
|
+
|
|
490
|
+
# Find work items blocked by this event
|
|
491
|
+
blocked_items = [
|
|
492
|
+
item
|
|
493
|
+
for item in queue.list()
|
|
494
|
+
if item.state.value == "blocked"
|
|
495
|
+
and item.metadata.get("block_reason") == event.id
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
for item in blocked_items:
|
|
499
|
+
await executor.handle_unblock(item.id)
|
|
500
|
+
logger.info(f"Unblocked work item {item.id}")
|
|
501
|
+
|
|
502
|
+
async def _execute_pending_work(self) -> None:
|
|
503
|
+
"""Execute pending work for all ready sessions.
|
|
504
|
+
|
|
505
|
+
Scans all work queues for pending work. For projects with work but no session,
|
|
506
|
+
auto-creates a session. Then executes the next available work item via WorkExecutor.
|
|
507
|
+
"""
|
|
508
|
+
# First pass: Auto-create and start sessions for projects with pending work
|
|
509
|
+
for project_id, queue in list(self.work_queues.items()):
|
|
510
|
+
logger.info(
|
|
511
|
+
f"Checking queue for {project_id}: pending={queue.pending_count}"
|
|
512
|
+
)
|
|
513
|
+
# Skip if no pending work
|
|
514
|
+
if queue.pending_count == 0:
|
|
515
|
+
continue
|
|
516
|
+
|
|
517
|
+
# Auto-create session if needed
|
|
518
|
+
if project_id not in self.sessions:
|
|
519
|
+
try:
|
|
520
|
+
logger.info(
|
|
521
|
+
f"Auto-creating session for project {project_id} with pending work"
|
|
522
|
+
)
|
|
523
|
+
session = self.get_or_create_session(project_id)
|
|
524
|
+
|
|
525
|
+
# Start the session so it's ready for work
|
|
526
|
+
if session.state.value == "idle":
|
|
527
|
+
logger.info(f"Auto-starting session for {project_id}")
|
|
528
|
+
await session.start()
|
|
529
|
+
except Exception as e:
|
|
530
|
+
logger.error(
|
|
531
|
+
f"Failed to auto-create/start session for {project_id}: {e}",
|
|
532
|
+
exc_info=True,
|
|
533
|
+
)
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
# Second pass: Execute work for ready sessions
|
|
537
|
+
for project_id, session in list(self.sessions.items()):
|
|
538
|
+
# Skip sessions that aren't ready for work
|
|
539
|
+
if not session.is_ready():
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
# Skip if no work queue exists
|
|
543
|
+
if project_id not in self.work_queues:
|
|
544
|
+
continue
|
|
545
|
+
|
|
546
|
+
# Get work executor for project
|
|
547
|
+
executor = self.work_executors.get(project_id)
|
|
548
|
+
if not executor:
|
|
549
|
+
logger.warning(
|
|
550
|
+
f"No work executor found for project {project_id}, skipping"
|
|
551
|
+
)
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
# Check if there's work available
|
|
555
|
+
queue = self.work_queues[project_id]
|
|
556
|
+
if queue.pending_count == 0:
|
|
557
|
+
continue
|
|
558
|
+
|
|
559
|
+
# Try to execute next work item
|
|
560
|
+
try:
|
|
561
|
+
# Pass the session's active pane for execution
|
|
562
|
+
executed = await executor.execute_next(pane_target=session.active_pane)
|
|
563
|
+
if executed:
|
|
564
|
+
logger.info(f"Started work execution for project {project_id}")
|
|
565
|
+
except Exception as e:
|
|
566
|
+
logger.error(
|
|
567
|
+
f"Error executing work for project {project_id}: {e}",
|
|
568
|
+
exc_info=True,
|
|
569
|
+
)
|
|
570
|
+
|
|
366
571
|
|
|
367
572
|
async def main(config: Optional[DaemonConfig] = None) -> None:
|
|
368
573
|
"""Main entry point for running the daemon.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Environment variable loader for Commander.
|
|
2
|
+
|
|
3
|
+
This module handles automatic loading of .env and .env.local files
|
|
4
|
+
at Commander startup. Environment files are loaded with the following precedence:
|
|
5
|
+
1. Existing environment variables (not overridden)
|
|
6
|
+
2. .env.local (local overrides)
|
|
7
|
+
3. .env (defaults)
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from claude_mpm.commander.env_loader import load_env
|
|
11
|
+
>>> load_env()
|
|
12
|
+
# Automatically loads .env.local and .env from project root
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_env() -> None:
|
|
24
|
+
"""Load environment variables from .env and .env.local files.
|
|
25
|
+
|
|
26
|
+
Searches for .env and .env.local in the project root directory
|
|
27
|
+
(parent of src/claude_mpm). Files are loaded with override=False,
|
|
28
|
+
meaning existing environment variables take precedence.
|
|
29
|
+
|
|
30
|
+
Precedence (highest to lowest):
|
|
31
|
+
1. Existing environment variables
|
|
32
|
+
2. .env.local
|
|
33
|
+
3. .env
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> load_env()
|
|
37
|
+
# Loads .env.local and .env if they exist
|
|
38
|
+
"""
|
|
39
|
+
# Find project root (parent of src/claude_mpm)
|
|
40
|
+
# Current file: src/claude_mpm/commander/env_loader.py
|
|
41
|
+
# Project root: ../../../ (3 levels up)
|
|
42
|
+
current_file = Path(__file__)
|
|
43
|
+
project_root = current_file.parent.parent.parent.parent
|
|
44
|
+
|
|
45
|
+
# Try loading .env.local first (higher priority)
|
|
46
|
+
env_local = project_root / ".env.local"
|
|
47
|
+
if env_local.exists():
|
|
48
|
+
load_dotenv(env_local, override=False)
|
|
49
|
+
logger.debug(f"Loaded environment from {env_local}")
|
|
50
|
+
|
|
51
|
+
# Then load .env (lower priority)
|
|
52
|
+
env_file = project_root / ".env"
|
|
53
|
+
if env_file.exists():
|
|
54
|
+
load_dotenv(env_file, override=False)
|
|
55
|
+
logger.debug(f"Loaded environment from {env_file}")
|
|
56
|
+
|
|
57
|
+
# Log if neither file exists
|
|
58
|
+
if not env_local.exists() and not env_file.exists():
|
|
59
|
+
logger.debug("No .env or .env.local files found in project root")
|
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Conversation memory system for Commander.
|
|
2
|
+
|
|
3
|
+
This module provides semantic search, storage, and context compression
|
|
4
|
+
for all Claude Code instance conversations.
|
|
5
|
+
|
|
6
|
+
Key Components:
|
|
7
|
+
- ConversationStore: CRUD operations for conversations
|
|
8
|
+
- EmbeddingService: Generate vector embeddings
|
|
9
|
+
- SemanticSearch: Query conversations semantically
|
|
10
|
+
- ContextCompressor: Summarize conversations for context
|
|
11
|
+
- EntityExtractor: Extract files, functions, errors
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from claude_mpm.commander.memory import (
|
|
15
|
+
... ConversationStore,
|
|
16
|
+
... EmbeddingService,
|
|
17
|
+
... SemanticSearch,
|
|
18
|
+
... ContextCompressor,
|
|
19
|
+
... )
|
|
20
|
+
>>> store = ConversationStore()
|
|
21
|
+
>>> embeddings = EmbeddingService()
|
|
22
|
+
>>> search = SemanticSearch(store, embeddings)
|
|
23
|
+
>>> results = await search.search("how did we fix the login bug?")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .compression import ContextCompressor
|
|
27
|
+
from .embeddings import EmbeddingService
|
|
28
|
+
from .entities import Entity, EntityExtractor, EntityType
|
|
29
|
+
from .integration import MemoryIntegration
|
|
30
|
+
from .search import SearchResult, SemanticSearch
|
|
31
|
+
from .store import Conversation, ConversationMessage, ConversationStore
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ContextCompressor",
|
|
35
|
+
"Conversation",
|
|
36
|
+
"ConversationMessage",
|
|
37
|
+
"ConversationStore",
|
|
38
|
+
"EmbeddingService",
|
|
39
|
+
"Entity",
|
|
40
|
+
"EntityExtractor",
|
|
41
|
+
"EntityType",
|
|
42
|
+
"MemoryIntegration",
|
|
43
|
+
"SearchResult",
|
|
44
|
+
"SemanticSearch",
|
|
45
|
+
]
|