claude-mpm 4.1.2__py3-none-any.whl → 4.1.4__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +16 -19
- claude_mpm/agents/MEMORY.md +21 -49
- claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +156 -0
- claude_mpm/agents/templates/api_qa.json +36 -116
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +42 -9
- claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +29 -6
- claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +34 -6
- claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +41 -9
- claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +30 -8
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +2 -2
- claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +29 -6
- claude_mpm/agents/templates/backup/research_memory_efficient.json +2 -2
- claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +41 -9
- claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +23 -7
- claude_mpm/agents/templates/code_analyzer.json +18 -36
- claude_mpm/agents/templates/data_engineer.json +43 -14
- claude_mpm/agents/templates/documentation.json +55 -74
- claude_mpm/agents/templates/engineer.json +57 -40
- claude_mpm/agents/templates/imagemagick.json +7 -2
- claude_mpm/agents/templates/memory_manager.json +1 -1
- claude_mpm/agents/templates/ops.json +36 -4
- claude_mpm/agents/templates/project_organizer.json +23 -71
- claude_mpm/agents/templates/qa.json +34 -2
- claude_mpm/agents/templates/refactoring_engineer.json +9 -5
- claude_mpm/agents/templates/research.json +36 -4
- claude_mpm/agents/templates/security.json +29 -2
- claude_mpm/agents/templates/ticketing.json +3 -3
- claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
- claude_mpm/agents/templates/version_control.json +28 -2
- claude_mpm/agents/templates/web_qa.json +38 -151
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/commands/agent_manager.py +221 -1
- claude_mpm/cli/commands/agents.py +556 -1009
- claude_mpm/cli/commands/memory.py +248 -927
- claude_mpm/cli/commands/run.py +139 -484
- claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
- claude_mpm/cli/startup_logging.py +76 -0
- claude_mpm/core/agent_registry.py +6 -10
- claude_mpm/core/framework_loader.py +205 -595
- claude_mpm/core/log_manager.py +49 -1
- claude_mpm/core/logging_config.py +2 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
- claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
- claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
- claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
- claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
- claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
- claude_mpm/services/agents/memory/memory_file_service.py +103 -0
- claude_mpm/services/agents/memory/memory_format_service.py +201 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
- claude_mpm/services/agents/registry/__init__.py +1 -1
- claude_mpm/services/cli/__init__.py +18 -0
- claude_mpm/services/cli/agent_cleanup_service.py +407 -0
- claude_mpm/services/cli/agent_dependency_service.py +395 -0
- claude_mpm/services/cli/agent_listing_service.py +463 -0
- claude_mpm/services/cli/agent_output_formatter.py +605 -0
- claude_mpm/services/cli/agent_validation_service.py +589 -0
- claude_mpm/services/cli/dashboard_launcher.py +424 -0
- claude_mpm/services/cli/memory_crud_service.py +617 -0
- claude_mpm/services/cli/memory_output_formatter.py +604 -0
- claude_mpm/services/cli/session_manager.py +513 -0
- claude_mpm/services/cli/socketio_manager.py +498 -0
- claude_mpm/services/cli/startup_checker.py +370 -0
- claude_mpm/services/core/cache_manager.py +311 -0
- claude_mpm/services/core/memory_manager.py +637 -0
- claude_mpm/services/core/path_resolver.py +498 -0
- claude_mpm/services/core/service_container.py +520 -0
- claude_mpm/services/core/service_interfaces.py +436 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
- claude_mpm/services/memory/router.py +116 -10
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +86 -55
- claude_mpm/cli/commands/run_config_checker.py +0 -159
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Base agent locator service for finding base agent configuration files."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseAgentLocator:
|
|
10
|
+
"""Service for locating base agent configuration files.
|
|
11
|
+
|
|
12
|
+
This service handles the priority-based search for base_agent.json
|
|
13
|
+
files across multiple possible locations including environment
|
|
14
|
+
variables, development paths, and user overrides.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
18
|
+
"""Initialize the base agent locator.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
logger: Optional logger instance
|
|
22
|
+
"""
|
|
23
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
def find_base_agent_file(self, paths_agents_dir: Path) -> Path:
|
|
26
|
+
"""Find base agent file with priority-based search.
|
|
27
|
+
|
|
28
|
+
Priority order:
|
|
29
|
+
1. Environment variable override (CLAUDE_MPM_BASE_AGENT_PATH)
|
|
30
|
+
2. Current working directory (for local development)
|
|
31
|
+
3. Known development locations
|
|
32
|
+
4. User override location (~/.claude/agents/)
|
|
33
|
+
5. Framework agents directory (from paths)
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
paths_agents_dir: Framework agents directory from paths
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Path to base agent file
|
|
40
|
+
"""
|
|
41
|
+
# Priority 0: Check environment variable override
|
|
42
|
+
env_path = os.environ.get("CLAUDE_MPM_BASE_AGENT_PATH")
|
|
43
|
+
if env_path:
|
|
44
|
+
env_base_agent = Path(env_path)
|
|
45
|
+
if env_base_agent.exists():
|
|
46
|
+
self.logger.info(
|
|
47
|
+
f"Using environment variable base_agent: {env_base_agent}"
|
|
48
|
+
)
|
|
49
|
+
return env_base_agent
|
|
50
|
+
self.logger.warning(
|
|
51
|
+
f"CLAUDE_MPM_BASE_AGENT_PATH set but file doesn't exist: {env_base_agent}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Priority 1: Check current working directory for local development
|
|
55
|
+
cwd = Path.cwd()
|
|
56
|
+
cwd_base_agent = cwd / "src" / "claude_mpm" / "agents" / "base_agent.json"
|
|
57
|
+
if cwd_base_agent.exists():
|
|
58
|
+
self.logger.info(
|
|
59
|
+
f"Using local development base_agent from cwd: {cwd_base_agent}"
|
|
60
|
+
)
|
|
61
|
+
return cwd_base_agent
|
|
62
|
+
|
|
63
|
+
# Priority 2: Check known development locations
|
|
64
|
+
known_dev_paths = [
|
|
65
|
+
Path(
|
|
66
|
+
"/Users/masa/Projects/claude-mpm/src/claude_mpm/agents/base_agent.json"
|
|
67
|
+
),
|
|
68
|
+
Path.home()
|
|
69
|
+
/ "Projects"
|
|
70
|
+
/ "claude-mpm"
|
|
71
|
+
/ "src"
|
|
72
|
+
/ "claude_mpm"
|
|
73
|
+
/ "agents"
|
|
74
|
+
/ "base_agent.json",
|
|
75
|
+
Path.home()
|
|
76
|
+
/ "projects"
|
|
77
|
+
/ "claude-mpm"
|
|
78
|
+
/ "src"
|
|
79
|
+
/ "claude_mpm"
|
|
80
|
+
/ "agents"
|
|
81
|
+
/ "base_agent.json",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
for dev_path in known_dev_paths:
|
|
85
|
+
if dev_path.exists():
|
|
86
|
+
self.logger.info(f"Using development base_agent: {dev_path}")
|
|
87
|
+
return dev_path
|
|
88
|
+
|
|
89
|
+
# Priority 3: Check user override location
|
|
90
|
+
user_base_agent = Path.home() / ".claude" / "agents" / "base_agent.json"
|
|
91
|
+
if user_base_agent.exists():
|
|
92
|
+
self.logger.info(f"Using user override base_agent: {user_base_agent}")
|
|
93
|
+
return user_base_agent
|
|
94
|
+
|
|
95
|
+
# Priority 4: Use framework agents directory (fallback)
|
|
96
|
+
framework_base_agent = paths_agents_dir / "base_agent.json"
|
|
97
|
+
if framework_base_agent.exists():
|
|
98
|
+
self.logger.info(f"Using framework base_agent: {framework_base_agent}")
|
|
99
|
+
return framework_base_agent
|
|
100
|
+
|
|
101
|
+
# If still not found, log all searched locations and raise error
|
|
102
|
+
self.logger.error("Base agent file not found in any location:")
|
|
103
|
+
self.logger.error(f" 1. CWD: {cwd_base_agent}")
|
|
104
|
+
self.logger.error(f" 2. Dev paths: {known_dev_paths}")
|
|
105
|
+
self.logger.error(f" 3. User: {user_base_agent}")
|
|
106
|
+
self.logger.error(f" 4. Framework: {framework_base_agent}")
|
|
107
|
+
|
|
108
|
+
# Final fallback to framework path even if it doesn't exist
|
|
109
|
+
# (will fail later with better error message)
|
|
110
|
+
return framework_base_agent
|
|
111
|
+
|
|
112
|
+
def determine_source_tier(self, templates_dir: Path) -> str:
|
|
113
|
+
"""Determine the source tier for logging.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
templates_dir: Templates directory path
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Source tier string (framework/user/project)
|
|
120
|
+
"""
|
|
121
|
+
templates_str = str(templates_dir.resolve())
|
|
122
|
+
|
|
123
|
+
# Check if this is a user-level installation
|
|
124
|
+
if str(Path.home()) in templates_str and ".claude-mpm" in templates_str:
|
|
125
|
+
return "user"
|
|
126
|
+
|
|
127
|
+
# Check if this is a project-level installation
|
|
128
|
+
if ".claude-mpm" in templates_str:
|
|
129
|
+
return "project"
|
|
130
|
+
|
|
131
|
+
# Default to framework
|
|
132
|
+
return "framework"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Deployment results manager for tracking deployment outcomes."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeploymentResultsManager:
|
|
10
|
+
"""Service for managing deployment results and metrics.
|
|
11
|
+
|
|
12
|
+
This service handles initialization and updates of deployment
|
|
13
|
+
results dictionaries, ensuring consistent structure across
|
|
14
|
+
all deployment operations.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
18
|
+
"""Initialize the deployment results manager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
logger: Optional logger instance
|
|
22
|
+
"""
|
|
23
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Initialize deployment metrics tracking
|
|
26
|
+
self._deployment_metrics = {
|
|
27
|
+
"total_deployments": 0,
|
|
28
|
+
"successful_deployments": 0,
|
|
29
|
+
"failed_deployments": 0,
|
|
30
|
+
"migrations_performed": 0,
|
|
31
|
+
"version_migration_count": 0,
|
|
32
|
+
"agent_type_counts": {},
|
|
33
|
+
"deployment_errors": {},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def initialize_deployment_results(
|
|
37
|
+
self, agents_dir: Path, deployment_start_time: float
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""Initialize the deployment results dictionary.
|
|
40
|
+
|
|
41
|
+
WHY: Consistent result structure ensures all deployment
|
|
42
|
+
operations return the same format for easier processing.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
agents_dir: Target agents directory
|
|
46
|
+
deployment_start_time: Start time for metrics
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Initialized results dictionary
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"target_dir": str(agents_dir),
|
|
53
|
+
"deployed": [],
|
|
54
|
+
"errors": [],
|
|
55
|
+
"skipped": [],
|
|
56
|
+
"updated": [],
|
|
57
|
+
"migrated": [], # Track agents migrated from old format
|
|
58
|
+
"converted": [], # Track YAML to MD conversions
|
|
59
|
+
"repaired": [], # Track agents with repaired frontmatter
|
|
60
|
+
"total": 0,
|
|
61
|
+
# METRICS: Add detailed timing and performance data to results
|
|
62
|
+
"metrics": {
|
|
63
|
+
"start_time": deployment_start_time,
|
|
64
|
+
"end_time": None,
|
|
65
|
+
"duration_ms": None,
|
|
66
|
+
"agent_timings": {}, # Track individual agent deployment times
|
|
67
|
+
"validation_times": {}, # Track template validation times
|
|
68
|
+
"resource_usage": {}, # Could track memory/CPU if needed
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def record_agent_deployment(
|
|
73
|
+
self,
|
|
74
|
+
agent_name: str,
|
|
75
|
+
template_file: Path,
|
|
76
|
+
target_file: Path,
|
|
77
|
+
is_update: bool,
|
|
78
|
+
is_migration: bool,
|
|
79
|
+
reason: str,
|
|
80
|
+
agent_start_time: float,
|
|
81
|
+
results: Dict[str, Any],
|
|
82
|
+
logger: Optional[logging.Logger] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Record deployment metrics and update results.
|
|
85
|
+
|
|
86
|
+
WHY: Centralized metrics recording ensures consistent tracking
|
|
87
|
+
of deployment performance and statistics.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
agent_name: Name of the agent
|
|
91
|
+
template_file: Template file
|
|
92
|
+
target_file: Target file
|
|
93
|
+
is_update: Whether this is an update
|
|
94
|
+
is_migration: Whether this is a migration
|
|
95
|
+
reason: Update/migration reason
|
|
96
|
+
agent_start_time: Start time for this agent
|
|
97
|
+
results: Results dictionary to update
|
|
98
|
+
logger: Optional logger for output
|
|
99
|
+
"""
|
|
100
|
+
logger = logger or self.logger
|
|
101
|
+
|
|
102
|
+
# METRICS: Record deployment time for this agent
|
|
103
|
+
agent_deployment_time = (time.time() - agent_start_time) * 1000 # Convert to ms
|
|
104
|
+
results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
|
|
105
|
+
|
|
106
|
+
# METRICS: Update agent type deployment counts
|
|
107
|
+
self._deployment_metrics["agent_type_counts"][agent_name] = (
|
|
108
|
+
self._deployment_metrics["agent_type_counts"].get(agent_name, 0) + 1
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
deployment_info = {
|
|
112
|
+
"name": agent_name,
|
|
113
|
+
"template": str(template_file),
|
|
114
|
+
"target": str(target_file),
|
|
115
|
+
"deployment_time_ms": agent_deployment_time,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if is_migration:
|
|
119
|
+
deployment_info["reason"] = reason
|
|
120
|
+
results["migrated"].append(deployment_info)
|
|
121
|
+
logger.info(
|
|
122
|
+
f"Successfully migrated agent: {agent_name} to semantic versioning"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# METRICS: Track migration statistics
|
|
126
|
+
self._deployment_metrics["migrations_performed"] += 1
|
|
127
|
+
self._deployment_metrics["version_migration_count"] += 1
|
|
128
|
+
|
|
129
|
+
elif is_update:
|
|
130
|
+
results["updated"].append(deployment_info)
|
|
131
|
+
logger.debug(f"Updated agent: {agent_name}")
|
|
132
|
+
else:
|
|
133
|
+
results["deployed"].append(deployment_info)
|
|
134
|
+
logger.debug(f"Built and deployed agent: {agent_name}")
|
|
135
|
+
|
|
136
|
+
def finalize_results(
|
|
137
|
+
self, results: Dict[str, Any], deployment_start_time: float
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Finalize deployment results with end metrics.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
results: Results dictionary to finalize
|
|
143
|
+
deployment_start_time: Original start time
|
|
144
|
+
"""
|
|
145
|
+
deployment_end_time = time.time()
|
|
146
|
+
deployment_duration = (deployment_end_time - deployment_start_time) * 1000 # ms
|
|
147
|
+
|
|
148
|
+
results["metrics"]["end_time"] = deployment_end_time
|
|
149
|
+
results["metrics"]["duration_ms"] = deployment_duration
|
|
150
|
+
|
|
151
|
+
def update_deployment_metrics(
|
|
152
|
+
self, success: bool, error_type: Optional[str] = None
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Update internal deployment metrics.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
success: Whether deployment succeeded
|
|
158
|
+
error_type: Type of error if failed
|
|
159
|
+
"""
|
|
160
|
+
self._deployment_metrics["total_deployments"] += 1
|
|
161
|
+
|
|
162
|
+
if success:
|
|
163
|
+
self._deployment_metrics["successful_deployments"] += 1
|
|
164
|
+
else:
|
|
165
|
+
self._deployment_metrics["failed_deployments"] += 1
|
|
166
|
+
if error_type:
|
|
167
|
+
self._deployment_metrics["deployment_errors"][error_type] = (
|
|
168
|
+
self._deployment_metrics["deployment_errors"].get(error_type, 0) + 1
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def get_deployment_metrics(self) -> Dict[str, Any]:
|
|
172
|
+
"""Get current deployment metrics."""
|
|
173
|
+
return self._deployment_metrics.copy()
|
|
174
|
+
|
|
175
|
+
def reset_metrics(self) -> None:
|
|
176
|
+
"""Reset deployment metrics."""
|
|
177
|
+
self._deployment_metrics = {
|
|
178
|
+
"total_deployments": 0,
|
|
179
|
+
"successful_deployments": 0,
|
|
180
|
+
"failed_deployments": 0,
|
|
181
|
+
"migrations_performed": 0,
|
|
182
|
+
"version_migration_count": 0,
|
|
183
|
+
"agent_type_counts": {},
|
|
184
|
+
"deployment_errors": {},
|
|
185
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Single agent deployer for deploying individual agents."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from claude_mpm.core.exceptions import AgentDeploymentError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SingleAgentDeployer:
|
|
12
|
+
"""Service for deploying individual agents.
|
|
13
|
+
|
|
14
|
+
This service handles the deployment of single agent templates,
|
|
15
|
+
including version checking, building, and writing agent files.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
template_builder,
|
|
21
|
+
version_manager,
|
|
22
|
+
results_manager,
|
|
23
|
+
logger: Optional[logging.Logger] = None,
|
|
24
|
+
):
|
|
25
|
+
"""Initialize the single agent deployer.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
template_builder: Template builder service
|
|
29
|
+
version_manager: Version manager service
|
|
30
|
+
results_manager: Results manager service
|
|
31
|
+
logger: Optional logger instance
|
|
32
|
+
"""
|
|
33
|
+
self.template_builder = template_builder
|
|
34
|
+
self.version_manager = version_manager
|
|
35
|
+
self.results_manager = results_manager
|
|
36
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
def deploy_single_agent(
|
|
39
|
+
self,
|
|
40
|
+
template_file: Path,
|
|
41
|
+
agents_dir: Path,
|
|
42
|
+
base_agent_data: dict,
|
|
43
|
+
base_agent_version: tuple,
|
|
44
|
+
force_rebuild: bool,
|
|
45
|
+
deployment_mode: str,
|
|
46
|
+
results: Dict[str, Any],
|
|
47
|
+
source_info: str = "unknown",
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Deploy a single agent template.
|
|
50
|
+
|
|
51
|
+
WHY: Extracting single agent deployment logic reduces complexity
|
|
52
|
+
and makes the main deployment loop more readable.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
template_file: Agent template file
|
|
56
|
+
agents_dir: Target agents directory
|
|
57
|
+
base_agent_data: Base agent data
|
|
58
|
+
base_agent_version: Base agent version
|
|
59
|
+
force_rebuild: Whether to force rebuild
|
|
60
|
+
deployment_mode: Deployment mode (update/project)
|
|
61
|
+
results: Results dictionary to update
|
|
62
|
+
source_info: Source of the agent (system/project/user)
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
# METRICS: Track individual agent deployment time
|
|
66
|
+
agent_start_time = time.time()
|
|
67
|
+
|
|
68
|
+
agent_name = template_file.stem
|
|
69
|
+
target_file = agents_dir / f"{agent_name}.md"
|
|
70
|
+
|
|
71
|
+
# Check if agent needs update
|
|
72
|
+
needs_update, is_migration, reason = self._check_update_status(
|
|
73
|
+
target_file,
|
|
74
|
+
template_file,
|
|
75
|
+
base_agent_version,
|
|
76
|
+
force_rebuild,
|
|
77
|
+
deployment_mode,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Skip if exists and doesn't need update (only in update mode)
|
|
81
|
+
if (
|
|
82
|
+
target_file.exists()
|
|
83
|
+
and not needs_update
|
|
84
|
+
and deployment_mode != "project"
|
|
85
|
+
):
|
|
86
|
+
results["skipped"].append(agent_name)
|
|
87
|
+
self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Build the agent file as markdown with YAML frontmatter
|
|
91
|
+
agent_content = self.template_builder.build_agent_markdown(
|
|
92
|
+
agent_name, template_file, base_agent_data, source_info
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Write the agent file
|
|
96
|
+
is_update = target_file.exists()
|
|
97
|
+
target_file.write_text(agent_content)
|
|
98
|
+
|
|
99
|
+
# Record metrics and update results
|
|
100
|
+
self.results_manager.record_agent_deployment(
|
|
101
|
+
agent_name,
|
|
102
|
+
template_file,
|
|
103
|
+
target_file,
|
|
104
|
+
is_update,
|
|
105
|
+
is_migration,
|
|
106
|
+
reason,
|
|
107
|
+
agent_start_time,
|
|
108
|
+
results,
|
|
109
|
+
self.logger,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except AgentDeploymentError as e:
|
|
113
|
+
# Re-raise our custom exceptions
|
|
114
|
+
self.logger.error(str(e))
|
|
115
|
+
results["errors"].append(str(e))
|
|
116
|
+
except Exception as e:
|
|
117
|
+
# Wrap generic exceptions with context
|
|
118
|
+
error_msg = f"Failed to build {template_file.name}: {e}"
|
|
119
|
+
self.logger.error(error_msg)
|
|
120
|
+
results["errors"].append(error_msg)
|
|
121
|
+
|
|
122
|
+
def _check_update_status(
|
|
123
|
+
self,
|
|
124
|
+
target_file: Path,
|
|
125
|
+
template_file: Path,
|
|
126
|
+
base_agent_version: tuple,
|
|
127
|
+
force_rebuild: bool,
|
|
128
|
+
deployment_mode: str,
|
|
129
|
+
) -> Tuple[bool, bool, str]:
|
|
130
|
+
"""Check if agent needs update and determine status.
|
|
131
|
+
|
|
132
|
+
WHY: Centralized update checking logic ensures consistent
|
|
133
|
+
version comparison and migration detection.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
target_file: Target agent file
|
|
137
|
+
template_file: Template file
|
|
138
|
+
base_agent_version: Base agent version
|
|
139
|
+
force_rebuild: Whether to force rebuild
|
|
140
|
+
deployment_mode: Deployment mode
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Tuple of (needs_update, is_migration, reason)
|
|
144
|
+
"""
|
|
145
|
+
needs_update = force_rebuild
|
|
146
|
+
is_migration = False
|
|
147
|
+
reason = ""
|
|
148
|
+
|
|
149
|
+
# In project deployment mode, always deploy regardless of version
|
|
150
|
+
if deployment_mode == "project":
|
|
151
|
+
if target_file.exists():
|
|
152
|
+
needs_update = True
|
|
153
|
+
self.logger.debug(
|
|
154
|
+
f"Project deployment mode: will deploy {template_file.stem}"
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
needs_update = True
|
|
158
|
+
elif not needs_update and target_file.exists():
|
|
159
|
+
# In update mode, check version compatibility
|
|
160
|
+
needs_update, reason = self.version_manager.check_agent_needs_update(
|
|
161
|
+
target_file, template_file, base_agent_version
|
|
162
|
+
)
|
|
163
|
+
if needs_update:
|
|
164
|
+
# Check if this is a migration from old format
|
|
165
|
+
if "migration needed" in reason:
|
|
166
|
+
is_migration = True
|
|
167
|
+
self.logger.info(f"Migrating agent {template_file.stem}: {reason}")
|
|
168
|
+
else:
|
|
169
|
+
self.logger.info(
|
|
170
|
+
f"Agent {template_file.stem} needs update: {reason}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return needs_update, is_migration, reason
|
|
174
|
+
|
|
175
|
+
def deploy_agent(
|
|
176
|
+
self,
|
|
177
|
+
agent_name: str,
|
|
178
|
+
templates_dir: Path,
|
|
179
|
+
target_dir: Path,
|
|
180
|
+
base_agent_path: Path,
|
|
181
|
+
force_rebuild: bool = False,
|
|
182
|
+
working_directory: Optional[Path] = None,
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""Deploy a single agent to the specified directory.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
agent_name: Name of the agent to deploy
|
|
188
|
+
templates_dir: Directory containing templates
|
|
189
|
+
target_dir: Target directory for deployment (Path object)
|
|
190
|
+
base_agent_path: Path to base agent configuration
|
|
191
|
+
force_rebuild: Whether to force rebuild even if version is current
|
|
192
|
+
working_directory: Working directory for determining agent source
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if deployment was successful, False otherwise
|
|
196
|
+
|
|
197
|
+
WHY: Single agent deployment because:
|
|
198
|
+
- Users may want to deploy specific agents only
|
|
199
|
+
- Reduces deployment time for targeted updates
|
|
200
|
+
- Enables selective agent management in projects
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
# Find the template file
|
|
204
|
+
template_file = templates_dir / f"{agent_name}.json"
|
|
205
|
+
if not template_file.exists():
|
|
206
|
+
self.logger.error(f"Agent template not found: {agent_name}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# Ensure target directory exists
|
|
210
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
|
|
212
|
+
# Build and deploy the agent
|
|
213
|
+
target_file = target_dir / f"{agent_name}.md"
|
|
214
|
+
|
|
215
|
+
# Check if update is needed
|
|
216
|
+
if not force_rebuild and target_file.exists():
|
|
217
|
+
# Load base agent data for version checking
|
|
218
|
+
base_agent_data = {}
|
|
219
|
+
base_agent_version = (0, 0, 0)
|
|
220
|
+
if base_agent_path.exists():
|
|
221
|
+
try:
|
|
222
|
+
import json
|
|
223
|
+
|
|
224
|
+
base_agent_data = json.loads(base_agent_path.read_text())
|
|
225
|
+
base_agent_version = self.version_manager.parse_version(
|
|
226
|
+
base_agent_data.get("base_version")
|
|
227
|
+
or base_agent_data.get("version", 0)
|
|
228
|
+
)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
self.logger.warning(
|
|
231
|
+
f"Could not load base agent for version check: {e}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
needs_update, reason = self.version_manager.check_agent_needs_update(
|
|
235
|
+
target_file, template_file, base_agent_version
|
|
236
|
+
)
|
|
237
|
+
if not needs_update:
|
|
238
|
+
self.logger.info(f"Agent {agent_name} is up to date")
|
|
239
|
+
return True
|
|
240
|
+
self.logger.info(f"Updating agent {agent_name}: {reason}")
|
|
241
|
+
|
|
242
|
+
# Load base agent data for building
|
|
243
|
+
base_agent_data = {}
|
|
244
|
+
if base_agent_path.exists():
|
|
245
|
+
try:
|
|
246
|
+
import json
|
|
247
|
+
|
|
248
|
+
base_agent_data = json.loads(base_agent_path.read_text())
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.logger.warning(f"Could not load base agent: {e}")
|
|
251
|
+
|
|
252
|
+
# Build the agent markdown
|
|
253
|
+
# For single agent deployment, determine source from template location
|
|
254
|
+
source_info = self._determine_agent_source(
|
|
255
|
+
template_file, working_directory or Path.cwd()
|
|
256
|
+
)
|
|
257
|
+
agent_content = self.template_builder.build_agent_markdown(
|
|
258
|
+
agent_name, template_file, base_agent_data, source_info
|
|
259
|
+
)
|
|
260
|
+
if not agent_content:
|
|
261
|
+
self.logger.error(f"Failed to build agent content for {agent_name}")
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
# Write to target file
|
|
265
|
+
target_file.write_text(agent_content)
|
|
266
|
+
self.logger.info(
|
|
267
|
+
f"Successfully deployed agent: {agent_name} to {target_file}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
except AgentDeploymentError:
|
|
273
|
+
# Re-raise our custom exceptions
|
|
274
|
+
raise
|
|
275
|
+
except Exception as e:
|
|
276
|
+
# Wrap generic exceptions with context
|
|
277
|
+
raise AgentDeploymentError(
|
|
278
|
+
f"Failed to deploy agent {agent_name}",
|
|
279
|
+
context={"agent_name": agent_name, "error": str(e)},
|
|
280
|
+
) from e
|
|
281
|
+
|
|
282
|
+
def _determine_agent_source(
|
|
283
|
+
self, template_path: Path, working_directory: Path
|
|
284
|
+
) -> str:
|
|
285
|
+
"""Determine the source of an agent from its template path.
|
|
286
|
+
|
|
287
|
+
WHY: When deploying single agents, we need to track their source
|
|
288
|
+
for proper version management and debugging.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
template_path: Path to the agent template
|
|
292
|
+
working_directory: Current working directory
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Source string (system/project/user/unknown)
|
|
296
|
+
"""
|
|
297
|
+
template_str = str(template_path.resolve())
|
|
298
|
+
|
|
299
|
+
# Check if it's a system template
|
|
300
|
+
if (
|
|
301
|
+
"/claude_mpm/agents/templates/" in template_str
|
|
302
|
+
or "/src/claude_mpm/agents/templates/" in template_str
|
|
303
|
+
):
|
|
304
|
+
return "system"
|
|
305
|
+
|
|
306
|
+
# Check if it's a project agent
|
|
307
|
+
if "/.claude-mpm/agents/" in template_str:
|
|
308
|
+
# Check if it's in the current working directory
|
|
309
|
+
if str(working_directory) in template_str:
|
|
310
|
+
return "project"
|
|
311
|
+
# Check if it's in user home
|
|
312
|
+
if str(Path.home()) in template_str:
|
|
313
|
+
return "user"
|
|
314
|
+
|
|
315
|
+
return "unknown"
|