claude-mpm 5.6.23__py3-none-any.whl → 5.6.73__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 (82) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/auth/__init__.py +35 -0
  3. claude_mpm/auth/callback_server.py +328 -0
  4. claude_mpm/auth/models.py +104 -0
  5. claude_mpm/auth/oauth_manager.py +266 -0
  6. claude_mpm/auth/providers/__init__.py +12 -0
  7. claude_mpm/auth/providers/base.py +165 -0
  8. claude_mpm/auth/providers/google.py +261 -0
  9. claude_mpm/auth/token_storage.py +252 -0
  10. claude_mpm/cli/commands/commander.py +6 -6
  11. claude_mpm/cli/commands/mcp.py +29 -17
  12. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  13. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  14. claude_mpm/cli/commands/oauth.py +481 -0
  15. claude_mpm/cli/executor.py +9 -0
  16. claude_mpm/cli/helpers.py +1 -1
  17. claude_mpm/cli/parsers/base_parser.py +13 -0
  18. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  19. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  20. claude_mpm/cli/startup.py +150 -33
  21. claude_mpm/cli/startup_display.py +3 -2
  22. claude_mpm/commander/chat/cli.py +5 -2
  23. claude_mpm/commander/chat/commands.py +42 -16
  24. claude_mpm/commander/chat/repl.py +1581 -70
  25. claude_mpm/commander/events/manager.py +61 -1
  26. claude_mpm/commander/frameworks/base.py +87 -0
  27. claude_mpm/commander/frameworks/mpm.py +9 -14
  28. claude_mpm/commander/git/__init__.py +5 -0
  29. claude_mpm/commander/git/worktree_manager.py +212 -0
  30. claude_mpm/commander/instance_manager.py +428 -13
  31. claude_mpm/commander/models/events.py +6 -0
  32. claude_mpm/commander/persistence/state_store.py +95 -1
  33. claude_mpm/commander/tmux_orchestrator.py +3 -2
  34. claude_mpm/constants.py +5 -0
  35. claude_mpm/core/hook_manager.py +2 -1
  36. claude_mpm/core/logging_utils.py +4 -2
  37. claude_mpm/core/output_style_manager.py +5 -2
  38. claude_mpm/core/socketio_pool.py +34 -10
  39. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  40. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
  41. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  42. claude_mpm/hooks/claude_hooks/installer.py +175 -51
  43. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  44. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  45. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  46. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  47. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  48. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  49. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  50. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  51. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  52. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  53. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  54. claude_mpm/init.py +21 -14
  55. claude_mpm/mcp/__init__.py +9 -0
  56. claude_mpm/mcp/google_workspace_server.py +610 -0
  57. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  58. claude_mpm/services/command_deployment_service.py +44 -26
  59. claude_mpm/services/hook_installer_service.py +77 -8
  60. claude_mpm/services/mcp_config_manager.py +99 -19
  61. claude_mpm/services/mcp_service_registry.py +294 -0
  62. claude_mpm/services/monitor/server.py +6 -1
  63. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/METADATA +24 -1
  64. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/RECORD +69 -64
  65. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/WHEEL +1 -1
  66. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/entry_points.txt +2 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  76. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  80. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE +0 -0
  81. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  82. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,32 @@
1
1
  """Manages running Claude Code/MPM instances."""
2
2
 
3
+ import asyncio
3
4
  import logging
5
+ import re
6
+ from datetime import datetime, timezone
4
7
  from pathlib import Path
5
- from typing import Optional
8
+ from typing import TYPE_CHECKING, Optional
6
9
 
7
10
  from claude_mpm.commander.adapters import (
8
11
  AdapterResponse,
9
12
  ClaudeCodeAdapter,
10
13
  ClaudeCodeCommunicationAdapter,
11
14
  )
12
- from claude_mpm.commander.frameworks.base import BaseFramework, InstanceInfo
15
+ from claude_mpm.commander.frameworks.base import (
16
+ BaseFramework,
17
+ InstanceInfo,
18
+ RegisteredInstance,
19
+ )
13
20
  from claude_mpm.commander.frameworks.claude_code import ClaudeCodeFramework
14
21
  from claude_mpm.commander.frameworks.mpm import MPMFramework
22
+ from claude_mpm.commander.git.worktree_manager import WorktreeManager
23
+ from claude_mpm.commander.models.events import EventType
15
24
  from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
16
25
 
26
+ if TYPE_CHECKING:
27
+ from claude_mpm.commander.events.manager import EventManager
28
+ from claude_mpm.commander.persistence.state_store import StateStore
29
+
17
30
  logger = logging.getLogger(__name__)
18
31
 
19
32
 
@@ -75,6 +88,32 @@ class InstanceManager:
75
88
  self._instances: dict[str, InstanceInfo] = {}
76
89
  self._frameworks = self._load_frameworks()
77
90
  self._adapters: dict[str, ClaudeCodeCommunicationAdapter] = {}
91
+ self._event_manager: Optional[EventManager] = None
92
+ self._state_store: Optional[StateStore] = None
93
+
94
+ def set_event_manager(self, event_manager: "EventManager") -> None:
95
+ """Set the event manager for emitting instance events.
96
+
97
+ Args:
98
+ event_manager: EventManager instance for event emission
99
+
100
+ Example:
101
+ >>> manager = InstanceManager(orchestrator)
102
+ >>> manager.set_event_manager(event_manager)
103
+ """
104
+ self._event_manager = event_manager
105
+
106
+ def set_state_store(self, state_store: "StateStore") -> None:
107
+ """Set state store for persistence.
108
+
109
+ Args:
110
+ state_store: StateStore instance for persisting registered instances
111
+
112
+ Example:
113
+ >>> manager = InstanceManager(orchestrator)
114
+ >>> manager.set_state_store(state_store)
115
+ """
116
+ self._state_store = state_store
78
117
 
79
118
  def _load_frameworks(self) -> dict[str, BaseFramework]:
80
119
  """Load available frameworks.
@@ -200,8 +239,187 @@ class InstanceManager:
200
239
  f"Started instance '{name}' with framework '{framework}' at {project_path}"
201
240
  )
202
241
 
242
+ # Emit starting event and start background ready detection
243
+ if self._event_manager:
244
+ event = self._event_manager.create(
245
+ project_id=name,
246
+ event_type=EventType.INSTANCE_STARTING,
247
+ title=f"Starting instance '{name}'",
248
+ content=f"Instance {name} is starting at {project_path}",
249
+ context={"instance_name": name, "working_dir": str(project_path)},
250
+ )
251
+ await self._event_manager.emit(event)
252
+
253
+ # Start background ready detection (always, not just with event_manager)
254
+ asyncio.create_task(self._detect_ready(name, instance))
255
+
203
256
  return instance
204
257
 
258
+ async def _detect_ready(
259
+ self, name: str, instance_info: InstanceInfo, timeout: int = 30
260
+ ) -> None:
261
+ """Background task to detect when instance is ready.
262
+
263
+ Monitors the pane output for patterns indicating the instance
264
+ is ready to accept commands.
265
+
266
+ Args:
267
+ name: Instance name
268
+ instance_info: InstanceInfo with pane details
269
+ timeout: Maximum seconds to wait for ready state
270
+
271
+ Example:
272
+ >>> # Called internally by start_instance
273
+ >>> asyncio.create_task(self._detect_ready(name, instance))
274
+ """
275
+ # TIER 1: Startup markers (most reliable, appear at T+0-2s)
276
+ startup_patterns = [
277
+ r"Claude Code v[\d.]+", # Version marker
278
+ r"Claude Code", # Product identification
279
+ r"Opus 4\.5", # Model identifier
280
+ r"Claude Max|Claude Pro", # Tier indicator
281
+ ]
282
+
283
+ # TIER 2: Ready state indicators (appear at T+5-10s)
284
+ ready_patterns = [
285
+ r">\s*$", # CLI prompt (end of line)
286
+ r"^>\s*", # CLI prompt (start of line)
287
+ r"What can I.*help", # Specific greeting
288
+ r"How can I.*assist", # Specific greeting
289
+ r"(Human|User):\s*$", # Anthropic format
290
+ ]
291
+
292
+ # TIER 3: Secondary indicators (confirmation)
293
+ secondary_patterns = [
294
+ r"Ready for input", # Explicit ready
295
+ r"Tips for getting", # Claude CLI tips
296
+ r"Use /help", # Help hint
297
+ r"claudeMd", # CLAUDE.md loaded
298
+ r"Agent Memory", # Agent initialized
299
+ r"PM_INSTRUCTIONS", # MPM initialized
300
+ ]
301
+
302
+ # Combine all patterns (TIER 1 most reliable)
303
+ all_patterns = startup_patterns + ready_patterns + secondary_patterns
304
+
305
+ start_time = asyncio.get_event_loop().time()
306
+ check_count = 0
307
+
308
+ while asyncio.get_event_loop().time() - start_time < timeout:
309
+ elapsed = asyncio.get_event_loop().time() - start_time
310
+ check_count += 1
311
+
312
+ # Progress logging at key milestones
313
+ if elapsed > 10 and check_count == 11:
314
+ logger.info(
315
+ f"Ready detection for '{name}' still waiting after 10s... "
316
+ f"(Claude Code startup may be slow)"
317
+ )
318
+ elif elapsed > 20 and check_count == 21:
319
+ logger.info(
320
+ f"Ready detection for '{name}' still waiting after 20s... "
321
+ f"(system may be overloaded)"
322
+ )
323
+
324
+ await asyncio.sleep(1)
325
+ try:
326
+ # IMPROVED: Increased buffer from 50 to 100 lines
327
+ output = self.orchestrator.capture_output(
328
+ instance_info.pane_target, lines=100
329
+ )
330
+
331
+ if output:
332
+ # Check all patterns (added re.IGNORECASE flag)
333
+ # IMPROVED: Use MULTILINE | IGNORECASE flags
334
+ for pattern in all_patterns:
335
+ if re.search(pattern, output, re.MULTILINE | re.IGNORECASE):
336
+ # Instance is ready!
337
+ if name in self._instances:
338
+ self._instances[name].ready = True
339
+
340
+ # Emit ready event
341
+ if self._event_manager:
342
+ event = self._event_manager.create(
343
+ project_id=name,
344
+ event_type=EventType.INSTANCE_READY,
345
+ title=f"Instance '{name}' ready",
346
+ content=(
347
+ f"Instance {name} is ready for commands "
348
+ f"(detected in {elapsed:.1f}s)"
349
+ ),
350
+ context={
351
+ "instance_name": name,
352
+ "detection_time": elapsed,
353
+ "pattern_matched": pattern,
354
+ },
355
+ )
356
+ await self._event_manager.emit(event)
357
+
358
+ logger.info(
359
+ f"Instance '{name}' is ready "
360
+ f"(detected in {elapsed:.1f}s via pattern: {pattern})"
361
+ )
362
+ return
363
+
364
+ except Exception as e:
365
+ logger.debug(
366
+ f"Error checking ready state for '{name}': {e}. "
367
+ f"Elapsed: {elapsed:.1f}s"
368
+ )
369
+
370
+ # Timeout - mark as ready anyway since instance might still work
371
+ logger.warning(
372
+ f"Instance '{name}' ready detection timed out after {timeout}s. "
373
+ f"Instance may still be functional. Check logs for issues."
374
+ )
375
+
376
+ if name in self._instances:
377
+ self._instances[name].ready = True
378
+
379
+ if self._event_manager:
380
+ event = self._event_manager.create(
381
+ project_id=name,
382
+ event_type=EventType.INSTANCE_READY,
383
+ title=f"Instance '{name}' started",
384
+ content=(
385
+ f"Instance {name} startup timeout. "
386
+ f"May be ready, or startup may be slow/blocked. "
387
+ f"Check instance manually if issues occur."
388
+ ),
389
+ context={
390
+ "instance_name": name,
391
+ "timeout": True,
392
+ "timeout_seconds": timeout,
393
+ },
394
+ )
395
+ await self._event_manager.emit(event)
396
+
397
+ async def wait_for_ready(self, name: str, timeout: int = 30) -> bool:
398
+ """Wait for an instance to be ready.
399
+
400
+ Polls the instance's ready flag until it becomes True or timeout.
401
+
402
+ Args:
403
+ name: Instance name
404
+ timeout: Maximum seconds to wait
405
+
406
+ Returns:
407
+ True if instance is ready, False if timeout
408
+
409
+ Example:
410
+ >>> manager = InstanceManager(orchestrator)
411
+ >>> await manager.start_instance("myapp", "/path/to/app", "mpm")
412
+ >>> if await manager.wait_for_ready("myapp", timeout=30):
413
+ ... print("Ready!")
414
+ """
415
+ start_time = asyncio.get_event_loop().time()
416
+ while asyncio.get_event_loop().time() - start_time < timeout:
417
+ inst = self._instances.get(name)
418
+ if inst and inst.ready:
419
+ return True
420
+ await asyncio.sleep(0.5)
421
+ return False
422
+
205
423
  async def stop_instance(self, name: str) -> bool:
206
424
  """Stop an instance.
207
425
 
@@ -387,26 +605,50 @@ class InstanceManager:
387
605
 
388
606
  return True
389
607
 
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.
608
+ async def close_instance(self, name: str, merge: bool = True) -> tuple[bool, str]:
609
+ """Close instance: stop tmux, optionally merge worktree, cleanup.
394
610
 
395
611
  Args:
396
612
  name: Instance name to close
613
+ merge: Whether to merge worktree to main (default True)
397
614
 
398
615
  Returns:
399
- True if closed successfully
400
-
401
- Raises:
402
- InstanceNotFoundError: If instance not found
616
+ Tuple of (success, message)
403
617
 
404
618
  Example:
405
619
  >>> manager = InstanceManager(orchestrator)
406
- >>> await manager.close_instance("myapp")
407
- True
620
+ >>> success, msg = await manager.close_instance("myapp")
621
+ >>> print(success, msg)
622
+ True Closed 'myapp'
408
623
  """
409
- return await self.stop_instance(name)
624
+ registered = (
625
+ self._state_store.get_registered_instance(name)
626
+ if self._state_store
627
+ else None
628
+ )
629
+
630
+ # Stop the tmux session
631
+ try:
632
+ await self.stop_instance(name)
633
+ except InstanceNotFoundError:
634
+ # Instance not running, but may still have worktree to clean up
635
+ pass
636
+
637
+ # Merge and cleanup worktree if enabled
638
+ if registered and registered.use_worktree and registered.worktree_path:
639
+ wt_manager = WorktreeManager(Path(registered.path))
640
+ if merge:
641
+ success, msg = wt_manager.merge_to_main(name, delete_after=True)
642
+ if not success:
643
+ return False, msg
644
+ else:
645
+ wt_manager.remove(name, force=True)
646
+
647
+ # Remove from registry
648
+ if self._state_store:
649
+ self._state_store.unregister_instance(name)
650
+
651
+ return True, f"Closed '{name}'"
410
652
 
411
653
  async def disconnect_instance(self, name: str) -> bool:
412
654
  """Disconnect from an instance without closing it.
@@ -448,3 +690,176 @@ class InstanceManager:
448
690
  logger.debug(f"No adapter to disconnect for instance '{name}'")
449
691
 
450
692
  return True
693
+
694
+ async def register_instance(
695
+ self,
696
+ path: str,
697
+ framework: str,
698
+ name: str,
699
+ use_worktree: bool = True,
700
+ branch: Optional[str] = None,
701
+ ) -> InstanceInfo:
702
+ """Register an instance and start it.
703
+
704
+ Registers the instance in persistent storage so it can be started
705
+ by name in future sessions, then starts the instance.
706
+
707
+ Args:
708
+ path: Project directory path
709
+ framework: Framework to use ("cc" or "mpm")
710
+ name: Instance name (also worktree name if enabled)
711
+ use_worktree: Create isolated git worktree (default True)
712
+ branch: Branch for worktree (default: session-{name})
713
+
714
+ Returns:
715
+ InstanceInfo with tmux session details
716
+
717
+ Raises:
718
+ FrameworkNotFoundError: If framework is not available
719
+ InstanceAlreadyExistsError: If instance already exists
720
+
721
+ Example:
722
+ >>> manager = InstanceManager(orchestrator)
723
+ >>> manager.set_state_store(state_store)
724
+ >>> instance = await manager.register_instance(
725
+ ... "/Users/user/myapp", "cc", "myapp"
726
+ ... )
727
+ >>> print(instance.name, instance.framework)
728
+ myapp cc
729
+ """
730
+ project_path = Path(path).expanduser().resolve()
731
+
732
+ worktree_path = None
733
+ worktree_branch = None
734
+ working_path = project_path
735
+
736
+ # Create worktree if enabled and it's a git repo
737
+ if use_worktree and (project_path / ".git").exists():
738
+ try:
739
+ wt_manager = WorktreeManager(project_path)
740
+ wt_info = wt_manager.create(name, branch)
741
+ worktree_path = str(wt_info.path)
742
+ worktree_branch = wt_info.branch
743
+ working_path = wt_info.path
744
+ logger.info(
745
+ f"Created worktree for '{name}' at {worktree_path} "
746
+ f"on branch {worktree_branch}"
747
+ )
748
+ except Exception as e:
749
+ logger.warning(f"Could not create worktree: {e}. Using original path.")
750
+
751
+ # Create registered instance with worktree info
752
+ registered = RegisteredInstance(
753
+ name=name,
754
+ path=str(project_path),
755
+ framework=framework,
756
+ registered_at=datetime.now(timezone.utc).isoformat(),
757
+ worktree_path=worktree_path,
758
+ worktree_branch=worktree_branch,
759
+ use_worktree=use_worktree and worktree_path is not None,
760
+ )
761
+
762
+ # Save to persistent storage
763
+ if self._state_store:
764
+ self._state_store.register_instance(registered)
765
+
766
+ # Start the instance in worktree path
767
+ return await self.start_instance(name, working_path, framework)
768
+
769
+ async def start_by_name(self, name: str) -> Optional[InstanceInfo]:
770
+ """Start a previously registered instance by name.
771
+
772
+ Looks up the instance registration and starts it with the
773
+ stored path and framework. Uses the worktree path if configured.
774
+
775
+ Args:
776
+ name: Instance name (must have been previously registered)
777
+
778
+ Returns:
779
+ InstanceInfo if instance was found and started, None if not registered
780
+
781
+ Example:
782
+ >>> manager = InstanceManager(orchestrator)
783
+ >>> manager.set_state_store(state_store)
784
+ >>> # After previous registration
785
+ >>> instance = await manager.start_by_name("myapp")
786
+ >>> if instance:
787
+ ... print(instance.name, instance.project_path)
788
+ myapp /Users/user/myapp
789
+ """
790
+ if not self._state_store:
791
+ return None
792
+
793
+ registered = self._state_store.get_registered_instance(name)
794
+ if not registered:
795
+ return None
796
+
797
+ # Use working_path which respects worktree setting
798
+ return await self.start_instance(
799
+ registered.name,
800
+ Path(registered.working_path),
801
+ registered.framework,
802
+ )
803
+
804
+ def list_registered(self) -> dict[str, RegisteredInstance]:
805
+ """List all registered instances.
806
+
807
+ Returns:
808
+ Dict mapping instance name to RegisteredInstance
809
+
810
+ Example:
811
+ >>> manager = InstanceManager(orchestrator)
812
+ >>> manager.set_state_store(state_store)
813
+ >>> registered = manager.list_registered()
814
+ >>> for name, instance in registered.items():
815
+ ... print(f"{name}: {instance.path} ({instance.framework})")
816
+ myapp: /Users/user/myapp (cc)
817
+ """
818
+ if not self._state_store:
819
+ return {}
820
+ return self._state_store.load_instances()
821
+
822
+ def unregister(self, name: str) -> bool:
823
+ """Unregister an instance.
824
+
825
+ Removes the instance from persistent storage. Does not stop
826
+ any running instance with this name.
827
+
828
+ Args:
829
+ name: Instance name to unregister
830
+
831
+ Returns:
832
+ True if instance was found and unregistered, False if not found
833
+
834
+ Example:
835
+ >>> manager = InstanceManager(orchestrator)
836
+ >>> manager.set_state_store(state_store)
837
+ >>> success = manager.unregister("myapp")
838
+ >>> print(success)
839
+ True
840
+ """
841
+ if not self._state_store:
842
+ return False
843
+ return self._state_store.unregister_instance(name)
844
+
845
+ def list_worktrees(self, path: str) -> list:
846
+ """List worktrees for a project.
847
+
848
+ Args:
849
+ path: Project directory path
850
+
851
+ Returns:
852
+ List of WorktreeInfo for all worktrees associated with the project
853
+
854
+ Example:
855
+ >>> manager = InstanceManager(orchestrator)
856
+ >>> worktrees = manager.list_worktrees("/Users/user/myapp")
857
+ >>> for wt in worktrees:
858
+ ... print(f"{wt.name}: {wt.path} ({wt.branch})")
859
+ myapp: /Users/user/.worktrees-myapp/myapp (session-myapp)
860
+ """
861
+ try:
862
+ wt_manager = WorktreeManager(Path(path))
863
+ return wt_manager.list()
864
+ except Exception:
865
+ return []
@@ -26,6 +26,9 @@ class EventType(Enum):
26
26
  MILESTONE = "milestone" # Significant progress
27
27
  STATUS = "status" # General update
28
28
  PROJECT_IDLE = "project_idle" # Project has no work
29
+ INSTANCE_STARTING = "instance_starting" # Instance is starting up
30
+ INSTANCE_READY = "instance_ready" # Instance is ready for work
31
+ INSTANCE_ERROR = "instance_error" # Instance encountered an error
29
32
 
30
33
 
31
34
  class EventPriority(Enum):
@@ -62,6 +65,9 @@ DEFAULT_PRIORITIES: Dict[EventType, EventPriority] = {
62
65
  EventType.MILESTONE: EventPriority.LOW,
63
66
  EventType.STATUS: EventPriority.INFO,
64
67
  EventType.PROJECT_IDLE: EventPriority.INFO,
68
+ EventType.INSTANCE_STARTING: EventPriority.INFO,
69
+ EventType.INSTANCE_READY: EventPriority.INFO,
70
+ EventType.INSTANCE_ERROR: EventPriority.HIGH,
65
71
  }
66
72
 
67
73
 
@@ -10,8 +10,9 @@ import logging
10
10
  import tempfile
11
11
  from datetime import datetime, timezone
12
12
  from pathlib import Path
13
- from typing import Any, Dict, List
13
+ from typing import Any, Dict, List, Optional
14
14
 
15
+ from ..frameworks.base import RegisteredInstance
15
16
  from ..models import Project, ProjectState, ToolSession
16
17
  from ..registry import ProjectRegistry
17
18
 
@@ -48,6 +49,7 @@ class StateStore:
48
49
 
49
50
  self.projects_path = self.state_dir / "projects.json"
50
51
  self.sessions_path = self.state_dir / "sessions.json"
52
+ self.instances_path = self.state_dir / "instances.json"
51
53
 
52
54
  logger.info(f"Initialized StateStore at {self.state_dir}")
53
55
 
@@ -307,3 +309,95 @@ class StateStore:
307
309
  else None
308
310
  ),
309
311
  )
312
+
313
+ # Instance persistence methods
314
+
315
+ def save_instances(self, instances: Dict[str, RegisteredInstance]) -> None:
316
+ """Save registered instances to disk.
317
+
318
+ Args:
319
+ instances: Dict mapping instance name to RegisteredInstance
320
+
321
+ Raises:
322
+ IOError: If write fails
323
+ """
324
+ data = {
325
+ "version": self.VERSION,
326
+ "saved_at": datetime.now(timezone.utc).isoformat(),
327
+ "instances": {name: inst.to_dict() for name, inst in instances.items()},
328
+ }
329
+ self._atomic_write(self.instances_path, data)
330
+ logger.info(f"Saved {len(instances)} instances to {self.instances_path}")
331
+
332
+ def load_instances(self) -> Dict[str, RegisteredInstance]:
333
+ """Load registered instances from disk.
334
+
335
+ Returns:
336
+ Dict mapping instance name to RegisteredInstance
337
+ (empty if file missing or corrupt)
338
+ """
339
+ if not self.instances_path.exists():
340
+ logger.info("No instances file found, returning empty dict")
341
+ return {}
342
+
343
+ try:
344
+ data = self._read_json(self.instances_path)
345
+
346
+ if data.get("version") != self.VERSION:
347
+ logger.warning(
348
+ f"Version mismatch: expected {self.VERSION}, "
349
+ f"got {data.get('version')}"
350
+ )
351
+
352
+ instances = {
353
+ name: RegisteredInstance.from_dict(inst_data)
354
+ for name, inst_data in data.get("instances", {}).items()
355
+ }
356
+
357
+ logger.info(f"Loaded {len(instances)} instances from {self.instances_path}")
358
+ return instances
359
+
360
+ except Exception as e:
361
+ logger.error(f"Failed to load instances: {e}", exc_info=True)
362
+ return {}
363
+
364
+ def register_instance(self, instance: RegisteredInstance) -> None:
365
+ """Register a single instance (add to existing).
366
+
367
+ Args:
368
+ instance: RegisteredInstance to add
369
+ """
370
+ instances = self.load_instances()
371
+ instances[instance.name] = instance
372
+ self.save_instances(instances)
373
+ logger.info(f"Registered instance '{instance.name}'")
374
+
375
+ def unregister_instance(self, name: str) -> bool:
376
+ """Remove an instance registration.
377
+
378
+ Args:
379
+ name: Instance name to remove
380
+
381
+ Returns:
382
+ True if instance was found and removed, False if not found
383
+ """
384
+ instances = self.load_instances()
385
+ if name in instances:
386
+ del instances[name]
387
+ self.save_instances(instances)
388
+ logger.info(f"Unregistered instance '{name}'")
389
+ return True
390
+ logger.warning(f"Instance '{name}' not found for unregistration")
391
+ return False
392
+
393
+ def get_registered_instance(self, name: str) -> Optional[RegisteredInstance]:
394
+ """Get a single registered instance by name.
395
+
396
+ Args:
397
+ name: Instance name to look up
398
+
399
+ Returns:
400
+ RegisteredInstance if found, None otherwise
401
+ """
402
+ instances = self.load_instances()
403
+ return instances.get(name)
@@ -158,10 +158,11 @@ class TmuxOrchestrator:
158
158
  """
159
159
  logger.info(f"Creating pane '{pane_id}' in {working_dir}")
160
160
 
161
- # Split window to create new pane
161
+ # Create new window instead of splitting pane to avoid "no space for new pane" error
162
+ # when tmux window is too small to split
162
163
  self._run_tmux(
163
164
  [
164
- "split-window",
165
+ "new-window",
165
166
  "-t",
166
167
  self.session_name,
167
168
  "-c",
claude_mpm/constants.py CHANGED
@@ -46,6 +46,7 @@ class CLICommands(str, Enum):
46
46
  DASHBOARD = "dashboard"
47
47
  UPGRADE = "upgrade"
48
48
  SKILLS = "skills"
49
+ OAUTH = "oauth"
49
50
 
50
51
  def with_prefix(self, prefix: CLIPrefix = CLIPrefix.MPM) -> str:
51
52
  """Get command with prefix."""
@@ -143,6 +144,10 @@ class MCPCommands(str, Enum):
143
144
  CONFIG = "config"
144
145
  SERVER = "server"
145
146
  EXTERNAL = "external"
147
+ # Service management commands
148
+ ENABLE = "enable"
149
+ DISABLE = "disable"
150
+ LIST = "list"
146
151
 
147
152
 
148
153
  class TicketCommands(str, Enum):
@@ -186,8 +186,9 @@ class HookManager:
186
186
  env["CLAUDE_MPM_HOOK_DEBUG"] = "true"
187
187
 
188
188
  # Execute with timeout in background thread
189
+ # Run as module to ensure proper package context for relative imports
189
190
  result = subprocess.run( # nosec B603 B607
190
- ["python", str(self.hook_handler_path)],
191
+ ["python", "-m", "claude_mpm.hooks.claude_hooks.hook_handler"],
191
192
  input=event_json,
192
193
  text=True,
193
194
  capture_output=True,
@@ -121,8 +121,10 @@ class LoggerFactory:
121
121
  root_logger.setLevel(desired_level)
122
122
  # else: root logger is suppressed (CRITICAL+1), keep it suppressed
123
123
 
124
- # Remove existing handlers
125
- root_logger.handlers = []
124
+ # Preserve FileHandlers (e.g., hooks logging), only remove StreamHandlers
125
+ root_logger.handlers = [
126
+ h for h in root_logger.handlers if isinstance(h, logging.FileHandler)
127
+ ]
126
128
 
127
129
  # CRITICAL FIX: Don't add handlers if logging is suppressed
128
130
  # If root logger is at CRITICAL+1 (startup suppression), don't add any handlers