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
@@ -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 global instances into API app
126
- global api_registry, api_tmux, api_event_manager, api_inbox
127
- api_registry = self.registry
128
- api_tmux = self.orchestrator
129
- api_event_manager = self.event_manager
130
- api_inbox = self.inbox
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
- # TODO: Check for resolved events and resume sessions (Phase 2 Sprint 3)
214
- # TODO: Check each ProjectSession for runnable work (Phase 2 Sprint 2)
215
- # TODO: Spawn RuntimeExecutors for new work items (Phase 2 Sprint 1)
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
- session = ProjectSession(project, self.orchestrator)
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
+ ]