claude-mpm 4.3.6__py3-none-any.whl → 4.3.12__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 (49) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +41 -8
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +85 -43
  4. claude_mpm/agents/templates/clerk-ops.json +223 -0
  5. claude_mpm/agents/templates/data_engineer.json +41 -5
  6. claude_mpm/agents/templates/php-engineer.json +185 -0
  7. claude_mpm/agents/templates/research.json +20 -8
  8. claude_mpm/agents/templates/web_qa.json +25 -10
  9. claude_mpm/cli/__init__.py +41 -2
  10. claude_mpm/cli/commands/agents.py +2 -2
  11. claude_mpm/cli/commands/analyze.py +4 -4
  12. claude_mpm/cli/commands/cleanup.py +7 -7
  13. claude_mpm/cli/commands/configure_tui.py +2 -2
  14. claude_mpm/cli/commands/debug.py +2 -2
  15. claude_mpm/cli/commands/info.py +3 -4
  16. claude_mpm/cli/commands/mcp.py +8 -6
  17. claude_mpm/cli/commands/mcp_command_router.py +11 -0
  18. claude_mpm/cli/commands/mcp_config.py +157 -0
  19. claude_mpm/cli/commands/mcp_external_commands.py +241 -0
  20. claude_mpm/cli/commands/mcp_install_commands.py +73 -32
  21. claude_mpm/cli/commands/mcp_setup_external.py +829 -0
  22. claude_mpm/cli/commands/run.py +73 -3
  23. claude_mpm/cli/commands/search.py +285 -0
  24. claude_mpm/cli/parsers/base_parser.py +13 -0
  25. claude_mpm/cli/parsers/mcp_parser.py +17 -0
  26. claude_mpm/cli/parsers/run_parser.py +5 -0
  27. claude_mpm/cli/parsers/search_parser.py +239 -0
  28. claude_mpm/cli/startup_logging.py +20 -7
  29. claude_mpm/constants.py +1 -0
  30. claude_mpm/core/unified_agent_registry.py +7 -0
  31. claude_mpm/hooks/instruction_reinforcement.py +295 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +28 -13
  33. claude_mpm/services/agents/deployment/agent_discovery_service.py +16 -6
  34. claude_mpm/services/agents/deployment/deployment_wrapper.py +59 -0
  35. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
  36. claude_mpm/services/cli/agent_cleanup_service.py +5 -0
  37. claude_mpm/services/mcp_config_manager.py +294 -0
  38. claude_mpm/services/mcp_gateway/config/configuration.py +17 -0
  39. claude_mpm/services/mcp_gateway/main.py +38 -0
  40. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +390 -0
  41. claude_mpm/utils/log_cleanup.py +17 -17
  42. claude_mpm/utils/subprocess_utils.py +6 -6
  43. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/METADATA +24 -1
  44. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/RECORD +48 -39
  45. claude_mpm/agents/templates/agent-manager.md +0 -619
  46. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/WHEEL +0 -0
  47. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/entry_points.txt +0 -0
  48. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/licenses/LICENSE +0 -0
  49. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/top_level.txt +0 -0
@@ -18,7 +18,7 @@ import logging
18
18
  import shutil
19
19
  import subprocess
20
20
  import sys
21
- from datetime import datetime
21
+ from datetime import datetime, timezone
22
22
  from pathlib import Path
23
23
  from typing import Any, Dict, Optional
24
24
 
@@ -339,14 +339,27 @@ class StartupStatusLogger:
339
339
 
340
340
  def _check_socketio_dependencies(self) -> Dict[str, Any]:
341
341
  """Check if Socket.IO dependencies are available."""
342
+ import importlib.util
343
+
342
344
  result = {"available": False, "error": None}
343
345
 
344
346
  try:
345
- import aiohttp
346
- import engineio
347
- import socketio
347
+ # Check for socketio dependencies without importing them
348
+ aiohttp_spec = importlib.util.find_spec("aiohttp")
349
+ engineio_spec = importlib.util.find_spec("engineio")
350
+ socketio_spec = importlib.util.find_spec("socketio")
348
351
 
349
- result["available"] = True
352
+ if aiohttp_spec and engineio_spec and socketio_spec:
353
+ result["available"] = True
354
+ else:
355
+ missing = []
356
+ if not aiohttp_spec:
357
+ missing.append("aiohttp")
358
+ if not engineio_spec:
359
+ missing.append("engineio")
360
+ if not socketio_spec:
361
+ missing.append("socketio")
362
+ result["error"] = f"Missing dependencies: {', '.join(missing)}"
350
363
  except ImportError as e:
351
364
  result["error"] = f"Missing dependencies: {e}"
352
365
  except Exception as e:
@@ -493,7 +506,7 @@ def setup_startup_logging(project_root: Optional[Path] = None) -> Path:
493
506
  log_dir.mkdir(parents=True, exist_ok=True)
494
507
 
495
508
  # Generate timestamp for log file
496
- timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
509
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H-%M-%S")
497
510
  log_file = log_dir / f"startup-{timestamp}.log"
498
511
 
499
512
  # Create file handler with detailed formatting
@@ -516,7 +529,7 @@ def setup_startup_logging(project_root: Optional[Path] = None) -> Path:
516
529
  # Log startup header
517
530
  logger = get_logger("startup")
518
531
  logger.info("=" * 60)
519
- logger.info(f"Claude MPM Startup - {datetime.now().isoformat()}")
532
+ logger.info(f"Claude MPM Startup - {datetime.now(timezone.utc).isoformat()}")
520
533
  logger.info(f"Log file: {log_file}")
521
534
  logger.info("=" * 60)
522
535
 
claude_mpm/constants.py CHANGED
@@ -140,6 +140,7 @@ class MCPCommands(str, Enum):
140
140
  TEST = "test"
141
141
  CONFIG = "config"
142
142
  SERVER = "server"
143
+ EXTERNAL = "external"
143
144
 
144
145
 
145
146
  class TicketCommands(str, Enum):
@@ -460,6 +460,13 @@ class UnifiedAgentRegistry:
460
460
  if agent_format == AgentFormat.JSON:
461
461
  data = json.loads(content)
462
462
 
463
+ # Ensure data is a dictionary, not a list
464
+ if not isinstance(data, dict):
465
+ logger.warning(
466
+ f"Invalid JSON structure in {file_path}: expected object, got {type(data).__name__}"
467
+ )
468
+ return "", []
469
+
463
470
  # Handle local agent JSON templates with metadata structure
464
471
  if "metadata" in data:
465
472
  metadata = data["metadata"]
@@ -0,0 +1,295 @@
1
+ """
2
+ Instruction Reinforcement Hook for PM Delegation Compliance
3
+
4
+ This hook monitors PM behavior for delegation violations and provides
5
+ escalating warnings when the PM attempts to implement instead of delegate.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+ from typing import Dict, List, Optional, Tuple
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ViolationType(Enum):
18
+ """Types of PM delegation violations"""
19
+
20
+ EDIT_ATTEMPT = "Edit/Write/MultiEdit"
21
+ BASH_IMPLEMENTATION = "Bash for implementation"
22
+ FILE_CREATION = "Direct file creation"
23
+ TEST_EXECUTION = "Test execution"
24
+ DEPLOYMENT = "Deployment operation"
25
+ CODE_WRITING = "Code writing"
26
+
27
+
28
+ @dataclass
29
+ class Violation:
30
+ """Record of a delegation violation"""
31
+
32
+ violation_type: ViolationType
33
+ context: str
34
+ timestamp: float
35
+ severity_level: int
36
+
37
+
38
+ class InstructionReinforcementHook:
39
+ """
40
+ Monitors PM messages for delegation violations and provides corrective feedback.
41
+
42
+ Circuit breaker pattern implementation to prevent PM from implementing
43
+ instead of delegating work to appropriate agents.
44
+ """
45
+
46
+ def __init__(self):
47
+ self.violation_count = 0
48
+ self.violations: List[Violation] = []
49
+
50
+ # Patterns that indicate PM is attempting forbidden actions
51
+ self.forbidden_patterns = [
52
+ # Direct implementation language - EXPANDED
53
+ (
54
+ r"I'll\s+(fix|create|write|implement|code|build|update|generate|modify|set\s+up|configure|optimize|rewrite|run|test|deploy|push|analyze|review|setup)",
55
+ ViolationType.CODE_WRITING,
56
+ ),
57
+ (
58
+ r"I'm\s+(fix|create|write|implement|code|build|update|generate|modify)",
59
+ ViolationType.CODE_WRITING,
60
+ ),
61
+ (
62
+ r"Let\s+me\s+(edit|write|modify|create|update|fix|run|execute|commit|refactor|configure|set\s+up|review)",
63
+ ViolationType.EDIT_ATTEMPT,
64
+ ),
65
+ (
66
+ r"I\s+will\s+(implement|code|build|create|write|update|fix)",
67
+ ViolationType.CODE_WRITING,
68
+ ),
69
+ (
70
+ r"I'm\s+(going\s+to|about\s+to)\s+(fix|create|write|implement|update|modify)",
71
+ ViolationType.CODE_WRITING,
72
+ ),
73
+ # Common honeypot phrases
74
+ (
75
+ r"Here's\s+(the|my|an?)\s+(implementation|code|SQL|query|solution|analysis|fix)",
76
+ ViolationType.CODE_WRITING,
77
+ ),
78
+ (
79
+ r"The\s+(query|code|implementation|solution)\s+(would\s+be|is)",
80
+ ViolationType.CODE_WRITING,
81
+ ),
82
+ (r"I\s+(found|identified)\s+(these\s+)?issues", ViolationType.CODE_WRITING),
83
+ # Deployment and setup patterns
84
+ (
85
+ r"Setting\s+up\s+(the\s+)?(authentication|containers|environment|docker)",
86
+ ViolationType.DEPLOYMENT,
87
+ ),
88
+ (r"Deploying\s+(to|the)", ViolationType.DEPLOYMENT),
89
+ (r"I'll\s+(deploy|push|host|launch)", ViolationType.DEPLOYMENT),
90
+ # Tool usage patterns - EXPANDED
91
+ (r"Using\s+(Edit|Write|MultiEdit)\s+tool", ViolationType.EDIT_ATTEMPT),
92
+ (r"<invoke\s+name=\"(Edit|Write|MultiEdit)\"", ViolationType.EDIT_ATTEMPT),
93
+ (
94
+ r"Running\s+(bash\s+command|git\s+commit|npm|yarn|python|node|go|tests|pytest)",
95
+ ViolationType.BASH_IMPLEMENTATION,
96
+ ),
97
+ (r"Executing\s+(tests|test\s+suite|pytest)", ViolationType.TEST_EXECUTION),
98
+ # Testing patterns - EXPANDED
99
+ (
100
+ r"(Testing|I'll\s+test|Let\s+me\s+test)\s+(the\s+)?(payment|API|endpoint)",
101
+ ViolationType.TEST_EXECUTION,
102
+ ),
103
+ (
104
+ r"I'll\s+(run|execute|verify)\s+(the\s+)?(tests|test\s+suite|endpoint)",
105
+ ViolationType.TEST_EXECUTION,
106
+ ),
107
+ (r"pytest|npm\s+test|yarn\s+test|go\s+test", ViolationType.TEST_EXECUTION),
108
+ # File operation patterns - EXPANDED
109
+ (
110
+ r"Creating\s+(new\s+|a\s+|the\s+)?(file|YAML|README|workflow)",
111
+ ViolationType.FILE_CREATION,
112
+ ),
113
+ (r"Writing\s+to\s+file", ViolationType.FILE_CREATION),
114
+ (
115
+ r"Updating\s+(the\s+)?(code|component|queries)",
116
+ ViolationType.CODE_WRITING,
117
+ ),
118
+ (
119
+ r"I'll\s+(update|modify)\s+(the\s+)?(component|code|React)",
120
+ ViolationType.CODE_WRITING,
121
+ ),
122
+ ]
123
+
124
+ # Patterns for correct delegation behavior - EXPANDED
125
+ self.delegation_patterns = [
126
+ r"delegat(e|ing)\s+(to|this)",
127
+ r"Task\s+tool",
128
+ r"(asking|request|have|use)\s+\w+\s+agent",
129
+ r"requesting\s+\w+\s+to",
130
+ r"will\s+(have|ask|request)\s+\w+\s+agent",
131
+ r"I'll\s+(have|ask|request|delegate)",
132
+ r"the\s+\w+\s+agent\s+(will|can|should)",
133
+ ]
134
+
135
+ def detect_violation_intent(
136
+ self, message: str
137
+ ) -> Optional[Tuple[ViolationType, str]]:
138
+ """
139
+ Check message for patterns indicating PM violation intent.
140
+
141
+ Args:
142
+ message: The PM's message to analyze
143
+
144
+ Returns:
145
+ Tuple of (ViolationType, matched_text) if violation detected, None otherwise
146
+ """
147
+ message_lower = message.lower()
148
+
149
+ # Check for forbidden patterns
150
+ for pattern, violation_type in self.forbidden_patterns:
151
+ match = re.search(pattern, message_lower, re.IGNORECASE)
152
+ if match:
153
+ # Check if this is actually a delegation (false positive check)
154
+ is_delegation = any(
155
+ re.search(
156
+ del_pattern,
157
+ message_lower[max(0, match.start() - 50) : match.end() + 50],
158
+ )
159
+ for del_pattern in self.delegation_patterns
160
+ )
161
+
162
+ if not is_delegation:
163
+ return (violation_type, match.group(0))
164
+
165
+ return None
166
+
167
+ def escalate_warning(self) -> str:
168
+ """
169
+ Generate escalating warning message based on violation count.
170
+
171
+ Returns:
172
+ Warning message with appropriate severity
173
+ """
174
+ if self.violation_count == 1:
175
+ return (
176
+ "⚠️ DELEGATION REMINDER: PM must delegate ALL implementation work.\n"
177
+ "Use the Task tool to delegate to the appropriate agent."
178
+ )
179
+ if self.violation_count == 2:
180
+ return (
181
+ "🚨 DELEGATION WARNING: Critical PM violation detected!\n"
182
+ "You MUST delegate implementation work. Do NOT use Edit/Write/Bash for implementation.\n"
183
+ "Next violation will result in session failure."
184
+ )
185
+ if self.violation_count == 3:
186
+ return (
187
+ "❌ CRITICAL DELEGATION FAILURE: Multiple PM violations detected.\n"
188
+ "PM has repeatedly attempted to implement instead of delegate.\n"
189
+ "Session integrity compromised. All work must be delegated to agents."
190
+ )
191
+ return (
192
+ f"❌❌❌ SEVERE VIOLATION (Count: {self.violation_count}): PM continues to violate delegation rules.\n"
193
+ "MANDATORY: Use Task tool to delegate ALL implementation to appropriate agents.\n"
194
+ "Current session may need to be terminated and restarted."
195
+ )
196
+
197
+ def check_message(self, message: str) -> Optional[Dict[str, any]]:
198
+ """
199
+ Check a PM message for violations and return feedback if needed.
200
+
201
+ Args:
202
+ message: The PM's message to check
203
+
204
+ Returns:
205
+ Dictionary with violation details and correction, or None if compliant
206
+ """
207
+ violation_result = self.detect_violation_intent(message)
208
+
209
+ if violation_result:
210
+ violation_type, context = violation_result
211
+ self.violation_count += 1
212
+
213
+ # Record the violation
214
+ import time
215
+
216
+ violation = Violation(
217
+ violation_type=violation_type,
218
+ context=context,
219
+ timestamp=time.time(),
220
+ severity_level=min(self.violation_count, 4),
221
+ )
222
+ self.violations.append(violation)
223
+
224
+ # Generate corrective feedback
225
+ warning = self.escalate_warning()
226
+
227
+ # Determine which agent should handle this
228
+ agent_mapping = {
229
+ ViolationType.EDIT_ATTEMPT: "Engineer",
230
+ ViolationType.CODE_WRITING: "Engineer",
231
+ ViolationType.BASH_IMPLEMENTATION: "Engineer or Ops",
232
+ ViolationType.FILE_CREATION: "Engineer or Documentation",
233
+ ViolationType.TEST_EXECUTION: "QA",
234
+ ViolationType.DEPLOYMENT: "Ops",
235
+ }
236
+
237
+ suggested_agent = agent_mapping.get(violation_type, "appropriate agent")
238
+
239
+ # Clean up context for task suggestion
240
+ clean_context = (
241
+ context.replace("I will ", "")
242
+ .replace("I'll ", "")
243
+ .replace("Let me ", "")
244
+ )
245
+
246
+ return {
247
+ "violation_detected": True,
248
+ "violation_count": self.violation_count,
249
+ "violation_type": violation_type.value,
250
+ "context": context,
251
+ "warning": warning,
252
+ "correction": f"MUST delegate to {suggested_agent} using Task tool",
253
+ "suggested_task": f"Task: Please {clean_context}",
254
+ "severity": min(self.violation_count, 4),
255
+ }
256
+
257
+ return None
258
+
259
+ def get_violation_summary(self) -> Dict[str, any]:
260
+ """
261
+ Get a summary of all violations in the session.
262
+
263
+ Returns:
264
+ Dictionary with violation statistics and details
265
+ """
266
+ if not self.violations:
267
+ return {
268
+ "total_violations": 0,
269
+ "status": "COMPLIANT",
270
+ "message": "No PM delegation violations detected",
271
+ }
272
+
273
+ violation_types = {}
274
+ for v in self.violations:
275
+ vtype = v.violation_type.value
276
+ violation_types[vtype] = violation_types.get(vtype, 0) + 1
277
+
278
+ status = "WARNING" if self.violation_count < 3 else "CRITICAL"
279
+
280
+ return {
281
+ "total_violations": self.violation_count,
282
+ "status": status,
283
+ "violation_types": violation_types,
284
+ "most_recent": self.violations[-1].context if self.violations else None,
285
+ "recommendation": (
286
+ "Review PM delegation training"
287
+ if self.violation_count > 2
288
+ else "Continue monitoring"
289
+ ),
290
+ }
291
+
292
+ def reset(self):
293
+ """Reset violation tracking for a new session"""
294
+ self.violation_count = 0
295
+ self.violations = []
@@ -776,16 +776,6 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
776
776
  agent_sources=agent_sources,
777
777
  )
778
778
 
779
- # Log version upgrades and source changes
780
- if comparison_results.get("version_upgrades"):
781
- self.logger.info(
782
- f"Version upgrades available for {len(comparison_results['version_upgrades'])} agents"
783
- )
784
- if comparison_results.get("source_changes"):
785
- self.logger.info(
786
- f"Source changes for {len(comparison_results['source_changes'])} agents"
787
- )
788
-
789
779
  # Filter agents based on comparison results (unless force_rebuild is set)
790
780
  if not force_rebuild:
791
781
  # Only deploy agents that need updates or are new
@@ -804,11 +794,36 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
804
794
  for name, path in agents_to_deploy.items()
805
795
  if name in agents_needing_update
806
796
  }
797
+
798
+ # Only log upgrade messages if we're actually going to deploy them
799
+ if filtered_agents and comparison_results.get("version_upgrades"):
800
+ # Filter upgrades to only those actually being deployed
801
+ deployed_upgrades = [
802
+ upgrade for upgrade in comparison_results["version_upgrades"]
803
+ if upgrade["name"] in filtered_agents
804
+ ]
805
+
806
+ if deployed_upgrades:
807
+ self.logger.info(
808
+ f"Deploying {len(deployed_upgrades)} agent upgrade(s):"
809
+ )
810
+ for upgrade in deployed_upgrades:
811
+ self.logger.info(
812
+ f" Upgrading: {upgrade['name']} "
813
+ f"{upgrade['deployed_version']} -> {upgrade['new_version']} "
814
+ f"(from {upgrade['source']})"
815
+ )
816
+
807
817
  agents_to_deploy = filtered_agents
808
818
 
809
- self.logger.info(
810
- f"Filtered to {len(agents_to_deploy)} agents needing deployment"
811
- )
819
+ if agents_to_deploy:
820
+ self.logger.info(
821
+ f"Deploying {len(agents_to_deploy)} agents that need updates"
822
+ )
823
+ else:
824
+ self.logger.debug(
825
+ f"All {len(comparison_results.get('up_to_date', []))} agents are up to date"
826
+ )
812
827
 
813
828
  # Convert to list of Path objects
814
829
  template_files = list(agents_to_deploy.values())
@@ -216,25 +216,35 @@ class AgentDiscoveryService:
216
216
  metadata = template_data.get("metadata", {})
217
217
  capabilities = template_data.get("capabilities", {})
218
218
 
219
+ # Handle capabilities as either dict or list
220
+ if isinstance(capabilities, list):
221
+ # If capabilities is a list (like in php-engineer.json), treat it as capabilities list
222
+ tools_list = template_data.get("tools", []) # Look for tools at root level
223
+ model_value = template_data.get("model", "sonnet")
224
+ else:
225
+ # If capabilities is a dict, extract tools and model from it
226
+ tools_list = capabilities.get("tools", [])
227
+ model_value = capabilities.get("model", "sonnet")
228
+
219
229
  agent_info = {
220
230
  "name": metadata.get("name", template_file.stem),
221
- "description": metadata.get("description", "No description available"),
231
+ "description": metadata.get("description", template_data.get("description", "No description available")),
222
232
  "type": template_data.get(
223
- "agent_type", metadata.get("category", "agent")
233
+ "agent_type", metadata.get("category", template_data.get("category", "agent"))
224
234
  ), # Extract agent type
225
235
  "version": template_data.get(
226
236
  "agent_version",
227
237
  template_data.get("version", metadata.get("version", "1.0.0")),
228
238
  ),
229
- "tools": capabilities.get("tools", []),
239
+ "tools": tools_list,
230
240
  "specializations": metadata.get(
231
- "tags", []
232
- ), # Use tags as specializations
241
+ "tags", template_data.get("tags", [])
242
+ ), # Use tags as specializations, fallback to root-level tags
233
243
  "file": template_file.name,
234
244
  "path": str(template_file),
235
245
  "file_path": str(template_file), # Keep for backward compatibility
236
246
  "size": template_file.stat().st_size,
237
- "model": capabilities.get("model", "sonnet"),
247
+ "model": model_value,
238
248
  "author": metadata.get("author", "unknown"),
239
249
  }
240
250
 
@@ -69,3 +69,62 @@ class DeploymentServiceWrapper:
69
69
  "errors": [],
70
70
  "target_dir": str(project_dir),
71
71
  }
72
+
73
+ def get_agent_details(self, agent_name: str) -> Dict[str, Any]:
74
+ """Get detailed information for a specific agent.
75
+
76
+ Args:
77
+ agent_name: Name of the agent
78
+
79
+ Returns:
80
+ Agent details dictionary or empty dict if not found
81
+ """
82
+ try:
83
+ # Try to get from list of available agents
84
+ available_agents = self.service.list_available_agents()
85
+ for agent in available_agents:
86
+ if agent.get("name") == agent_name:
87
+ # Get template path for the agent
88
+ templates_dir = self.service.templates_dir
89
+ agent_path = templates_dir / f"{agent_name}.md"
90
+
91
+ # Read agent content if file exists
92
+ if agent_path.exists():
93
+ with open(agent_path) as f:
94
+ content = f.read()
95
+
96
+ # Parse metadata from content
97
+ import yaml
98
+
99
+ metadata = {}
100
+ if content.startswith("---"):
101
+ # Extract frontmatter
102
+ parts = content.split("---", 2)
103
+ if len(parts) >= 2:
104
+ try:
105
+ metadata = yaml.safe_load(parts[1])
106
+ except yaml.YAMLError:
107
+ pass
108
+
109
+ return {
110
+ "name": agent_name,
111
+ "path": str(agent_path),
112
+ "type": agent.get("type", "agent"),
113
+ "version": metadata.get("version", "1.0.0"),
114
+ "description": metadata.get("description", ""),
115
+ "specializations": metadata.get("specializations", []),
116
+ "metadata": metadata,
117
+ "content": content,
118
+ "exists": True,
119
+ }
120
+
121
+ # Agent not found in available agents
122
+ return {
123
+ "name": agent_name,
124
+ "exists": False,
125
+ "error": f"Agent '{agent_name}' not found",
126
+ }
127
+
128
+ except Exception as e:
129
+ # Return error information
130
+ return {"name": agent_name, "exists": False, "error": str(e)}
@@ -742,18 +742,20 @@ class MultiSourceAgentDeploymentService:
742
742
 
743
743
  self.logger.info(f"Version comparison complete: {', '.join(summary_parts)}")
744
744
 
745
+ # Don't log upgrades here - let the caller decide when to log
746
+ # This prevents repeated upgrade messages on every startup
745
747
  if comparison_results["version_upgrades"]:
746
748
  for upgrade in comparison_results["version_upgrades"]:
747
- self.logger.info(
748
- f" Upgrade: {upgrade['name']} "
749
+ self.logger.debug(
750
+ f" Upgrade available: {upgrade['name']} "
749
751
  f"{upgrade['deployed_version']} -> {upgrade['new_version']} "
750
752
  f"(from {upgrade['source']})"
751
753
  )
752
754
 
753
755
  if comparison_results["source_changes"]:
754
756
  for change in comparison_results["source_changes"]:
755
- self.logger.info(
756
- f" Source change: {change['name']} "
757
+ self.logger.debug(
758
+ f" Source change available: {change['name']} "
757
759
  f"from {change['from_source']} to {change['to_source']}"
758
760
  )
759
761
 
@@ -138,6 +138,11 @@ class AgentCleanupService(IAgentCleanupService):
138
138
  if not isinstance(result, dict):
139
139
  result = {"success": bool(result)}
140
140
 
141
+ # Add success flag based on whether there were errors
142
+ if "success" not in result:
143
+ # Consider it successful if no errors occurred
144
+ result["success"] = not bool(result.get("errors"))
145
+
141
146
  # Add cleaned_count for backward compatibility
142
147
  if "cleaned_count" not in result:
143
148
  removed_count = len(result.get("removed", []))