claude-mpm 4.0.31__py3-none-any.whl → 4.0.34__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 +33 -25
- claude_mpm/agents/INSTRUCTIONS.md +14 -10
- claude_mpm/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +63 -26
- claude_mpm/cli/commands/agent_manager.py +10 -8
- claude_mpm/core/framework_loader.py +272 -113
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +85 -6
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +86 -37
- claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -13
- claude_mpm/services/agents/memory/agent_memory_manager.py +141 -184
- claude_mpm/services/agents/memory/content_manager.py +182 -232
- claude_mpm/services/agents/memory/template_generator.py +4 -40
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/event_bus.py +334 -0
- claude_mpm/services/event_bus/relay.py +301 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +78 -20
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +71 -50
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
|
@@ -385,12 +385,23 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
385
385
|
|
|
386
386
|
if use_multi_source:
|
|
387
387
|
# Use multi-source deployment to get highest version agents
|
|
388
|
-
template_files, agent_sources = self._get_multi_source_templates(
|
|
388
|
+
template_files, agent_sources, cleanup_results = self._get_multi_source_templates(
|
|
389
389
|
excluded_agents, config, agents_dir, force_rebuild
|
|
390
390
|
)
|
|
391
391
|
results["total"] = len(template_files)
|
|
392
392
|
results["multi_source"] = True
|
|
393
393
|
results["agent_sources"] = agent_sources
|
|
394
|
+
results["cleanup"] = cleanup_results
|
|
395
|
+
|
|
396
|
+
# Log cleanup results if any agents were removed
|
|
397
|
+
if cleanup_results.get("removed"):
|
|
398
|
+
self.logger.info(
|
|
399
|
+
f"Cleaned up {len(cleanup_results['removed'])} outdated user agents"
|
|
400
|
+
)
|
|
401
|
+
for removed in cleanup_results["removed"]:
|
|
402
|
+
self.logger.debug(
|
|
403
|
+
f" - Removed: {removed['name']} v{removed['version']} ({removed['reason']})"
|
|
404
|
+
)
|
|
394
405
|
else:
|
|
395
406
|
# Get and filter template files from single source
|
|
396
407
|
template_files = self._get_filtered_templates(excluded_agents, config)
|
|
@@ -416,8 +427,12 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
416
427
|
source_info=source_info,
|
|
417
428
|
)
|
|
418
429
|
|
|
419
|
-
#
|
|
420
|
-
|
|
430
|
+
# CRITICAL: System instructions deployment disabled to prevent automatic file creation
|
|
431
|
+
# The system should NEVER automatically write INSTRUCTIONS.md, MEMORY.md, WORKFLOW.md
|
|
432
|
+
# to .claude/ directory. These files should only be created when explicitly requested
|
|
433
|
+
# by the user through agent-manager commands.
|
|
434
|
+
# See deploy_system_instructions_explicit() for manual deployment.
|
|
435
|
+
# self._deploy_system_instructions(agents_dir, force_rebuild, results)
|
|
421
436
|
|
|
422
437
|
self.logger.info(
|
|
423
438
|
f"Deployed {len(results['deployed'])} agents, "
|
|
@@ -512,11 +527,11 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
512
527
|
return False
|
|
513
528
|
|
|
514
529
|
# Ensure target directory exists
|
|
515
|
-
|
|
516
|
-
|
|
530
|
+
# target_dir should already be the agents directory
|
|
531
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
517
532
|
|
|
518
533
|
# Build and deploy the agent
|
|
519
|
-
target_file =
|
|
534
|
+
target_file = target_dir / f"{agent_name}.md"
|
|
520
535
|
|
|
521
536
|
# Check if update is needed
|
|
522
537
|
if not force_rebuild and target_file.exists():
|
|
@@ -614,9 +629,64 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
614
629
|
|
|
615
630
|
deployer = SystemInstructionsDeployer(self.logger, self.working_directory)
|
|
616
631
|
deployer.deploy_system_instructions(
|
|
617
|
-
target_dir, force_rebuild, results
|
|
632
|
+
target_dir, force_rebuild, results
|
|
618
633
|
)
|
|
619
634
|
|
|
635
|
+
def deploy_system_instructions_explicit(
|
|
636
|
+
self, target_dir: Optional[Path] = None, force_rebuild: bool = False
|
|
637
|
+
) -> Dict[str, Any]:
|
|
638
|
+
"""
|
|
639
|
+
Explicitly deploy system instructions when requested by user.
|
|
640
|
+
|
|
641
|
+
This method should ONLY be called when the user explicitly requests
|
|
642
|
+
deployment of system instructions through agent-manager commands.
|
|
643
|
+
It will deploy INSTRUCTIONS.md, MEMORY.md, and WORKFLOW.md to .claude/
|
|
644
|
+
directory in the project.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
target_dir: Target directory for deployment (ignored - always uses .claude/)
|
|
648
|
+
force_rebuild: Force rebuild even if files exist
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
Dict with deployment results
|
|
652
|
+
"""
|
|
653
|
+
results = {
|
|
654
|
+
"deployed": [],
|
|
655
|
+
"updated": [],
|
|
656
|
+
"skipped": [],
|
|
657
|
+
"errors": [],
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
# Always use project's .claude directory
|
|
662
|
+
target_dir = self.working_directory / ".claude"
|
|
663
|
+
|
|
664
|
+
# Ensure directory exists
|
|
665
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
666
|
+
|
|
667
|
+
# Deploy using the deployer (targeting .claude/)
|
|
668
|
+
from .system_instructions_deployer import SystemInstructionsDeployer
|
|
669
|
+
deployer = SystemInstructionsDeployer(self.logger, self.working_directory)
|
|
670
|
+
|
|
671
|
+
# Deploy to .claude directory
|
|
672
|
+
deployer.deploy_system_instructions(
|
|
673
|
+
target_dir, force_rebuild, results
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
self.logger.info(
|
|
677
|
+
f"Explicitly deployed system instructions to {target_dir}: "
|
|
678
|
+
f"deployed={len(results['deployed'])}, "
|
|
679
|
+
f"updated={len(results['updated'])}, "
|
|
680
|
+
f"skipped={len(results['skipped'])}"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
error_msg = f"Failed to deploy system instructions: {e}"
|
|
685
|
+
self.logger.error(error_msg)
|
|
686
|
+
results["errors"].append(error_msg)
|
|
687
|
+
|
|
688
|
+
return results
|
|
689
|
+
|
|
620
690
|
def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
|
|
621
691
|
"""Convert existing YAML agent files to MD format with YAML frontmatter."""
|
|
622
692
|
return self.format_converter.convert_yaml_to_md(target_dir)
|
|
@@ -701,32 +771,9 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
701
771
|
"""Determine the correct agents directory based on input."""
|
|
702
772
|
from .agents_directory_resolver import AgentsDirectoryResolver
|
|
703
773
|
|
|
704
|
-
resolver = AgentsDirectoryResolver(
|
|
705
|
-
self.working_directory,
|
|
706
|
-
self._is_system_agent_deployment(),
|
|
707
|
-
self._is_project_specific_deployment(),
|
|
708
|
-
)
|
|
774
|
+
resolver = AgentsDirectoryResolver(self.working_directory)
|
|
709
775
|
return resolver.determine_agents_directory(target_dir)
|
|
710
776
|
|
|
711
|
-
def _is_system_agent_deployment(self) -> bool:
|
|
712
|
-
"""Check if this is a deployment of system agents."""
|
|
713
|
-
from .deployment_type_detector import DeploymentTypeDetector
|
|
714
|
-
|
|
715
|
-
return DeploymentTypeDetector.is_system_agent_deployment(self.templates_dir)
|
|
716
|
-
|
|
717
|
-
def _is_project_specific_deployment(self) -> bool:
|
|
718
|
-
"""Check if deploying project-specific agents."""
|
|
719
|
-
from .deployment_type_detector import DeploymentTypeDetector
|
|
720
|
-
|
|
721
|
-
return DeploymentTypeDetector.is_project_specific_deployment(
|
|
722
|
-
self.templates_dir, self.working_directory
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
def _is_user_custom_deployment(self) -> bool:
|
|
726
|
-
"""Check if deploying user custom agents."""
|
|
727
|
-
from .deployment_type_detector import DeploymentTypeDetector
|
|
728
|
-
|
|
729
|
-
return DeploymentTypeDetector.is_user_custom_deployment(self.templates_dir)
|
|
730
777
|
|
|
731
778
|
def _initialize_deployment_results(
|
|
732
779
|
self, agents_dir: Path, deployment_start_time: float
|
|
@@ -1062,7 +1109,7 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
1062
1109
|
def _get_multi_source_templates(
|
|
1063
1110
|
self, excluded_agents: List[str], config: Config, agents_dir: Path,
|
|
1064
1111
|
force_rebuild: bool = False
|
|
1065
|
-
) -> Tuple[List[Path], Dict[str, str]]:
|
|
1112
|
+
) -> Tuple[List[Path], Dict[str, str], Dict[str, Any]]:
|
|
1066
1113
|
"""Get agent templates from multiple sources with version comparison.
|
|
1067
1114
|
|
|
1068
1115
|
WHY: This method uses the multi-source service to discover agents
|
|
@@ -1072,9 +1119,10 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
1072
1119
|
excluded_agents: List of agents to exclude
|
|
1073
1120
|
config: Configuration object
|
|
1074
1121
|
agents_dir: Target deployment directory
|
|
1122
|
+
force_rebuild: Whether to force rebuild
|
|
1075
1123
|
|
|
1076
1124
|
Returns:
|
|
1077
|
-
Tuple of (template_files, agent_sources)
|
|
1125
|
+
Tuple of (template_files, agent_sources, cleanup_results)
|
|
1078
1126
|
"""
|
|
1079
1127
|
# Determine source directories
|
|
1080
1128
|
system_templates_dir = self.templates_dir
|
|
@@ -1095,14 +1143,15 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
1095
1143
|
user_agents_dir = potential_user_dir
|
|
1096
1144
|
self.logger.info(f"Found user agents at: {user_agents_dir}")
|
|
1097
1145
|
|
|
1098
|
-
# Get agents with version comparison
|
|
1099
|
-
agents_to_deploy, agent_sources = self.multi_source_service.get_agents_for_deployment(
|
|
1146
|
+
# Get agents with version comparison and cleanup
|
|
1147
|
+
agents_to_deploy, agent_sources, cleanup_results = self.multi_source_service.get_agents_for_deployment(
|
|
1100
1148
|
system_templates_dir=system_templates_dir,
|
|
1101
1149
|
project_agents_dir=project_agents_dir,
|
|
1102
1150
|
user_agents_dir=user_agents_dir,
|
|
1103
1151
|
working_directory=self.working_directory,
|
|
1104
1152
|
excluded_agents=excluded_agents,
|
|
1105
|
-
config=config
|
|
1153
|
+
config=config,
|
|
1154
|
+
cleanup_outdated=True # Enable cleanup by default
|
|
1106
1155
|
)
|
|
1107
1156
|
|
|
1108
1157
|
# Compare with deployed versions if agents directory exists
|
|
@@ -1143,7 +1192,7 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
|
|
|
1143
1192
|
# Convert to list of Path objects
|
|
1144
1193
|
template_files = list(agents_to_deploy.values())
|
|
1145
1194
|
|
|
1146
|
-
return template_files, agent_sources
|
|
1195
|
+
return template_files, agent_sources, cleanup_results
|
|
1147
1196
|
|
|
1148
1197
|
# ================================================================================
|
|
1149
1198
|
# Interface Adapter Methods
|
|
@@ -148,16 +148,24 @@ class AgentTemplateBuilder:
|
|
|
148
148
|
else:
|
|
149
149
|
claude_model = "inherit"
|
|
150
150
|
|
|
151
|
-
# Determine color
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
# Determine color - prefer template's color, fallback to type-based defaults
|
|
152
|
+
template_metadata = template_data.get("metadata", {})
|
|
153
|
+
template_color = template_metadata.get("color")
|
|
154
|
+
|
|
155
|
+
if template_color:
|
|
156
|
+
# Use the color specified in the template
|
|
157
|
+
color = template_color
|
|
158
|
+
else:
|
|
159
|
+
# Fallback to default color map based on agent type
|
|
160
|
+
color_map = {
|
|
161
|
+
"engineer": "blue",
|
|
162
|
+
"qa": "green",
|
|
163
|
+
"security": "red",
|
|
164
|
+
"research": "purple",
|
|
165
|
+
"documentation": "cyan", # Changed default to match template preference
|
|
166
|
+
"ops": "gray",
|
|
167
|
+
}
|
|
168
|
+
color = color_map.get(agent_type, "blue")
|
|
161
169
|
|
|
162
170
|
# Check if we should include tools field (only if significantly restricting)
|
|
163
171
|
# Claude Code approach: omit tools field unless specifically restricting
|
|
@@ -15,32 +15,25 @@ class AgentsDirectoryResolver:
|
|
|
15
15
|
def __init__(
|
|
16
16
|
self,
|
|
17
17
|
working_directory: Path,
|
|
18
|
-
is_system_deployment: bool,
|
|
19
|
-
is_project_specific: bool,
|
|
20
18
|
):
|
|
21
19
|
"""
|
|
22
20
|
Initialize the resolver.
|
|
23
21
|
|
|
24
22
|
Args:
|
|
25
23
|
working_directory: Current working directory
|
|
26
|
-
is_system_deployment: Whether this is a system agent deployment
|
|
27
|
-
is_project_specific: Whether this is a project-specific deployment
|
|
28
24
|
"""
|
|
29
25
|
self.working_directory = working_directory
|
|
30
|
-
self.is_system_deployment = is_system_deployment
|
|
31
|
-
self.is_project_specific = is_project_specific
|
|
32
26
|
|
|
33
27
|
def determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
|
|
34
28
|
"""
|
|
35
29
|
Determine the correct agents directory based on input.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Project-specific agents from <project>/.claude-mpm/agents/ → Deploy to <project>/.claude/agents/
|
|
30
|
+
|
|
31
|
+
MODIFIED: Always deploy to project .claude/agents directory
|
|
32
|
+
regardless of agent source (system, user, or project).
|
|
33
|
+
|
|
34
|
+
This ensures all agents are deployed at the project level while
|
|
35
|
+
maintaining discovery from both user (~/.claude-mpm) and project
|
|
36
|
+
(.claude-mpm) directories.
|
|
44
37
|
|
|
45
38
|
Args:
|
|
46
39
|
target_dir: Optional target directory
|
|
@@ -49,17 +42,9 @@ class AgentsDirectoryResolver:
|
|
|
49
42
|
Path to agents directory
|
|
50
43
|
"""
|
|
51
44
|
if not target_dir:
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
# System agents go to user's home ~/.claude/agents/
|
|
56
|
-
return Path.home() / ".claude" / "agents"
|
|
57
|
-
elif self.is_project_specific:
|
|
58
|
-
# Project agents stay in project directory
|
|
59
|
-
return self.working_directory / ".claude" / "agents"
|
|
60
|
-
else:
|
|
61
|
-
# Default: User custom agents go to home ~/.claude/agents/
|
|
62
|
-
return Path.home() / ".claude" / "agents"
|
|
45
|
+
# Always deploy to project directory
|
|
46
|
+
# This is the key change - all agents go to project .claude/agents
|
|
47
|
+
return self.working_directory / ".claude" / "agents"
|
|
63
48
|
|
|
64
49
|
# If target_dir provided, use it directly (caller decides structure)
|
|
65
50
|
target_dir = Path(target_dir)
|
|
@@ -13,6 +13,7 @@ Key Features:
|
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import logging
|
|
16
|
+
import os
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Dict, List, Optional, Tuple
|
|
18
19
|
|
|
@@ -190,8 +191,9 @@ class MultiSourceAgentDeploymentService:
|
|
|
190
191
|
user_agents_dir: Optional[Path] = None,
|
|
191
192
|
working_directory: Optional[Path] = None,
|
|
192
193
|
excluded_agents: Optional[List[str]] = None,
|
|
193
|
-
config: Optional[Config] = None
|
|
194
|
-
|
|
194
|
+
config: Optional[Config] = None,
|
|
195
|
+
cleanup_outdated: bool = True
|
|
196
|
+
) -> Tuple[Dict[str, Path], Dict[str, str], Dict[str, Any]]:
|
|
195
197
|
"""Get the highest version agents from all sources for deployment.
|
|
196
198
|
|
|
197
199
|
Args:
|
|
@@ -201,11 +203,13 @@ class MultiSourceAgentDeploymentService:
|
|
|
201
203
|
working_directory: Current working directory for finding project agents
|
|
202
204
|
excluded_agents: List of agent names to exclude from deployment
|
|
203
205
|
config: Configuration object for additional filtering
|
|
206
|
+
cleanup_outdated: Whether to cleanup outdated user agents (default: True)
|
|
204
207
|
|
|
205
208
|
Returns:
|
|
206
209
|
Tuple of:
|
|
207
210
|
- Dictionary mapping agent names to template file paths
|
|
208
211
|
- Dictionary mapping agent names to their source
|
|
212
|
+
- Dictionary with cleanup results (removed, preserved, errors)
|
|
209
213
|
"""
|
|
210
214
|
# Discover all available agents
|
|
211
215
|
agents_by_name = self.discover_agents_from_all_sources(
|
|
@@ -218,6 +222,27 @@ class MultiSourceAgentDeploymentService:
|
|
|
218
222
|
# Select highest version for each agent
|
|
219
223
|
selected_agents = self.select_highest_version_agents(agents_by_name)
|
|
220
224
|
|
|
225
|
+
# Clean up outdated user agents if enabled
|
|
226
|
+
cleanup_results = {"removed": [], "preserved": [], "errors": []}
|
|
227
|
+
if cleanup_outdated:
|
|
228
|
+
# Check if cleanup is enabled in config or environment
|
|
229
|
+
cleanup_enabled = True
|
|
230
|
+
|
|
231
|
+
# Check environment variable first (for CI/CD and testing)
|
|
232
|
+
env_cleanup = os.environ.get("CLAUDE_MPM_CLEANUP_USER_AGENTS", "").lower()
|
|
233
|
+
if env_cleanup in ["false", "0", "no", "disabled"]:
|
|
234
|
+
cleanup_enabled = False
|
|
235
|
+
self.logger.debug("User agent cleanup disabled via environment variable")
|
|
236
|
+
|
|
237
|
+
# Check config if environment doesn't disable it
|
|
238
|
+
if cleanup_enabled and config:
|
|
239
|
+
cleanup_enabled = config.get("agent_deployment.cleanup_outdated_user_agents", True)
|
|
240
|
+
|
|
241
|
+
if cleanup_enabled:
|
|
242
|
+
cleanup_results = self.cleanup_outdated_user_agents(
|
|
243
|
+
agents_by_name, selected_agents
|
|
244
|
+
)
|
|
245
|
+
|
|
221
246
|
# Apply exclusion filters
|
|
222
247
|
if excluded_agents:
|
|
223
248
|
for agent_name in excluded_agents:
|
|
@@ -256,7 +281,168 @@ class MultiSourceAgentDeploymentService:
|
|
|
256
281
|
f"user: {sum(1 for s in agent_sources.values() if s == 'user')})"
|
|
257
282
|
)
|
|
258
283
|
|
|
259
|
-
return agents_to_deploy, agent_sources
|
|
284
|
+
return agents_to_deploy, agent_sources, cleanup_results
|
|
285
|
+
|
|
286
|
+
def cleanup_outdated_user_agents(
|
|
287
|
+
self,
|
|
288
|
+
agents_by_name: Dict[str, List[Dict[str, Any]]],
|
|
289
|
+
selected_agents: Dict[str, Dict[str, Any]]
|
|
290
|
+
) -> Dict[str, Any]:
|
|
291
|
+
"""Remove outdated user agents when project or system agents have higher versions.
|
|
292
|
+
|
|
293
|
+
WHY: When project agents are updated to newer versions, outdated user agent
|
|
294
|
+
copies should be removed to prevent confusion and ensure the latest version
|
|
295
|
+
is always used. User agents with same or higher versions are preserved to
|
|
296
|
+
respect user customizations.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
agents_by_name: Dictionary mapping agent names to list of agent info from different sources
|
|
300
|
+
selected_agents: Dictionary mapping agent names to the selected highest version agent
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Dictionary with cleanup results:
|
|
304
|
+
- removed: List of removed agent info
|
|
305
|
+
- preserved: List of preserved agent info with reasons
|
|
306
|
+
- errors: List of errors during cleanup
|
|
307
|
+
"""
|
|
308
|
+
cleanup_results = {
|
|
309
|
+
"removed": [],
|
|
310
|
+
"preserved": [],
|
|
311
|
+
"errors": []
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Get user agents directory
|
|
315
|
+
user_agents_dir = Path.home() / ".claude-mpm" / "agents"
|
|
316
|
+
|
|
317
|
+
# Safety check - only operate on user agents directory
|
|
318
|
+
if not user_agents_dir.exists():
|
|
319
|
+
self.logger.debug("User agents directory does not exist, no cleanup needed")
|
|
320
|
+
return cleanup_results
|
|
321
|
+
|
|
322
|
+
for agent_name, agent_versions in agents_by_name.items():
|
|
323
|
+
# Skip if only one version exists
|
|
324
|
+
if len(agent_versions) < 2:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
selected = selected_agents.get(agent_name)
|
|
328
|
+
if not selected:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
# Process each version of this agent
|
|
332
|
+
for agent_info in agent_versions:
|
|
333
|
+
# Only consider user agents for cleanup
|
|
334
|
+
if agent_info["source"] != "user":
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# Safety check - ensure path is within user agents directory
|
|
338
|
+
user_agent_path = Path(agent_info["path"])
|
|
339
|
+
try:
|
|
340
|
+
# Resolve paths to compare them safely
|
|
341
|
+
resolved_user_path = user_agent_path.resolve()
|
|
342
|
+
resolved_user_agents_dir = user_agents_dir.resolve()
|
|
343
|
+
|
|
344
|
+
# Verify the agent is actually in the user agents directory
|
|
345
|
+
if not str(resolved_user_path).startswith(str(resolved_user_agents_dir)):
|
|
346
|
+
self.logger.warning(
|
|
347
|
+
f"Skipping cleanup for {agent_name}: path {user_agent_path} "
|
|
348
|
+
f"is not within user agents directory"
|
|
349
|
+
)
|
|
350
|
+
cleanup_results["errors"].append({
|
|
351
|
+
"agent": agent_name,
|
|
352
|
+
"error": "Path outside user agents directory"
|
|
353
|
+
})
|
|
354
|
+
continue
|
|
355
|
+
except Exception as e:
|
|
356
|
+
self.logger.error(f"Error resolving paths for {agent_name}: {e}")
|
|
357
|
+
cleanup_results["errors"].append({
|
|
358
|
+
"agent": agent_name,
|
|
359
|
+
"error": f"Path resolution error: {e}"
|
|
360
|
+
})
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
# Compare versions
|
|
364
|
+
user_version = self.version_manager.parse_version(
|
|
365
|
+
agent_info.get("version", "0.0.0")
|
|
366
|
+
)
|
|
367
|
+
selected_version = self.version_manager.parse_version(
|
|
368
|
+
selected.get("version", "0.0.0")
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
version_comparison = self.version_manager.compare_versions(
|
|
372
|
+
user_version, selected_version
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Determine action based on version comparison and selected source
|
|
376
|
+
if version_comparison < 0 and selected["source"] in ["project", "system"]:
|
|
377
|
+
# User agent has lower version than selected project/system agent - remove it
|
|
378
|
+
if user_agent_path.exists():
|
|
379
|
+
try:
|
|
380
|
+
# Log before removal for audit trail
|
|
381
|
+
self.logger.info(
|
|
382
|
+
f"Removing outdated user agent: {agent_name} "
|
|
383
|
+
f"v{self.version_manager.format_version_display(user_version)} "
|
|
384
|
+
f"(superseded by {selected['source']} "
|
|
385
|
+
f"v{self.version_manager.format_version_display(selected_version)})"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Remove the file
|
|
389
|
+
user_agent_path.unlink()
|
|
390
|
+
|
|
391
|
+
cleanup_results["removed"].append({
|
|
392
|
+
"name": agent_name,
|
|
393
|
+
"version": self.version_manager.format_version_display(user_version),
|
|
394
|
+
"path": str(user_agent_path),
|
|
395
|
+
"reason": f"Superseded by {selected['source']} v{self.version_manager.format_version_display(selected_version)}"
|
|
396
|
+
})
|
|
397
|
+
except PermissionError as e:
|
|
398
|
+
error_msg = f"Permission denied removing {agent_name}: {e}"
|
|
399
|
+
self.logger.error(error_msg)
|
|
400
|
+
cleanup_results["errors"].append({
|
|
401
|
+
"agent": agent_name,
|
|
402
|
+
"error": error_msg
|
|
403
|
+
})
|
|
404
|
+
except Exception as e:
|
|
405
|
+
error_msg = f"Error removing {agent_name}: {e}"
|
|
406
|
+
self.logger.error(error_msg)
|
|
407
|
+
cleanup_results["errors"].append({
|
|
408
|
+
"agent": agent_name,
|
|
409
|
+
"error": error_msg
|
|
410
|
+
})
|
|
411
|
+
else:
|
|
412
|
+
# Preserve the user agent
|
|
413
|
+
if version_comparison >= 0:
|
|
414
|
+
reason = "User version same or higher than selected version"
|
|
415
|
+
elif selected["source"] == "user":
|
|
416
|
+
reason = "User agent is the selected version"
|
|
417
|
+
else:
|
|
418
|
+
reason = "User customization preserved"
|
|
419
|
+
|
|
420
|
+
cleanup_results["preserved"].append({
|
|
421
|
+
"name": agent_name,
|
|
422
|
+
"version": self.version_manager.format_version_display(user_version),
|
|
423
|
+
"reason": reason
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
self.logger.debug(
|
|
427
|
+
f"Preserving user agent {agent_name} "
|
|
428
|
+
f"v{self.version_manager.format_version_display(user_version)}: {reason}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Log cleanup summary
|
|
432
|
+
if cleanup_results["removed"]:
|
|
433
|
+
self.logger.info(
|
|
434
|
+
f"Cleanup complete: removed {len(cleanup_results['removed'])} outdated user agents"
|
|
435
|
+
)
|
|
436
|
+
if cleanup_results["preserved"]:
|
|
437
|
+
self.logger.debug(
|
|
438
|
+
f"Preserved {len(cleanup_results['preserved'])} user agents"
|
|
439
|
+
)
|
|
440
|
+
if cleanup_results["errors"]:
|
|
441
|
+
self.logger.warning(
|
|
442
|
+
f"Encountered {len(cleanup_results['errors'])} errors during cleanup"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return cleanup_results
|
|
260
446
|
|
|
261
447
|
def _apply_config_filters(
|
|
262
448
|
self,
|
|
@@ -37,8 +37,9 @@ class TargetDirectorySetupStep(BaseDeploymentStep):
|
|
|
37
37
|
if context.target_dir:
|
|
38
38
|
context.actual_target_dir = context.target_dir
|
|
39
39
|
else:
|
|
40
|
-
# Default to
|
|
41
|
-
|
|
40
|
+
# MODIFIED: Default to project .claude/agents directory
|
|
41
|
+
# All agents now deploy to the project level
|
|
42
|
+
context.actual_target_dir = Path.cwd() / ".claude" / "agents"
|
|
42
43
|
|
|
43
44
|
# Create target directory if it doesn't exist
|
|
44
45
|
context.actual_target_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -63,15 +63,22 @@ class SystemAgentDeploymentStrategy(BaseDeploymentStrategy):
|
|
|
63
63
|
def determine_target_directory(self, context: DeploymentContext) -> Path:
|
|
64
64
|
"""Determine target directory for system agents.
|
|
65
65
|
|
|
66
|
-
System agents are
|
|
66
|
+
MODIFIED: System agents are now deployed to project .claude/agents/
|
|
67
|
+
to maintain consistency with the new deployment behavior.
|
|
68
|
+
All agents (system, user, project) deploy to the project level.
|
|
67
69
|
|
|
68
70
|
Args:
|
|
69
71
|
context: Deployment context
|
|
70
72
|
|
|
71
73
|
Returns:
|
|
72
|
-
Path to
|
|
74
|
+
Path to <project>/.claude/agents/
|
|
73
75
|
"""
|
|
74
|
-
|
|
76
|
+
# Always deploy to project directory
|
|
77
|
+
if context.working_directory:
|
|
78
|
+
return context.working_directory / ".claude" / "agents"
|
|
79
|
+
else:
|
|
80
|
+
# Fallback to current working directory if not specified
|
|
81
|
+
return Path.cwd() / ".claude" / "agents"
|
|
75
82
|
|
|
76
83
|
def get_templates_directory(self, context: DeploymentContext) -> Path:
|
|
77
84
|
"""Get templates directory for system agents.
|
|
@@ -57,26 +57,22 @@ class UserAgentDeploymentStrategy(BaseDeploymentStrategy):
|
|
|
57
57
|
def determine_target_directory(self, context: DeploymentContext) -> Path:
|
|
58
58
|
"""Determine target directory for user agents.
|
|
59
59
|
|
|
60
|
-
User agents
|
|
60
|
+
MODIFIED: User agents are now deployed to project .claude/agents/
|
|
61
|
+
to maintain consistency with the new deployment behavior.
|
|
62
|
+
All agents (system, user, project) deploy to the project level.
|
|
61
63
|
|
|
62
64
|
Args:
|
|
63
65
|
context: Deployment context
|
|
64
66
|
|
|
65
67
|
Returns:
|
|
66
|
-
Path to
|
|
68
|
+
Path to <project>/.claude/agents/
|
|
67
69
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if "CLAUDE_MPM_USER_PWD" in os.environ:
|
|
75
|
-
user_pwd = Path(os.environ["CLAUDE_MPM_USER_PWD"])
|
|
76
|
-
return user_pwd / ".claude" / "agents"
|
|
77
|
-
|
|
78
|
-
# Default to user's home directory
|
|
79
|
-
return Path.home() / ".claude-mpm" / "agents"
|
|
70
|
+
# Always deploy to project directory
|
|
71
|
+
if context.working_directory:
|
|
72
|
+
return context.working_directory / ".claude" / "agents"
|
|
73
|
+
else:
|
|
74
|
+
# Fallback to current working directory if not specified
|
|
75
|
+
return Path.cwd() / ".claude" / "agents"
|
|
80
76
|
|
|
81
77
|
def get_templates_directory(self, context: DeploymentContext) -> Path:
|
|
82
78
|
"""Get templates directory for user agents.
|
|
@@ -22,29 +22,24 @@ class SystemInstructionsDeployer:
|
|
|
22
22
|
target_dir: Path,
|
|
23
23
|
force_rebuild: bool,
|
|
24
24
|
results: Dict[str, Any],
|
|
25
|
-
is_project_specific: bool,
|
|
26
25
|
) -> None:
|
|
27
26
|
"""
|
|
28
27
|
Deploy system instructions and framework files for PM framework.
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Always deploys to project .claude directory regardless of agent source
|
|
30
|
+
(system, user, or project). This ensures consistent project-level
|
|
31
|
+
deployment while maintaining discovery from both user (~/.claude-mpm)
|
|
32
|
+
and project (.claude-mpm) directories.
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
|
-
target_dir: Target directory for deployment
|
|
35
|
+
target_dir: Target directory for deployment (not used - always uses project .claude)
|
|
36
36
|
force_rebuild: Force rebuild even if exists
|
|
37
37
|
results: Results dictionary to update
|
|
38
|
-
is_project_specific: Whether this is a project-specific deployment
|
|
39
38
|
"""
|
|
40
39
|
try:
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
claude_dir = self.working_directory / ".claude"
|
|
45
|
-
else:
|
|
46
|
-
# System and user files go to home ~/.claude directory
|
|
47
|
-
claude_dir = Path.home() / ".claude"
|
|
40
|
+
# Always use project's .claude directory
|
|
41
|
+
# This is the key change - all system instructions go to project .claude
|
|
42
|
+
claude_dir = self.working_directory / ".claude"
|
|
48
43
|
|
|
49
44
|
# Ensure .claude directory exists
|
|
50
45
|
claude_dir.mkdir(parents=True, exist_ok=True)
|