claude-mpm 4.18.0__py3-none-any.whl → 4.20.0__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/agents/BASE_ENGINEER.md +286 -0
- claude_mpm/agents/BASE_PM.md +238 -37
- claude_mpm/agents/PM_INSTRUCTIONS.md +40 -0
- claude_mpm/agents/templates/engineer.json +5 -1
- claude_mpm/agents/templates/python_engineer.json +8 -3
- claude_mpm/agents/templates/rust_engineer.json +12 -7
- claude_mpm/cli/commands/mpm_init.py +109 -24
- claude_mpm/commands/mpm-init.md +112 -6
- claude_mpm/core/config.py +42 -0
- claude_mpm/hooks/__init__.py +8 -0
- claude_mpm/hooks/session_resume_hook.py +121 -0
- claude_mpm/services/agents/deployment/agent_validator.py +17 -1
- claude_mpm/services/cli/resume_service.py +617 -0
- claude_mpm/services/cli/session_manager.py +87 -0
- claude_mpm/services/cli/session_resume_helper.py +352 -0
- {claude_mpm-4.18.0.dist-info → claude_mpm-4.20.0.dist-info}/METADATA +19 -4
- {claude_mpm-4.18.0.dist-info → claude_mpm-4.20.0.dist-info}/RECORD +22 -19
- {claude_mpm-4.18.0.dist-info → claude_mpm-4.20.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.18.0.dist-info → claude_mpm-4.20.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.18.0.dist-info → claude_mpm-4.20.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.18.0.dist-info → claude_mpm-4.20.0.dist-info}/top_level.txt +0 -0
claude_mpm/core/config.py
CHANGED
|
@@ -578,6 +578,11 @@ class Config:
|
|
|
578
578
|
"[PM-REMINDER] Your role is coordination and management",
|
|
579
579
|
],
|
|
580
580
|
},
|
|
581
|
+
# Session management configuration
|
|
582
|
+
"session": {
|
|
583
|
+
"auto_save": True, # Enable automatic session saving
|
|
584
|
+
"save_interval": 300, # Auto-save interval in seconds (5 minutes)
|
|
585
|
+
},
|
|
581
586
|
}
|
|
582
587
|
|
|
583
588
|
# Apply defaults for missing keys
|
|
@@ -588,6 +593,9 @@ class Config:
|
|
|
588
593
|
# Validate health and recovery configuration
|
|
589
594
|
self._validate_health_recovery_config()
|
|
590
595
|
|
|
596
|
+
# Validate session configuration
|
|
597
|
+
self._validate_session_config()
|
|
598
|
+
|
|
591
599
|
def get(self, key: str, default: Any = None) -> Any:
|
|
592
600
|
"""Get configuration value."""
|
|
593
601
|
# Support nested keys with dot notation
|
|
@@ -764,6 +772,40 @@ class Config:
|
|
|
764
772
|
except Exception as e:
|
|
765
773
|
logger.error(f"Error validating health/recovery configuration: {e}")
|
|
766
774
|
|
|
775
|
+
def _validate_session_config(self) -> None:
|
|
776
|
+
"""Validate session management configuration."""
|
|
777
|
+
try:
|
|
778
|
+
session_config = self.get("session", {})
|
|
779
|
+
|
|
780
|
+
# Validate save_interval range (60-1800 seconds)
|
|
781
|
+
save_interval = session_config.get("save_interval", 300)
|
|
782
|
+
if not isinstance(save_interval, int):
|
|
783
|
+
logger.warning(
|
|
784
|
+
f"Session save_interval must be integer, got {type(save_interval).__name__}, using default 300"
|
|
785
|
+
)
|
|
786
|
+
self.set("session.save_interval", 300)
|
|
787
|
+
elif save_interval < 60:
|
|
788
|
+
logger.warning(
|
|
789
|
+
f"Session save_interval must be at least 60 seconds, got {save_interval}, using 60"
|
|
790
|
+
)
|
|
791
|
+
self.set("session.save_interval", 60)
|
|
792
|
+
elif save_interval > 1800:
|
|
793
|
+
logger.warning(
|
|
794
|
+
f"Session save_interval must be at most 1800 seconds (30 min), got {save_interval}, using 1800"
|
|
795
|
+
)
|
|
796
|
+
self.set("session.save_interval", 1800)
|
|
797
|
+
|
|
798
|
+
# Validate auto_save is boolean
|
|
799
|
+
auto_save = session_config.get("auto_save", True)
|
|
800
|
+
if not isinstance(auto_save, bool):
|
|
801
|
+
logger.warning(
|
|
802
|
+
f"Session auto_save must be boolean, got {type(auto_save).__name__}, using True"
|
|
803
|
+
)
|
|
804
|
+
self.set("session.auto_save", True)
|
|
805
|
+
|
|
806
|
+
except Exception as e:
|
|
807
|
+
logger.error(f"Error validating session configuration: {e}")
|
|
808
|
+
|
|
767
809
|
def get_health_monitoring_config(self) -> Dict[str, Any]:
|
|
768
810
|
"""Get health monitoring configuration with defaults."""
|
|
769
811
|
base_config = {
|
claude_mpm/hooks/__init__.py
CHANGED
|
@@ -12,6 +12,11 @@ from .failure_learning import (
|
|
|
12
12
|
from .kuzu_enrichment_hook import KuzuEnrichmentHook, get_kuzu_enrichment_hook
|
|
13
13
|
from .kuzu_memory_hook import KuzuMemoryHook, get_kuzu_memory_hook
|
|
14
14
|
from .kuzu_response_hook import KuzuResponseHook, get_kuzu_response_hook
|
|
15
|
+
from .session_resume_hook import (
|
|
16
|
+
SessionResumeStartupHook,
|
|
17
|
+
get_session_resume_hook,
|
|
18
|
+
trigger_session_resume_check,
|
|
19
|
+
)
|
|
15
20
|
|
|
16
21
|
__all__ = [
|
|
17
22
|
"BaseHook",
|
|
@@ -24,10 +29,13 @@ __all__ = [
|
|
|
24
29
|
"KuzuMemoryHook",
|
|
25
30
|
"KuzuResponseHook",
|
|
26
31
|
"LearningExtractionHook",
|
|
32
|
+
"SessionResumeStartupHook",
|
|
27
33
|
"get_failure_detection_hook",
|
|
28
34
|
"get_fix_detection_hook",
|
|
29
35
|
"get_kuzu_enrichment_hook",
|
|
30
36
|
"get_kuzu_memory_hook",
|
|
31
37
|
"get_kuzu_response_hook",
|
|
32
38
|
"get_learning_extraction_hook",
|
|
39
|
+
"get_session_resume_hook",
|
|
40
|
+
"trigger_session_resume_check",
|
|
33
41
|
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Session Resume Startup Hook.
|
|
2
|
+
|
|
3
|
+
WHY: This hook automatically checks for paused sessions on PM startup and displays
|
|
4
|
+
resume context to help users continue their work seamlessly.
|
|
5
|
+
|
|
6
|
+
DESIGN DECISIONS:
|
|
7
|
+
- Runs automatically on PM startup
|
|
8
|
+
- Non-blocking: doesn't prevent PM from starting if check fails
|
|
9
|
+
- Displays context to stdout for user visibility
|
|
10
|
+
- Integrates with existing session pause/resume infrastructure
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
from claude_mpm.core.logger import get_logger
|
|
17
|
+
from claude_mpm.services.cli.session_resume_helper import SessionResumeHelper
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SessionResumeStartupHook:
|
|
23
|
+
"""Hook for automatic session resume detection on PM startup."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, project_path: Optional[Path] = None):
|
|
26
|
+
"""Initialize the session resume hook.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
project_path: Project root path (default: current directory)
|
|
30
|
+
"""
|
|
31
|
+
self.project_path = project_path or Path.cwd()
|
|
32
|
+
self.resume_helper = SessionResumeHelper(self.project_path)
|
|
33
|
+
self._session_displayed = False
|
|
34
|
+
|
|
35
|
+
def on_pm_startup(self) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""Execute on PM startup to check for paused sessions.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Session data if paused session found, None otherwise
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
# Check if we already displayed a session in this process
|
|
43
|
+
if self._session_displayed:
|
|
44
|
+
logger.debug("Session already displayed, skipping")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Check for paused sessions
|
|
48
|
+
session_data = self.resume_helper.check_and_display_resume_prompt()
|
|
49
|
+
|
|
50
|
+
if session_data:
|
|
51
|
+
self._session_displayed = True
|
|
52
|
+
logger.info("Paused session context displayed to user")
|
|
53
|
+
|
|
54
|
+
return session_data
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Failed to check for paused sessions: {e}", exc_info=True)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def get_session_count(self) -> int:
|
|
61
|
+
"""Get count of paused sessions.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Number of paused sessions
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
return self.resume_helper.get_session_count()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Failed to get session count: {e}")
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
def clear_displayed_session(self, session_data: Dict[str, Any]) -> bool:
|
|
73
|
+
"""Clear a session after it has been displayed and user has acknowledged.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
session_data: Session data to clear
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if successfully cleared, False otherwise
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
return self.resume_helper.clear_session(session_data)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Failed to clear session: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Global hook instance
|
|
89
|
+
_session_resume_hook: Optional[SessionResumeStartupHook] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_session_resume_hook(
|
|
93
|
+
project_path: Optional[Path] = None,
|
|
94
|
+
) -> SessionResumeStartupHook:
|
|
95
|
+
"""Get or create the global session resume hook instance.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
project_path: Project root path (default: current directory)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
SessionResumeStartupHook instance
|
|
102
|
+
"""
|
|
103
|
+
global _session_resume_hook
|
|
104
|
+
|
|
105
|
+
if _session_resume_hook is None:
|
|
106
|
+
_session_resume_hook = SessionResumeStartupHook(project_path)
|
|
107
|
+
logger.debug("Created session resume hook instance")
|
|
108
|
+
|
|
109
|
+
return _session_resume_hook
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def trigger_session_resume_check() -> Optional[Dict[str, Any]]:
|
|
113
|
+
"""Trigger a session resume check (convenience function).
|
|
114
|
+
|
|
115
|
+
This is the main entry point for PM startup integration.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Session data if found, None otherwise
|
|
119
|
+
"""
|
|
120
|
+
hook = get_session_resume_hook()
|
|
121
|
+
return hook.on_pm_startup()
|
|
@@ -329,10 +329,26 @@ class AgentValidator:
|
|
|
329
329
|
"type": "agent", # Default type
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
# Extract from YAML frontmatter
|
|
332
|
+
# Extract ONLY from YAML frontmatter (between --- markers)
|
|
333
333
|
lines = content.split("\n")
|
|
334
|
+
in_frontmatter = False
|
|
335
|
+
frontmatter_ended = False
|
|
336
|
+
|
|
334
337
|
for line in lines:
|
|
335
338
|
stripped_line = line.strip()
|
|
339
|
+
|
|
340
|
+
# Track frontmatter boundaries
|
|
341
|
+
if stripped_line == "---":
|
|
342
|
+
if not in_frontmatter:
|
|
343
|
+
in_frontmatter = True
|
|
344
|
+
continue
|
|
345
|
+
frontmatter_ended = True
|
|
346
|
+
break # Stop parsing after frontmatter ends
|
|
347
|
+
|
|
348
|
+
# Only parse within frontmatter
|
|
349
|
+
if not in_frontmatter or frontmatter_ended:
|
|
350
|
+
continue
|
|
351
|
+
|
|
336
352
|
if stripped_line.startswith("name:"):
|
|
337
353
|
agent_info["name"] = stripped_line.split(":", 1)[1].strip().strip("\"'")
|
|
338
354
|
elif stripped_line.startswith("description:"):
|