claude-mpm 4.2.51__py3-none-any.whl → 4.3.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.
Files changed (26) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +77 -447
  3. claude_mpm/agents/OUTPUT_STYLE.md +0 -39
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +145 -0
  5. claude_mpm/agents/WORKFLOW.md +74 -368
  6. claude_mpm/agents/templates/prompt-engineer.json +294 -0
  7. claude_mpm/agents/templates/vercel_ops_agent.json +153 -32
  8. claude_mpm/cli/commands/uninstall.py +0 -1
  9. claude_mpm/core/framework_loader.py +72 -24
  10. claude_mpm/core/log_manager.py +52 -0
  11. claude_mpm/core/logging_utils.py +30 -12
  12. claude_mpm/services/agents/deployment/agent_template_builder.py +260 -18
  13. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +35 -16
  14. claude_mpm/services/agents/local_template_manager.py +0 -1
  15. claude_mpm/services/monitor/daemon_manager.py +1 -3
  16. claude_mpm/services/monitor/event_emitter.py +5 -1
  17. claude_mpm/services/monitor/handlers/hooks.py +0 -2
  18. claude_mpm/tools/code_tree_analyzer.py +1 -3
  19. claude_mpm/utils/log_cleanup.py +612 -0
  20. {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/METADATA +41 -28
  21. {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/RECORD +26 -23
  22. /claude_mpm/agents/{INSTRUCTIONS.md → INSTRUCTIONS_OLD_DEPRECATED.md} +0 -0
  23. {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/WHEEL +0 -0
  24. {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/entry_points.txt +0 -0
  25. {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/licenses/LICENSE +0 -0
  26. {claude_mpm-4.2.51.dist-info → claude_mpm-4.3.4.dist-info}/top_level.txt +0 -0
@@ -639,7 +639,14 @@ class FrameworkLoader:
639
639
  self._load_packaged_framework_content(content)
640
640
  else:
641
641
  # Load from filesystem for development mode
642
- # Load framework's INSTRUCTIONS.md
642
+ # Try new consolidated PM_INSTRUCTIONS.md first, fall back to INSTRUCTIONS.md
643
+ pm_instructions_path = (
644
+ self.framework_path
645
+ / "src"
646
+ / "claude_mpm"
647
+ / "agents"
648
+ / "PM_INSTRUCTIONS.md"
649
+ )
643
650
  framework_instructions_path = (
644
651
  self.framework_path
645
652
  / "src"
@@ -647,12 +654,25 @@ class FrameworkLoader:
647
654
  / "agents"
648
655
  / "INSTRUCTIONS.md"
649
656
  )
650
- if framework_instructions_path.exists():
657
+
658
+ # Try loading new consolidated file first
659
+ if pm_instructions_path.exists():
651
660
  loaded_content = self._try_load_file(
652
- framework_instructions_path, "framework INSTRUCTIONS.md"
661
+ pm_instructions_path, "consolidated PM_INSTRUCTIONS.md"
653
662
  )
654
663
  if loaded_content:
655
664
  content["framework_instructions"] = loaded_content
665
+ self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md")
666
+ # Fall back to legacy file for backward compatibility
667
+ elif framework_instructions_path.exists():
668
+ loaded_content = self._try_load_file(
669
+ framework_instructions_path, "framework INSTRUCTIONS.md (legacy)"
670
+ )
671
+ if loaded_content:
672
+ content["framework_instructions"] = loaded_content
673
+ self.logger.warning(
674
+ "Using legacy INSTRUCTIONS.md - consider migrating to PM_INSTRUCTIONS.md"
675
+ )
656
676
  content["loaded"] = True
657
677
  # Add framework version to content
658
678
  if self.framework_version:
@@ -717,20 +737,33 @@ class FrameworkLoader:
717
737
  return
718
738
 
719
739
  try:
720
- # Load INSTRUCTIONS.md
721
- instructions_content = self._load_packaged_file("INSTRUCTIONS.md")
722
- if instructions_content:
723
- content["framework_instructions"] = instructions_content
740
+ # Try new consolidated PM_INSTRUCTIONS.md first
741
+ pm_instructions_content = self._load_packaged_file("PM_INSTRUCTIONS.md")
742
+ if pm_instructions_content:
743
+ content["framework_instructions"] = pm_instructions_content
724
744
  content["loaded"] = True
745
+ self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md from package")
725
746
  # Extract and store version/timestamp metadata
726
747
  self._extract_metadata_from_content(
727
- instructions_content, "INSTRUCTIONS.md"
748
+ pm_instructions_content, "PM_INSTRUCTIONS.md"
728
749
  )
729
- if self.framework_version:
730
- content["instructions_version"] = self.framework_version
731
- content["version"] = self.framework_version
732
- if self.framework_last_modified:
733
- content["instructions_last_modified"] = self.framework_last_modified
750
+ else:
751
+ # Fall back to legacy INSTRUCTIONS.md
752
+ instructions_content = self._load_packaged_file("INSTRUCTIONS.md")
753
+ if instructions_content:
754
+ content["framework_instructions"] = instructions_content
755
+ content["loaded"] = True
756
+ self.logger.warning("Using legacy INSTRUCTIONS.md from package")
757
+ # Extract and store version/timestamp metadata
758
+ self._extract_metadata_from_content(
759
+ instructions_content, "INSTRUCTIONS.md"
760
+ )
761
+
762
+ if self.framework_version:
763
+ content["instructions_version"] = self.framework_version
764
+ content["version"] = self.framework_version
765
+ if self.framework_last_modified:
766
+ content["instructions_last_modified"] = self.framework_last_modified
734
767
 
735
768
  # Load BASE_PM.md
736
769
  base_pm_content = self._load_packaged_file("BASE_PM.md")
@@ -757,22 +790,37 @@ class FrameworkLoader:
757
790
  ) -> None:
758
791
  """Load framework content using importlib.resources fallback."""
759
792
  try:
760
- # Load INSTRUCTIONS.md
761
- instructions_content = self._load_packaged_file_fallback(
762
- "INSTRUCTIONS.md", resources
793
+ # Try new consolidated PM_INSTRUCTIONS.md first
794
+ pm_instructions_content = self._load_packaged_file_fallback(
795
+ "PM_INSTRUCTIONS.md", resources
763
796
  )
764
- if instructions_content:
765
- content["framework_instructions"] = instructions_content
797
+ if pm_instructions_content:
798
+ content["framework_instructions"] = pm_instructions_content
766
799
  content["loaded"] = True
800
+ self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md via fallback")
767
801
  # Extract and store version/timestamp metadata
768
802
  self._extract_metadata_from_content(
769
- instructions_content, "INSTRUCTIONS.md"
803
+ pm_instructions_content, "PM_INSTRUCTIONS.md"
770
804
  )
771
- if self.framework_version:
772
- content["instructions_version"] = self.framework_version
773
- content["version"] = self.framework_version
774
- if self.framework_last_modified:
775
- content["instructions_last_modified"] = self.framework_last_modified
805
+ else:
806
+ # Fall back to legacy INSTRUCTIONS.md
807
+ instructions_content = self._load_packaged_file_fallback(
808
+ "INSTRUCTIONS.md", resources
809
+ )
810
+ if instructions_content:
811
+ content["framework_instructions"] = instructions_content
812
+ content["loaded"] = True
813
+ self.logger.warning("Using legacy INSTRUCTIONS.md via fallback")
814
+ # Extract and store version/timestamp metadata
815
+ self._extract_metadata_from_content(
816
+ instructions_content, "INSTRUCTIONS.md"
817
+ )
818
+
819
+ if self.framework_version:
820
+ content["instructions_version"] = self.framework_version
821
+ content["version"] = self.framework_version
822
+ if self.framework_last_modified:
823
+ content["instructions_last_modified"] = self.framework_last_modified
776
824
 
777
825
  # Load BASE_PM.md
778
826
  base_pm_content = self._load_packaged_file_fallback("BASE_PM.md", resources)
@@ -29,6 +29,12 @@ from ..core.constants import SystemLimits
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
32
+ # Import cleanup utility for automatic cleanup
33
+ try:
34
+ from ..utils.log_cleanup import run_cleanup_on_startup
35
+ except ImportError:
36
+ run_cleanup_on_startup = None
37
+
32
38
 
33
39
  class LogManager:
34
40
  """
@@ -76,6 +82,9 @@ class LogManager:
76
82
  # Start background threads
77
83
  self._start_background_threads()
78
84
 
85
+ # Run automatic cleanup on startup if enabled
86
+ self._run_startup_cleanup()
87
+
79
88
  def _setup_logging_config(self):
80
89
  """Load and setup logging configuration from config."""
81
90
  logging_config = self.config.get("logging", {})
@@ -107,6 +116,49 @@ class LogManager:
107
116
  if not self.base_log_dir.is_absolute():
108
117
  self.base_log_dir = Path.cwd() / self.base_log_dir
109
118
 
119
+ def _run_startup_cleanup(self):
120
+ """Run automatic log cleanup on startup if enabled."""
121
+ if run_cleanup_on_startup is None:
122
+ return # Cleanup utility not available
123
+
124
+ try:
125
+ # Get cleanup configuration
126
+ cleanup_config = self.config.get("log_cleanup", {})
127
+
128
+ # Check if automatic cleanup is enabled (default: True)
129
+ if not cleanup_config.get("auto_cleanup_enabled", True):
130
+ logger.debug("Automatic log cleanup is disabled")
131
+ return
132
+
133
+ # Convert hours to days for cleanup utility
134
+ cleanup_params = {
135
+ "auto_cleanup_enabled": True,
136
+ "session_retention_days": self.retention_hours.get("sessions", 168)
137
+ // 24,
138
+ "archive_retention_days": cleanup_config.get(
139
+ "archive_retention_days", 30
140
+ ),
141
+ "log_retention_days": cleanup_config.get("log_retention_days", 14),
142
+ }
143
+
144
+ # Run cleanup in background thread to avoid blocking startup
145
+ def cleanup_task():
146
+ try:
147
+ result = run_cleanup_on_startup(self.base_log_dir, cleanup_params)
148
+ if result:
149
+ logger.debug(
150
+ f"Startup cleanup completed: "
151
+ f"Removed {result.get('total_removed', 0)} items"
152
+ )
153
+ except Exception as e:
154
+ logger.debug(f"Startup cleanup failed: {e}")
155
+
156
+ cleanup_thread = Thread(target=cleanup_task, daemon=True)
157
+ cleanup_thread.start()
158
+
159
+ except Exception as e:
160
+ logger.debug(f"Could not run startup cleanup: {e}")
161
+
110
162
  def _start_background_threads(self):
111
163
  """Start background threads for async operations."""
112
164
  with self._lock:
@@ -42,8 +42,10 @@ class LoggingConfig:
42
42
  ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
43
43
 
44
44
  # File settings
45
- MAX_BYTES = 10 * 1024 * 1024 # 10MB
45
+ MAX_BYTES = 5 * 1024 * 1024 # 5MB - lowered for better rotation testing
46
46
  BACKUP_COUNT = 5
47
+ ROTATION_INTERVAL = "midnight" # Daily rotation at midnight
48
+ ROTATION_BACKUP_COUNT = 7 # Keep 7 days of daily logs
47
49
 
48
50
  # Component-specific log names
49
51
  COMPONENT_NAMES = {
@@ -129,30 +131,46 @@ class LoggerFactory:
129
131
  log_format: Optional[str] = None,
130
132
  date_format: Optional[str] = None,
131
133
  ) -> None:
132
- """Set up file logging handler."""
134
+ """Set up file logging handlers with both size and time-based rotation."""
133
135
  if not cls._log_dir:
134
136
  return
135
137
 
136
138
  # Ensure log directory exists
137
139
  cls._log_dir.mkdir(parents=True, exist_ok=True)
138
140
 
139
- # Create rotating file handler
141
+ formatter = logging.Formatter(
142
+ log_format or LoggingConfig.DETAILED_FORMAT,
143
+ date_format or LoggingConfig.DATE_FORMAT,
144
+ )
145
+
146
+ # 1. Size-based rotating file handler (for current active log)
140
147
  log_file = cls._log_dir / "claude_mpm.log"
141
- file_handler = logging.handlers.RotatingFileHandler(
148
+ size_handler = logging.handlers.RotatingFileHandler(
142
149
  log_file,
143
150
  maxBytes=LoggingConfig.MAX_BYTES,
144
151
  backupCount=LoggingConfig.BACKUP_COUNT,
145
152
  )
146
- file_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
147
-
148
- file_formatter = logging.Formatter(
149
- log_format or LoggingConfig.DETAILED_FORMAT,
150
- date_format or LoggingConfig.DATE_FORMAT,
153
+ size_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
154
+ size_handler.setFormatter(formatter)
155
+ logging.getLogger().addHandler(size_handler)
156
+ cls._handlers["file"] = size_handler
157
+
158
+ # 2. Time-based rotating file handler (daily rotation)
159
+ daily_log_file = cls._log_dir / "claude_mpm_daily.log"
160
+ time_handler = logging.handlers.TimedRotatingFileHandler(
161
+ daily_log_file,
162
+ when=LoggingConfig.ROTATION_INTERVAL,
163
+ interval=1,
164
+ backupCount=LoggingConfig.ROTATION_BACKUP_COUNT,
151
165
  )
152
- file_handler.setFormatter(file_formatter)
166
+ time_handler.setLevel(LoggingConfig.LEVELS.get(cls._log_level, logging.INFO))
167
+ time_handler.setFormatter(formatter)
168
+
169
+ # Add suffix to rotated files (e.g., claude_mpm_daily.log.2024-09-18)
170
+ time_handler.suffix = "%Y-%m-%d"
153
171
 
154
- logging.getLogger().addHandler(file_handler)
155
- cls._handlers["file"] = file_handler
172
+ logging.getLogger().addHandler(time_handler)
173
+ cls._handlers["file_daily"] = time_handler
156
174
 
157
175
  @classmethod
158
176
  def get_logger(
@@ -30,6 +30,52 @@ class AgentTemplateBuilder:
30
30
  """Initialize the template builder."""
31
31
  self.logger = get_logger(__name__)
32
32
 
33
+ def normalize_tools_input(self, tools):
34
+ """Normalize various tool input formats to a consistent list.
35
+
36
+ Handles multiple input formats:
37
+ - None/empty: Returns default tools
38
+ - String: Splits by comma and strips whitespace
39
+ - List: Ensures all items are strings and strips whitespace
40
+ - Dict: Takes enabled tools (where value is True)
41
+
42
+ Args:
43
+ tools: Tools input in various formats (str, list, dict, or None)
44
+
45
+ Returns:
46
+ List of tool names, normalized and cleaned
47
+ """
48
+ default_tools = ["Read", "Write", "Edit", "Grep", "Glob", "Bash"]
49
+
50
+ # Handle None or empty
51
+ if not tools:
52
+ self.logger.debug("No tools provided, using defaults")
53
+ return default_tools
54
+
55
+ # Convert to list format
56
+ if isinstance(tools, str):
57
+ # Split by comma, strip whitespace
58
+ tool_list = [t.strip() for t in tools.split(",") if t.strip()]
59
+ self.logger.debug(f"Converted string tools '{tools}' to list: {tool_list}")
60
+ elif isinstance(tools, list):
61
+ # Ensure all items are strings and strip whitespace
62
+ tool_list = [str(t).strip() for t in tools if t and str(t).strip()]
63
+ self.logger.debug(f"Normalized list tools: {tool_list}")
64
+ elif isinstance(tools, dict):
65
+ # Handle dict format - take enabled tools
66
+ tool_list = [k for k, v in tools.items() if v]
67
+ self.logger.info(f"Converting dict tools format: {tools} -> {tool_list}")
68
+ else:
69
+ self.logger.warning(f"Unknown tools format: {type(tools)}, using defaults")
70
+ return default_tools
71
+
72
+ # Return processed list or defaults if empty
73
+ if not tool_list:
74
+ self.logger.debug("Tools list empty after processing, using defaults")
75
+ return default_tools
76
+
77
+ return tool_list
78
+
33
79
  def _load_base_agent_instructions(self, agent_type: str) -> str:
34
80
  """Load BASE instructions for a specific agent type.
35
81
 
@@ -138,13 +184,39 @@ class AgentTemplateBuilder:
138
184
  capabilities.get("tools") if isinstance(capabilities, dict) else None
139
185
  )
140
186
 
141
- tools = (
187
+ # Get raw tools from various possible locations
188
+ raw_tools = (
142
189
  template_data.get("tools")
143
190
  or capabilities_tools
144
191
  or template_data.get("configuration_fields", {}).get("tools")
145
- or ["Read", "Write", "Edit", "Grep", "Glob", "LS"] # Default fallback
146
192
  )
147
193
 
194
+ # Normalize tools to a consistent list format
195
+ tools = self.normalize_tools_input(raw_tools)
196
+
197
+ # Log if we see non-standard tool names (info level, not warning)
198
+ standard_tools = {
199
+ "Read",
200
+ "Write",
201
+ "Edit",
202
+ "MultiEdit", # File operations
203
+ "Grep",
204
+ "Glob",
205
+ "LS", # Search and navigation
206
+ "Bash",
207
+ "BashOutput",
208
+ "KillShell", # Command execution
209
+ "TodoWrite",
210
+ "ExitPlanMode", # Task management
211
+ "WebSearch",
212
+ "WebFetch", # Web operations
213
+ "NotebookRead",
214
+ "NotebookEdit", # Jupyter notebook support
215
+ }
216
+ non_standard = [t for t in tools if t not in standard_tools]
217
+ if non_standard:
218
+ self.logger.info(f"Using non-standard tools: {non_standard}")
219
+
148
220
  # Extract model from template with fallback
149
221
  capabilities_model = (
150
222
  capabilities.get("model") if isinstance(capabilities, dict) else None
@@ -157,15 +229,8 @@ class AgentTemplateBuilder:
157
229
  or "sonnet" # Default fallback
158
230
  )
159
231
 
160
- # Convert tools list to comma-separated string (no spaces!)
161
- tools_str = ",".join(tools) if isinstance(tools, list) else str(tools)
162
-
163
- # Validate tools format - CRITICAL: No spaces allowed!
164
- if ", " in tools_str:
165
- self.logger.error(f"Tools contain spaces: '{tools_str}'")
166
- raise ValueError(
167
- f"Tools must be comma-separated WITHOUT spaces: {tools_str}"
168
- )
232
+ # Convert tools list to comma-separated string (without spaces for compatibility)
233
+ tools_str = ",".join(tools)
169
234
 
170
235
  # Map model names to Claude Code format (as required)
171
236
  model_map = {
@@ -331,12 +396,20 @@ class AgentTemplateBuilder:
331
396
  base_instructions = self._load_base_agent_instructions(agent_type)
332
397
 
333
398
  # Get agent instructions from template data (primary) or base agent data (fallback)
334
- agent_specific_instructions = (
335
- template_data.get("instructions")
336
- or base_agent_data.get("content")
337
- or base_agent_data.get("instructions")
338
- or "# Agent Instructions\n\nThis agent provides specialized assistance."
339
- )
399
+ raw_instructions = template_data.get("instructions")
400
+
401
+ # Handle dictionary instructions format
402
+ if isinstance(raw_instructions, dict):
403
+ agent_specific_instructions = self._convert_instructions_dict_to_markdown(
404
+ raw_instructions
405
+ )
406
+ else:
407
+ agent_specific_instructions = (
408
+ raw_instructions
409
+ or base_agent_data.get("content")
410
+ or base_agent_data.get("instructions")
411
+ or "# Agent Instructions\n\nThis agent provides specialized assistance."
412
+ )
340
413
 
341
414
  # Combine BASE instructions with agent-specific instructions
342
415
  if base_instructions:
@@ -423,7 +496,8 @@ Only include memories that are:
423
496
  )
424
497
 
425
498
  # Get tools and model with fallbacks
426
- tools = merged_config.get("tools", ["Read", "Write", "Edit"])
499
+ raw_tools = merged_config.get("tools")
500
+ tools = self.normalize_tools_input(raw_tools)
427
501
  model = merged_config.get("model", "sonnet")
428
502
 
429
503
  # Format tools as YAML list
@@ -890,3 +964,171 @@ tools:
890
964
 
891
965
  # Return as quoted string
892
966
  return f'"{escaped}"'
967
+
968
+ def _convert_instructions_dict_to_markdown(self, instructions_dict: dict) -> str:
969
+ """Convert complex instructions dictionary to markdown format.
970
+
971
+ Args:
972
+ instructions_dict: Dictionary containing structured instructions
973
+
974
+ Returns:
975
+ Formatted markdown string representing the instructions
976
+ """
977
+ if not instructions_dict:
978
+ return "# Agent Instructions\n\nThis agent provides specialized assistance."
979
+
980
+ markdown_parts = []
981
+
982
+ # Add primary role
983
+ if "primary_role" in instructions_dict:
984
+ markdown_parts.extend(["# Role", "", instructions_dict["primary_role"], ""])
985
+
986
+ # Add core identity
987
+ if "core_identity" in instructions_dict:
988
+ markdown_parts.extend(
989
+ ["## Core Identity", "", instructions_dict["core_identity"], ""]
990
+ )
991
+
992
+ # Add responsibilities
993
+ if "responsibilities" in instructions_dict:
994
+ markdown_parts.extend(["## Responsibilities", ""])
995
+
996
+ responsibilities = instructions_dict["responsibilities"]
997
+ if isinstance(responsibilities, list):
998
+ for resp in responsibilities:
999
+ if isinstance(resp, dict):
1000
+ area = resp.get("area", "Unknown Area")
1001
+ tasks = resp.get("tasks", [])
1002
+
1003
+ markdown_parts.extend([f"### {area}", ""])
1004
+
1005
+ if isinstance(tasks, list):
1006
+ for task in tasks:
1007
+ markdown_parts.append(f"- {task}")
1008
+
1009
+ markdown_parts.append("")
1010
+ else:
1011
+ markdown_parts.append(f"- {resp}")
1012
+
1013
+ markdown_parts.append("")
1014
+
1015
+ # Add analytical framework
1016
+ if "analytical_framework" in instructions_dict:
1017
+ framework = instructions_dict["analytical_framework"]
1018
+ if isinstance(framework, dict):
1019
+ markdown_parts.extend(["## Analytical Framework", ""])
1020
+
1021
+ for framework_area, framework_data in framework.items():
1022
+ markdown_parts.extend(
1023
+ [f"### {framework_area.replace('_', ' ').title()}", ""]
1024
+ )
1025
+
1026
+ if isinstance(framework_data, dict):
1027
+ for category, items in framework_data.items():
1028
+ markdown_parts.extend(
1029
+ [f"#### {category.replace('_', ' ').title()}", ""]
1030
+ )
1031
+
1032
+ if isinstance(items, list):
1033
+ for item in items:
1034
+ markdown_parts.append(f"- {item}")
1035
+ elif isinstance(items, str):
1036
+ markdown_parts.append(items)
1037
+
1038
+ markdown_parts.append("")
1039
+ elif isinstance(framework_data, list):
1040
+ for item in framework_data:
1041
+ markdown_parts.append(f"- {item}")
1042
+ markdown_parts.append("")
1043
+
1044
+ # Add methodologies
1045
+ if "methodologies" in instructions_dict:
1046
+ methodologies = instructions_dict["methodologies"]
1047
+ if isinstance(methodologies, dict):
1048
+ markdown_parts.extend(["## Methodologies", ""])
1049
+
1050
+ for method_name, method_data in methodologies.items():
1051
+ markdown_parts.extend(
1052
+ [f"### {method_name.replace('_', ' ').title()}", ""]
1053
+ )
1054
+
1055
+ if isinstance(method_data, dict):
1056
+ for key, value in method_data.items():
1057
+ if isinstance(value, list):
1058
+ markdown_parts.extend(
1059
+ [f"#### {key.replace('_', ' ').title()}", ""]
1060
+ )
1061
+ for item in value:
1062
+ markdown_parts.append(f"- {item}")
1063
+ markdown_parts.append("")
1064
+ elif isinstance(value, str):
1065
+ markdown_parts.extend(
1066
+ [
1067
+ f"**{key.replace('_', ' ').title()}**: {value}",
1068
+ "",
1069
+ ]
1070
+ )
1071
+
1072
+ # Add quality standards
1073
+ if "quality_standards" in instructions_dict:
1074
+ standards = instructions_dict["quality_standards"]
1075
+ if isinstance(standards, dict):
1076
+ markdown_parts.extend(["## Quality Standards", ""])
1077
+
1078
+ for standard_area, standard_items in standards.items():
1079
+ markdown_parts.extend(
1080
+ [f"### {standard_area.replace('_', ' ').title()}", ""]
1081
+ )
1082
+
1083
+ if isinstance(standard_items, list):
1084
+ for item in standard_items:
1085
+ markdown_parts.append(f"- {item}")
1086
+ elif isinstance(standard_items, str):
1087
+ markdown_parts.append(standard_items)
1088
+
1089
+ markdown_parts.append("")
1090
+
1091
+ # Add communication style
1092
+ if "communication_style" in instructions_dict:
1093
+ comm_style = instructions_dict["communication_style"]
1094
+ if isinstance(comm_style, dict):
1095
+ markdown_parts.extend(["## Communication Style", ""])
1096
+
1097
+ for style_area, style_items in comm_style.items():
1098
+ markdown_parts.extend(
1099
+ [f"### {style_area.replace('_', ' ').title()}", ""]
1100
+ )
1101
+
1102
+ if isinstance(style_items, list):
1103
+ for item in style_items:
1104
+ markdown_parts.append(f"- {item}")
1105
+ elif isinstance(style_items, str):
1106
+ markdown_parts.append(style_items)
1107
+
1108
+ markdown_parts.append("")
1109
+
1110
+ # If no specific sections were found, convert as generic dict
1111
+ if not markdown_parts:
1112
+ markdown_parts = ["# Agent Instructions", ""]
1113
+ for key, value in instructions_dict.items():
1114
+ key_title = key.replace("_", " ").title()
1115
+ if isinstance(value, str):
1116
+ markdown_parts.extend([f"## {key_title}", "", value, ""])
1117
+ elif isinstance(value, list):
1118
+ markdown_parts.extend([f"## {key_title}", ""])
1119
+ for item in value:
1120
+ markdown_parts.append(f"- {item}")
1121
+ markdown_parts.append("")
1122
+ elif isinstance(value, dict):
1123
+ markdown_parts.extend([f"## {key_title}", ""])
1124
+ # Simple dict formatting
1125
+ for subkey, subvalue in value.items():
1126
+ if isinstance(subvalue, str):
1127
+ markdown_parts.extend(
1128
+ [
1129
+ f"**{subkey.replace('_', ' ').title()}**: {subvalue}",
1130
+ "",
1131
+ ]
1132
+ )
1133
+
1134
+ return "\n".join(markdown_parts).strip()
@@ -176,24 +176,43 @@ class MultiSourceAgentDeploymentService:
176
176
  # Log if a higher priority source was overridden by version
177
177
  for other_agent in agent_versions:
178
178
  if other_agent != highest_version_agent:
179
- self.version_manager.parse_version(
179
+ # Parse both versions for comparison
180
+ other_version = self.version_manager.parse_version(
180
181
  other_agent.get("version", "0.0.0")
181
182
  )
182
- if (
183
- other_agent["source"] == "project"
184
- and highest_version_agent["source"] == "system"
185
- ):
186
- self.logger.warning(
187
- f"Project agent '{agent_name}' v{other_agent['version']} "
188
- f"overridden by higher system version v{highest_version_agent['version']}"
189
- )
190
- elif other_agent["source"] == "user" and highest_version_agent[
191
- "source"
192
- ] in ["system", "project"]:
193
- self.logger.warning(
194
- f"User agent '{agent_name}' v{other_agent['version']} "
195
- f"overridden by higher {highest_version_agent['source']} version v{highest_version_agent['version']}"
196
- )
183
+ highest_version = self.version_manager.parse_version(
184
+ highest_version_agent.get("version", "0.0.0")
185
+ )
186
+
187
+ # Compare the versions
188
+ version_comparison = self.version_manager.compare_versions(
189
+ other_version, highest_version
190
+ )
191
+
192
+ # Only warn if the other version is actually lower
193
+ if version_comparison < 0:
194
+ if (
195
+ other_agent["source"] == "project"
196
+ and highest_version_agent["source"] == "system"
197
+ ):
198
+ self.logger.warning(
199
+ f"Project agent '{agent_name}' v{other_agent['version']} "
200
+ f"overridden by higher system version v{highest_version_agent['version']}"
201
+ )
202
+ elif other_agent["source"] == "user" and highest_version_agent[
203
+ "source"
204
+ ] in ["system", "project"]:
205
+ self.logger.warning(
206
+ f"User agent '{agent_name}' v{other_agent['version']} "
207
+ f"overridden by higher {highest_version_agent['source']} version v{highest_version_agent['version']}"
208
+ )
209
+ elif version_comparison == 0:
210
+ # Log info when versions are equal but different sources
211
+ if other_agent["source"] != highest_version_agent["source"]:
212
+ self.logger.info(
213
+ f"Using {highest_version_agent['source']} source for '{agent_name}' "
214
+ f"(same version v{highest_version_agent['version']} as {other_agent['source']} source)"
215
+ )
197
216
 
198
217
  return selected_agents
199
218
 
@@ -280,7 +280,6 @@ class LocalAgentTemplateManager:
280
280
  parent_agent=parent_agent,
281
281
  )
282
282
 
283
-
284
283
  def save_local_template(
285
284
  self, template: LocalAgentTemplate, tier: Optional[str] = None
286
285
  ) -> Path: