claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__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.
Files changed (53) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/engineer.json +33 -11
  3. claude_mpm/cli/commands/agents.py +556 -1009
  4. claude_mpm/cli/commands/memory.py +248 -927
  5. claude_mpm/cli/commands/run.py +139 -484
  6. claude_mpm/cli/startup_logging.py +76 -0
  7. claude_mpm/core/agent_registry.py +6 -10
  8. claude_mpm/core/framework_loader.py +114 -595
  9. claude_mpm/core/logging_config.py +2 -4
  10. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  11. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  12. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  13. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  14. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  15. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  16. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  17. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  18. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  19. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  20. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  21. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  22. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  23. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  24. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  25. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  26. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  28. claude_mpm/services/agents/registry/__init__.py +1 -1
  29. claude_mpm/services/cli/__init__.py +18 -0
  30. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  31. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  32. claude_mpm/services/cli/agent_listing_service.py +463 -0
  33. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  34. claude_mpm/services/cli/agent_validation_service.py +589 -0
  35. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  36. claude_mpm/services/cli/memory_crud_service.py +617 -0
  37. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  38. claude_mpm/services/cli/session_manager.py +513 -0
  39. claude_mpm/services/cli/socketio_manager.py +498 -0
  40. claude_mpm/services/cli/startup_checker.py +370 -0
  41. claude_mpm/services/core/cache_manager.py +311 -0
  42. claude_mpm/services/core/memory_manager.py +637 -0
  43. claude_mpm/services/core/path_resolver.py +498 -0
  44. claude_mpm/services/core/service_container.py +520 -0
  45. claude_mpm/services/core/service_interfaces.py +436 -0
  46. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  47. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
  48. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
  49. claude_mpm/cli/commands/run_config_checker.py +0 -159
  50. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
  51. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
  52. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
  53. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.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"