claude-mpm 4.4.0__py3-none-any.whl → 4.4.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 (50) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/cli/commands/configure.py +2 -29
  4. claude_mpm/cli/commands/mpm_init.py +3 -3
  5. claude_mpm/cli/parsers/configure_parser.py +4 -15
  6. claude_mpm/core/framework/__init__.py +38 -0
  7. claude_mpm/core/framework/formatters/__init__.py +11 -0
  8. claude_mpm/core/framework/formatters/capability_generator.py +356 -0
  9. claude_mpm/core/framework/formatters/content_formatter.py +283 -0
  10. claude_mpm/core/framework/formatters/context_generator.py +180 -0
  11. claude_mpm/core/framework/loaders/__init__.py +13 -0
  12. claude_mpm/core/framework/loaders/agent_loader.py +202 -0
  13. claude_mpm/core/framework/loaders/file_loader.py +213 -0
  14. claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
  15. claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
  16. claude_mpm/core/framework/processors/__init__.py +11 -0
  17. claude_mpm/core/framework/processors/memory_processor.py +222 -0
  18. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  19. claude_mpm/core/framework/processors/template_processor.py +238 -0
  20. claude_mpm/core/framework_loader.py +277 -1798
  21. claude_mpm/hooks/__init__.py +9 -1
  22. claude_mpm/hooks/kuzu_memory_hook.py +352 -0
  23. claude_mpm/services/core/path_resolver.py +1 -0
  24. claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
  25. claude_mpm/services/mcp_config_manager.py +67 -4
  26. claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
  27. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  28. claude_mpm/services/mcp_gateway/main.py +3 -13
  29. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  30. claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
  31. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
  32. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
  33. claude_mpm/services/shared/__init__.py +2 -1
  34. claude_mpm/services/shared/service_factory.py +8 -5
  35. claude_mpm/services/unified/config_strategies/__init__.py +190 -0
  36. claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
  37. claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
  38. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
  39. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
  40. claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
  41. claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
  42. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
  43. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +47 -27
  44. claude_mpm/cli/commands/configure_tui.py +0 -1927
  45. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  46. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  47. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
  48. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
  49. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
  50. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,222 @@
1
+ """Memory content processor for framework memory management."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional, Set
5
+
6
+ from claude_mpm.core.logging_utils import get_logger
7
+
8
+
9
+ class MemoryProcessor:
10
+ """Processes and manages memory content for agents."""
11
+
12
+ def __init__(self):
13
+ """Initialize the memory processor."""
14
+ self.logger = get_logger("memory_processor")
15
+
16
+ def load_pm_memories(self) -> Dict[str, str]:
17
+ """Load PM memories from various locations.
18
+
19
+ Returns:
20
+ Dictionary with actual_memories content
21
+ """
22
+ memories = {}
23
+
24
+ # Check for project-specific PM memories (highest priority)
25
+ project_memory_file = Path.cwd() / ".claude-mpm" / "memories" / "PM_memories.md"
26
+ if project_memory_file.exists():
27
+ try:
28
+ content = project_memory_file.read_text()
29
+ memories["actual_memories"] = content
30
+ memories["memory_source"] = "project"
31
+ self.logger.info(f"Loaded PM memories from project: {project_memory_file}")
32
+ return memories
33
+ except Exception as e:
34
+ self.logger.error(f"Failed to load project PM memories: {e}")
35
+
36
+ # Check for user-specific PM memories (fallback)
37
+ user_memory_file = Path.home() / ".claude-mpm" / "memories" / "PM_memories.md"
38
+ if user_memory_file.exists():
39
+ try:
40
+ content = user_memory_file.read_text()
41
+ memories["actual_memories"] = content
42
+ memories["memory_source"] = "user"
43
+ self.logger.info(f"Loaded PM memories from user: {user_memory_file}")
44
+ return memories
45
+ except Exception as e:
46
+ self.logger.error(f"Failed to load user PM memories: {e}")
47
+
48
+ return memories
49
+
50
+ def load_agent_memories(self, deployed_agents: Set[str]) -> Dict[str, str]:
51
+ """Load memories for deployed agents.
52
+
53
+ Args:
54
+ deployed_agents: Set of deployed agent names
55
+
56
+ Returns:
57
+ Dictionary mapping agent names to their memory content
58
+ """
59
+ agent_memories = {}
60
+
61
+ # Define memory file search locations
62
+ memory_locations = [
63
+ Path.cwd() / ".claude-mpm" / "memories", # Project memories
64
+ Path.home() / ".claude-mpm" / "memories", # User memories
65
+ ]
66
+
67
+ for agent_name in deployed_agents:
68
+ memory_filename = f"{agent_name}_memories.md"
69
+
70
+ # Search for memory file in each location (project takes precedence)
71
+ for memory_dir in memory_locations:
72
+ memory_file = memory_dir / memory_filename
73
+ if memory_file.exists():
74
+ try:
75
+ content = memory_file.read_text()
76
+ agent_memories[agent_name] = content
77
+ self.logger.debug(f"Loaded memories for {agent_name} from {memory_file}")
78
+ break # Use first found (project > user)
79
+ except Exception as e:
80
+ self.logger.error(f"Failed to load memories for {agent_name}: {e}")
81
+
82
+ return agent_memories
83
+
84
+ def aggregate_memories(
85
+ self,
86
+ pm_memories: Dict[str, str],
87
+ agent_memories: Dict[str, str],
88
+ ) -> Dict[str, Any]:
89
+ """Aggregate all memories into a single structure.
90
+
91
+ Args:
92
+ pm_memories: PM memory content
93
+ agent_memories: Agent-specific memories
94
+
95
+ Returns:
96
+ Aggregated memory structure
97
+ """
98
+ result = {}
99
+
100
+ # Add PM memories
101
+ if pm_memories.get("actual_memories"):
102
+ result["actual_memories"] = pm_memories["actual_memories"]
103
+ result["memory_source"] = pm_memories.get("memory_source", "unknown")
104
+
105
+ # Add agent memories
106
+ if agent_memories:
107
+ result["agent_memories"] = agent_memories
108
+
109
+ return result
110
+
111
+ def format_memory_section(self, memories: Dict[str, Any]) -> str:
112
+ """Format memories into a section for instructions.
113
+
114
+ Args:
115
+ memories: Memory content dictionary
116
+
117
+ Returns:
118
+ Formatted memory section
119
+ """
120
+ sections = []
121
+
122
+ # Format PM memories
123
+ if memories.get("actual_memories"):
124
+ sections.append("\n\n## Current PM Memories\n\n")
125
+ sections.append(
126
+ "**The following are your accumulated memories and knowledge from this project:**\n\n"
127
+ )
128
+ sections.append(memories["actual_memories"])
129
+ sections.append("\n")
130
+
131
+ # Format agent memories
132
+ if memories.get("agent_memories"):
133
+ agent_memories = memories["agent_memories"]
134
+ if agent_memories:
135
+ sections.append("\n\n## Agent Memories\n\n")
136
+ sections.append(
137
+ "**The following are accumulated memories from specialized agents:**\n\n"
138
+ )
139
+
140
+ for agent_name in sorted(agent_memories.keys()):
141
+ memory_content = agent_memories[agent_name]
142
+ if memory_content:
143
+ formatted_name = agent_name.replace("_", " ").title()
144
+ sections.append(f"### {formatted_name} Agent Memory\n\n")
145
+ sections.append(memory_content)
146
+ sections.append("\n\n")
147
+
148
+ return "".join(sections)
149
+
150
+ def deduplicate_memories(self, memories: Dict[str, str]) -> Dict[str, str]:
151
+ """Remove duplicate entries from memories.
152
+
153
+ Args:
154
+ memories: Raw memory content
155
+
156
+ Returns:
157
+ Deduplicated memories
158
+ """
159
+ deduplicated = {}
160
+
161
+ for key, content in memories.items():
162
+ if not content:
163
+ continue
164
+
165
+ # Split into lines and remove duplicates while preserving order
166
+ lines = content.split("\n")
167
+ seen = set()
168
+ unique_lines = []
169
+
170
+ for line in lines:
171
+ # Skip empty lines in deduplication
172
+ if not line.strip():
173
+ unique_lines.append(line)
174
+ continue
175
+
176
+ # Add non-duplicate lines
177
+ if line not in seen:
178
+ seen.add(line)
179
+ unique_lines.append(line)
180
+
181
+ deduplicated[key] = "\n".join(unique_lines)
182
+
183
+ return deduplicated
184
+
185
+ def migrate_legacy_memories(self) -> bool:
186
+ """Migrate memories from old .claude/ locations to new .claude-mpm/ locations.
187
+
188
+ Returns:
189
+ True if any migrations were performed
190
+ """
191
+ migrated = False
192
+
193
+ # Define migration paths
194
+ migrations = [
195
+ # Project memories
196
+ (
197
+ Path.cwd() / ".claude" / "memories" / "PM_memories.md",
198
+ Path.cwd() / ".claude-mpm" / "memories" / "PM_memories.md",
199
+ ),
200
+ # User memories
201
+ (
202
+ Path.home() / ".claude" / "memories" / "PM_memories.md",
203
+ Path.home() / ".claude-mpm" / "memories" / "PM_memories.md",
204
+ ),
205
+ ]
206
+
207
+ for old_path, new_path in migrations:
208
+ if old_path.exists() and not new_path.exists():
209
+ try:
210
+ # Create new directory if needed
211
+ new_path.parent.mkdir(parents=True, exist_ok=True)
212
+
213
+ # Copy content
214
+ content = old_path.read_text()
215
+ new_path.write_text(content)
216
+
217
+ self.logger.info(f"Migrated memories from {old_path} to {new_path}")
218
+ migrated = True
219
+ except Exception as e:
220
+ self.logger.error(f"Failed to migrate memories from {old_path}: {e}")
221
+
222
+ return migrated
@@ -0,0 +1,146 @@
1
+ """Metadata extraction and processing for framework files."""
2
+
3
+ import re
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional, Tuple
7
+
8
+ import yaml
9
+
10
+ from claude_mpm.core.logging_utils import get_logger
11
+
12
+
13
+ class MetadataProcessor:
14
+ """Processes and extracts metadata from framework files and agents."""
15
+
16
+ def __init__(self):
17
+ """Initialize the metadata processor."""
18
+ self.logger = get_logger("metadata_processor")
19
+
20
+ def extract_metadata_from_content(self, content: str) -> Dict[str, Optional[str]]:
21
+ """Extract metadata from content string.
22
+
23
+ Args:
24
+ content: Content to extract metadata from
25
+
26
+ Returns:
27
+ Dictionary with extracted metadata
28
+ """
29
+ metadata = {
30
+ "version": None,
31
+ "last_modified": None,
32
+ }
33
+
34
+ # Extract version
35
+ version_match = re.search(r"<!-- FRAMEWORK_VERSION: (\d+) -->", content)
36
+ if version_match:
37
+ metadata["version"] = version_match.group(1)
38
+ self.logger.debug(f"Extracted version: {metadata['version']}")
39
+
40
+ # Extract timestamp
41
+ timestamp_match = re.search(r"<!-- LAST_MODIFIED: ([^>]+) -->", content)
42
+ if timestamp_match:
43
+ metadata["last_modified"] = timestamp_match.group(1).strip()
44
+ self.logger.debug(f"Extracted last_modified: {metadata['last_modified']}")
45
+
46
+ return metadata
47
+
48
+ def parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
49
+ """Parse agent metadata from deployed agent file.
50
+
51
+ Args:
52
+ agent_file: Path to deployed agent file
53
+
54
+ Returns:
55
+ Dictionary with agent metadata or None
56
+ """
57
+ try:
58
+ with open(agent_file) as f:
59
+ content = f.read()
60
+
61
+ # Default values
62
+ agent_data = {
63
+ "id": agent_file.stem,
64
+ "display_name": agent_file.stem.replace("_", " ").replace("-", " ").title(),
65
+ "description": "Specialized agent",
66
+ "file_path": str(agent_file),
67
+ "file_mtime": agent_file.stat().st_mtime,
68
+ }
69
+
70
+ # Extract YAML frontmatter if present
71
+ if content.startswith("---"):
72
+ end_marker = content.find("---", 3)
73
+ if end_marker > 0:
74
+ frontmatter = content[3:end_marker]
75
+ metadata = yaml.safe_load(frontmatter)
76
+ if metadata:
77
+ # Use name as ID for Task tool
78
+ agent_data["id"] = metadata.get("name", agent_data["id"])
79
+ agent_data["display_name"] = (
80
+ metadata.get("name", agent_data["display_name"])
81
+ .replace("-", " ")
82
+ .title()
83
+ )
84
+
85
+ # Copy all metadata fields directly
86
+ for key, value in metadata.items():
87
+ if key not in ["name"]: # Skip already processed fields
88
+ agent_data[key] = value
89
+
90
+ # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
91
+ # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
92
+
93
+ return agent_data
94
+
95
+ except Exception as e:
96
+ self.logger.debug(f"Could not parse metadata from {agent_file}: {e}")
97
+ return None
98
+
99
+ def extract_cache_metadata(
100
+ self, data: Any, cache_key: str
101
+ ) -> Tuple[Any, float]:
102
+ """Extract cache metadata for storage.
103
+
104
+ Args:
105
+ data: Data to cache
106
+ cache_key: Cache key for identification
107
+
108
+ Returns:
109
+ Tuple of (data, timestamp) for cache storage
110
+ """
111
+ return data, time.time()
112
+
113
+ def validate_cache_metadata(
114
+ self,
115
+ cached_data: Tuple[Any, float],
116
+ file_path: Optional[Path] = None,
117
+ ttl: float = 60.0,
118
+ ) -> bool:
119
+ """Validate cache metadata for freshness.
120
+
121
+ Args:
122
+ cached_data: Tuple of (data, timestamp) from cache
123
+ file_path: Optional file path to check modification time
124
+ ttl: Time-to-live in seconds
125
+
126
+ Returns:
127
+ True if cache is valid, False otherwise
128
+ """
129
+ try:
130
+ data, cache_time = cached_data
131
+ current_time = time.time()
132
+
133
+ # Check TTL
134
+ if current_time - cache_time > ttl:
135
+ return False
136
+
137
+ # Check file modification time if provided
138
+ if file_path and file_path.exists():
139
+ file_mtime = file_path.stat().st_mtime
140
+ if file_mtime > cache_time:
141
+ return False
142
+
143
+ return True
144
+ except Exception as e:
145
+ self.logger.debug(f"Cache validation failed: {e}")
146
+ return False
@@ -0,0 +1,238 @@
1
+ """JSON template processor for agent configurations."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from claude_mpm.core.logging_utils import get_logger
8
+
9
+ # Import resource handling for packaged installations
10
+ try:
11
+ from importlib.resources import files
12
+ except ImportError:
13
+ try:
14
+ from importlib_resources import files
15
+ except ImportError:
16
+ files = None
17
+
18
+
19
+ class TemplateProcessor:
20
+ """Processes JSON template files for agent configurations."""
21
+
22
+ def __init__(self, framework_path: Optional[Path] = None):
23
+ """Initialize the template processor.
24
+
25
+ Args:
26
+ framework_path: Path to framework installation
27
+ """
28
+ self.logger = get_logger("template_processor")
29
+ self.framework_path = framework_path
30
+
31
+ def load_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
32
+ """Load JSON template for an agent.
33
+
34
+ Args:
35
+ agent_name: Name of the agent
36
+
37
+ Returns:
38
+ Template data or None if not found
39
+ """
40
+ try:
41
+ # Check if we have a framework path
42
+ if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
43
+ return self._load_packaged_template(agent_name)
44
+
45
+ # For development mode, load from filesystem
46
+ return self._load_filesystem_template(agent_name)
47
+
48
+ except Exception as e:
49
+ self.logger.debug(f"Could not load template for {agent_name}: {e}")
50
+ return None
51
+
52
+ def _load_packaged_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
53
+ """Load template from packaged installation.
54
+
55
+ Args:
56
+ agent_name: Name of the agent
57
+
58
+ Returns:
59
+ Template data or None if not found
60
+ """
61
+ if not files:
62
+ return None
63
+
64
+ try:
65
+ templates_package = files("claude_mpm.agents.templates")
66
+ template_file = templates_package / f"{agent_name}.json"
67
+
68
+ if template_file.is_file():
69
+ template_content = template_file.read_text()
70
+ return json.loads(template_content)
71
+ except Exception as e:
72
+ self.logger.debug(f"Could not load packaged template for {agent_name}: {e}")
73
+
74
+ return None
75
+
76
+ def _load_filesystem_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
77
+ """Load template from filesystem.
78
+
79
+ Args:
80
+ agent_name: Name of the agent
81
+
82
+ Returns:
83
+ Template data or None if not found
84
+ """
85
+ templates_dir = self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
86
+
87
+ # Try exact match first
88
+ template_file = templates_dir / f"{agent_name}.json"
89
+ if template_file.exists():
90
+ with open(template_file) as f:
91
+ return json.load(f)
92
+
93
+ # Try alternative naming variations
94
+ alternative_names = self._get_alternative_names(agent_name)
95
+ for alt_name in alternative_names:
96
+ alt_file = templates_dir / f"{alt_name}.json"
97
+ if alt_file.exists():
98
+ with open(alt_file) as f:
99
+ return json.load(f)
100
+
101
+ return None
102
+
103
+ def _get_alternative_names(self, agent_name: str) -> List[str]:
104
+ """Get alternative naming variations for an agent.
105
+
106
+ Args:
107
+ agent_name: Original agent name
108
+
109
+ Returns:
110
+ List of alternative names to try
111
+ """
112
+ # Remove duplicates by using a set
113
+ return list(
114
+ {
115
+ agent_name.replace("-", "_"), # api-qa -> api_qa
116
+ agent_name.replace("_", "-"), # api_qa -> api-qa
117
+ agent_name.replace("-", ""), # api-qa -> apiqa
118
+ agent_name.replace("_", ""), # api_qa -> apiqa
119
+ agent_name.replace("-agent", ""), # research-agent -> research
120
+ agent_name.replace("_agent", ""), # research_agent -> research
121
+ agent_name + "_agent", # research -> research_agent
122
+ agent_name + "-agent", # research -> research-agent
123
+ }
124
+ )
125
+
126
+ def extract_routing(self, template_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
127
+ """Extract routing information from template.
128
+
129
+ Args:
130
+ template_data: Template data
131
+
132
+ Returns:
133
+ Routing information or None
134
+ """
135
+ return template_data.get("routing")
136
+
137
+ def extract_memory_routing(self, template_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
138
+ """Extract memory routing information from template.
139
+
140
+ Args:
141
+ template_data: Template data
142
+
143
+ Returns:
144
+ Memory routing information or None
145
+ """
146
+ return template_data.get("memory_routing")
147
+
148
+ def extract_tools(self, template_data: Dict[str, Any]) -> str:
149
+ """Extract tools string from template data.
150
+
151
+ Args:
152
+ template_data: Template data
153
+
154
+ Returns:
155
+ Tools string for display
156
+ """
157
+ capabilities = template_data.get("capabilities", {})
158
+ tools = capabilities.get("tools", "*")
159
+
160
+ if tools == "*":
161
+ return "All Tools"
162
+ if isinstance(tools, list):
163
+ return ", ".join(tools) if tools else "Standard Tools"
164
+ if isinstance(tools, str):
165
+ return tools
166
+ return "Standard Tools"
167
+
168
+ def extract_metadata(self, template_data: Dict[str, Any]) -> Dict[str, Any]:
169
+ """Extract agent metadata from template.
170
+
171
+ Args:
172
+ template_data: Template data
173
+
174
+ Returns:
175
+ Dictionary with extracted metadata
176
+ """
177
+ metadata = template_data.get("metadata", {})
178
+ agent_id = template_data.get("agent_id", "unknown")
179
+
180
+ return {
181
+ "id": agent_id,
182
+ "display_name": metadata.get("name", agent_id.replace("_", " ").title()),
183
+ "description": metadata.get("description", f"Agent {agent_id}"),
184
+ "authority": metadata.get("authority"),
185
+ "primary_function": metadata.get("primary_function"),
186
+ "handoff_to": metadata.get("handoff_to"),
187
+ "model": template_data.get("model", {}).get("model"),
188
+ "tools": self.extract_tools(template_data),
189
+ "routing": self.extract_routing(template_data),
190
+ "memory_routing": self.extract_memory_routing(template_data),
191
+ "author": template_data.get("author", "unknown"),
192
+ "version": template_data.get("agent_version", "1.0.0"),
193
+ }
194
+
195
+ def process_local_templates(self) -> Dict[str, Dict[str, Any]]:
196
+ """Process all local JSON templates.
197
+
198
+ Returns:
199
+ Dictionary mapping agent IDs to processed metadata
200
+ """
201
+ local_agents = {}
202
+
203
+ # Check for local JSON templates in priority order
204
+ template_dirs = [
205
+ Path.cwd() / ".claude-mpm" / "agents", # Project local agents
206
+ Path.home() / ".claude-mpm" / "agents", # User local agents
207
+ ]
208
+
209
+ for priority, template_dir in enumerate(template_dirs):
210
+ if not template_dir.exists():
211
+ continue
212
+
213
+ for json_file in template_dir.glob("*.json"):
214
+ try:
215
+ with open(json_file) as f:
216
+ template_data = json.load(f)
217
+
218
+ agent_metadata = self.extract_metadata(template_data)
219
+ agent_id = agent_metadata["id"]
220
+
221
+ # Skip if already found at higher priority
222
+ if agent_id in local_agents:
223
+ continue
224
+
225
+ # Add local-specific fields
226
+ agent_metadata["is_local"] = True
227
+ agent_metadata["tier"] = "project" if priority == 0 else "user"
228
+ agent_metadata["source_file"] = str(json_file)
229
+
230
+ local_agents[agent_id] = agent_metadata
231
+ self.logger.debug(
232
+ f"Processed local template: {agent_id} from {template_dir}"
233
+ )
234
+
235
+ except Exception as e:
236
+ self.logger.warning(f"Failed to process template {json_file}: {e}")
237
+
238
+ return local_agents