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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/mpm_init.py +3 -3
- claude_mpm/cli/parsers/configure_parser.py +4 -15
- claude_mpm/core/framework/__init__.py +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +356 -0
- claude_mpm/core/framework/formatters/content_formatter.py +283 -0
- claude_mpm/core/framework/formatters/context_generator.py +180 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +202 -0
- claude_mpm/core/framework/loaders/file_loader.py +213 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +222 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +238 -0
- claude_mpm/core/framework_loader.py +277 -1798
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/kuzu_memory_hook.py +352 -0
- claude_mpm/services/core/path_resolver.py +1 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/unified/config_strategies/__init__.py +190 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +47 -27
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,28 @@
|
|
1
|
-
"""Framework loader for Claude MPM."""
|
1
|
+
"""Framework loader for Claude MPM - Refactored modular version."""
|
2
2
|
|
3
|
-
import
|
4
|
-
import locale
|
5
|
-
import logging
|
3
|
+
import asyncio
|
6
4
|
import os
|
7
|
-
import platform
|
8
|
-
import time
|
9
|
-
from datetime import datetime, timezone
|
10
5
|
from pathlib import Path
|
11
|
-
from typing import Any, Dict, Optional
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
6
|
+
from typing import Any, Dict, List, Optional, Set
|
7
|
+
|
8
|
+
from claude_mpm.core.logging_utils import get_logger
|
9
|
+
from claude_mpm.utils.imports import safe_import
|
10
|
+
|
11
|
+
# Import framework components
|
12
|
+
from claude_mpm.core.framework import (
|
13
|
+
AgentLoader,
|
14
|
+
CapabilityGenerator,
|
15
|
+
ContentFormatter,
|
16
|
+
ContextGenerator,
|
17
|
+
FileLoader,
|
18
|
+
InstructionLoader,
|
19
|
+
MemoryProcessor,
|
20
|
+
MetadataProcessor,
|
21
|
+
PackagedLoader,
|
22
|
+
TemplateProcessor,
|
23
|
+
)
|
26
24
|
|
27
|
-
# Import with fallback support
|
28
|
-
get_logger = safe_import("claude_mpm.core.logger", "core.logger", ["get_logger"])
|
25
|
+
# Import with fallback support
|
29
26
|
AgentRegistryAdapter = safe_import(
|
30
27
|
"claude_mpm.core.agent_registry", "core.agent_registry", ["AgentRegistryAdapter"]
|
31
28
|
)
|
@@ -67,43 +64,13 @@ class FrameworkLoader:
|
|
67
64
|
"""
|
68
65
|
Load and prepare framework instructions for injection.
|
69
66
|
|
70
|
-
This
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
The framework loader supports custom instructions through .claude-mpm/ directories.
|
78
|
-
It NEVER reads from .claude/ directories to avoid conflicts with Claude Code.
|
79
|
-
|
80
|
-
File Loading Precedence (highest to lowest):
|
81
|
-
|
82
|
-
INSTRUCTIONS.md:
|
83
|
-
1. Project: ./.claude-mpm/INSTRUCTIONS.md
|
84
|
-
2. User: ~/.claude-mpm/INSTRUCTIONS.md
|
85
|
-
3. System: (built-in framework instructions)
|
86
|
-
|
87
|
-
WORKFLOW.md:
|
88
|
-
1. Project: ./.claude-mpm/WORKFLOW.md
|
89
|
-
2. User: ~/.claude-mpm/WORKFLOW.md
|
90
|
-
3. System: src/claude_mpm/agents/WORKFLOW.md
|
91
|
-
|
92
|
-
MEMORY.md:
|
93
|
-
1. Project: ./.claude-mpm/MEMORY.md
|
94
|
-
2. User: ~/.claude-mpm/MEMORY.md
|
95
|
-
3. System: src/claude_mpm/agents/MEMORY.md
|
96
|
-
|
97
|
-
Actual Memories:
|
98
|
-
- User: ~/.claude-mpm/memories/PM_memories.md
|
99
|
-
- Project: ./.claude-mpm/memories/PM_memories.md (overrides user)
|
100
|
-
- Agent memories: *_memories.md files (only loaded if agent is deployed)
|
101
|
-
|
102
|
-
Important Notes:
|
103
|
-
- Project-level files always override user-level files
|
104
|
-
- User-level files always override system defaults
|
105
|
-
- The framework NEVER reads from .claude/ directories
|
106
|
-
- Custom instructions are clearly labeled with their source level
|
67
|
+
This refactored version uses modular components for better maintainability
|
68
|
+
and testability while maintaining backward compatibility.
|
69
|
+
|
70
|
+
Components:
|
71
|
+
- Loaders: Handle file I/O and resource loading
|
72
|
+
- Formatters: Generate and format content sections
|
73
|
+
- Processors: Process metadata, templates, and memories
|
107
74
|
"""
|
108
75
|
|
109
76
|
def __init__(
|
@@ -114,7 +81,7 @@ class FrameworkLoader:
|
|
114
81
|
config: Optional[Dict[str, Any]] = None,
|
115
82
|
):
|
116
83
|
"""
|
117
|
-
Initialize framework loader.
|
84
|
+
Initialize framework loader with modular components.
|
118
85
|
|
119
86
|
Args:
|
120
87
|
framework_path: Explicit path to framework (auto-detected if None)
|
@@ -129,6 +96,37 @@ class FrameworkLoader:
|
|
129
96
|
self.config = config or {}
|
130
97
|
|
131
98
|
# Validate API keys on startup (before any other initialization)
|
99
|
+
self._validate_api_keys()
|
100
|
+
|
101
|
+
# Initialize service container
|
102
|
+
self.container = service_container or get_global_container()
|
103
|
+
self._register_services()
|
104
|
+
|
105
|
+
# Resolve services from container
|
106
|
+
self._cache_manager = self.container.resolve(ICacheManager)
|
107
|
+
self._path_resolver = self.container.resolve(IPathResolver)
|
108
|
+
self._memory_manager = self.container.resolve(IMemoryManager)
|
109
|
+
|
110
|
+
# Initialize framework path
|
111
|
+
self.framework_path = framework_path or self._path_resolver.detect_framework_path()
|
112
|
+
|
113
|
+
# Initialize modular components
|
114
|
+
self._init_components()
|
115
|
+
|
116
|
+
# Keep cache TTL constants for backward compatibility
|
117
|
+
self._init_cache_ttl()
|
118
|
+
|
119
|
+
# Load framework content
|
120
|
+
self.framework_content = self._load_framework_content()
|
121
|
+
|
122
|
+
# Initialize agent registry
|
123
|
+
self.agent_registry = AgentRegistryAdapter(self.framework_path)
|
124
|
+
|
125
|
+
# Output style manager (deferred initialization)
|
126
|
+
self.output_style_manager = None
|
127
|
+
|
128
|
+
def _validate_api_keys(self) -> None:
|
129
|
+
"""Validate API keys if enabled in config."""
|
132
130
|
if self.config.get("validate_api_keys", True):
|
133
131
|
try:
|
134
132
|
self.logger.info("Validating configured API keys...")
|
@@ -141,21 +139,17 @@ class FrameworkLoader:
|
|
141
139
|
self.logger.error(f"❌ Unexpected error during API validation: {e}")
|
142
140
|
raise
|
143
141
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
# Register services if not already registered
|
142
|
+
def _register_services(self) -> None:
|
143
|
+
"""Register services in the container if not already registered."""
|
148
144
|
if not self.container.is_registered(ICacheManager):
|
149
|
-
self.container.register(ICacheManager, CacheManager, True)
|
145
|
+
self.container.register(ICacheManager, CacheManager, True)
|
150
146
|
|
151
147
|
if not self.container.is_registered(IPathResolver):
|
152
|
-
# PathResolver depends on CacheManager, so resolve it first
|
153
148
|
cache_manager = self.container.resolve(ICacheManager)
|
154
149
|
path_resolver = PathResolver(cache_manager=cache_manager)
|
155
150
|
self.container.register_instance(IPathResolver, path_resolver)
|
156
151
|
|
157
152
|
if not self.container.is_registered(IMemoryManager):
|
158
|
-
# MemoryManager depends on both CacheManager and PathResolver
|
159
153
|
cache_manager = self.container.resolve(ICacheManager)
|
160
154
|
path_resolver = self.container.resolve(IPathResolver)
|
161
155
|
memory_manager = MemoryManager(
|
@@ -163,18 +157,26 @@ class FrameworkLoader:
|
|
163
157
|
)
|
164
158
|
self.container.register_instance(IMemoryManager, memory_manager)
|
165
159
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
self.
|
170
|
-
|
171
|
-
|
172
|
-
self.
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
160
|
+
def _init_components(self) -> None:
|
161
|
+
"""Initialize modular components."""
|
162
|
+
# Loaders
|
163
|
+
self.file_loader = FileLoader()
|
164
|
+
self.packaged_loader = PackagedLoader()
|
165
|
+
self.instruction_loader = InstructionLoader(self.framework_path)
|
166
|
+
self.agent_loader = AgentLoader(self.framework_path)
|
167
|
+
|
168
|
+
# Formatters
|
169
|
+
self.content_formatter = ContentFormatter()
|
170
|
+
self.capability_generator = CapabilityGenerator()
|
171
|
+
self.context_generator = ContextGenerator()
|
172
|
+
|
173
|
+
# Processors
|
174
|
+
self.metadata_processor = MetadataProcessor()
|
175
|
+
self.template_processor = TemplateProcessor(self.framework_path)
|
176
|
+
self.memory_processor = MemoryProcessor()
|
177
|
+
|
178
|
+
def _init_cache_ttl(self) -> None:
|
179
|
+
"""Initialize cache TTL constants for backward compatibility."""
|
178
180
|
if hasattr(self._cache_manager, "capabilities_ttl"):
|
179
181
|
self.CAPABILITIES_CACHE_TTL = self._cache_manager.capabilities_ttl
|
180
182
|
self.DEPLOYED_AGENTS_CACHE_TTL = self._cache_manager.deployed_agents_ttl
|
@@ -187,430 +189,24 @@ class FrameworkLoader:
|
|
187
189
|
self.METADATA_CACHE_TTL = 60
|
188
190
|
self.MEMORIES_CACHE_TTL = 60
|
189
191
|
|
190
|
-
|
191
|
-
|
192
|
-
# Initialize agent registry
|
193
|
-
self.agent_registry = AgentRegistryAdapter(self.framework_path)
|
194
|
-
|
195
|
-
# Initialize output style manager (must be after content is loaded)
|
196
|
-
self.output_style_manager = None
|
197
|
-
# Defer initialization until first use to ensure content is loaded
|
192
|
+
# === Cache Management Methods (backward compatibility) ===
|
198
193
|
|
199
194
|
def clear_all_caches(self) -> None:
|
200
195
|
"""Clear all caches to force reload on next access."""
|
201
196
|
self._cache_manager.clear_all()
|
202
197
|
|
203
198
|
def clear_agent_caches(self) -> None:
|
204
|
-
"""Clear agent-related caches
|
199
|
+
"""Clear agent-related caches."""
|
205
200
|
self._cache_manager.clear_agent_caches()
|
206
201
|
|
207
202
|
def clear_memory_caches(self) -> None:
|
208
203
|
"""Clear memory-related caches."""
|
209
204
|
self._cache_manager.clear_memory_caches()
|
210
205
|
|
211
|
-
|
212
|
-
"""Initialize output style management and deploy if applicable."""
|
213
|
-
try:
|
214
|
-
from claude_mpm.core.output_style_manager import OutputStyleManager
|
215
|
-
|
216
|
-
self.output_style_manager = OutputStyleManager()
|
217
|
-
|
218
|
-
# Log detailed output style status
|
219
|
-
self._log_output_style_status()
|
220
|
-
|
221
|
-
# Extract and save output style content (pass self to reuse loaded content)
|
222
|
-
output_style_content = (
|
223
|
-
self.output_style_manager.extract_output_style_content(
|
224
|
-
framework_loader=self
|
225
|
-
)
|
226
|
-
)
|
227
|
-
self.output_style_manager.save_output_style(output_style_content)
|
228
|
-
|
229
|
-
# Deploy to Claude Code if supported
|
230
|
-
deployed = self.output_style_manager.deploy_output_style(
|
231
|
-
output_style_content
|
232
|
-
)
|
233
|
-
|
234
|
-
if deployed:
|
235
|
-
self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
|
236
|
-
else:
|
237
|
-
self.logger.info(
|
238
|
-
"📝 Output style will be injected into instructions for older Claude versions"
|
239
|
-
)
|
240
|
-
|
241
|
-
except Exception as e:
|
242
|
-
self.logger.warning(f"❌ Failed to initialize output style manager: {e}")
|
243
|
-
# Continue without output style management
|
244
|
-
|
245
|
-
def _log_output_style_status(self) -> None:
|
246
|
-
"""Log comprehensive output style status information."""
|
247
|
-
if not self.output_style_manager:
|
248
|
-
return
|
249
|
-
|
250
|
-
# Claude version detection
|
251
|
-
claude_version = self.output_style_manager.claude_version
|
252
|
-
if claude_version:
|
253
|
-
self.logger.info(f"Claude Code version detected: {claude_version}")
|
254
|
-
|
255
|
-
# Check if version supports output styles
|
256
|
-
if self.output_style_manager.supports_output_styles():
|
257
|
-
self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
|
258
|
-
|
259
|
-
# Check deployment status
|
260
|
-
output_style_path = self.output_style_manager.output_style_path
|
261
|
-
if output_style_path.exists():
|
262
|
-
self.logger.info(
|
263
|
-
f"📁 Output style file exists: {output_style_path}"
|
264
|
-
)
|
265
|
-
else:
|
266
|
-
self.logger.info(
|
267
|
-
f"📝 Output style will be created at: {output_style_path}"
|
268
|
-
)
|
269
|
-
|
270
|
-
else:
|
271
|
-
self.logger.info(
|
272
|
-
f"⚠️ Claude Code {claude_version} does not support output styles (< 1.0.83)"
|
273
|
-
)
|
274
|
-
self.logger.info(
|
275
|
-
"📝 Output style content will be injected into framework instructions"
|
276
|
-
)
|
277
|
-
else:
|
278
|
-
self.logger.info("⚠️ Claude Code not detected or version unknown")
|
279
|
-
self.logger.info(
|
280
|
-
"📝 Output style content will be injected into framework instructions as fallback"
|
281
|
-
)
|
282
|
-
|
283
|
-
def _try_load_file(self, file_path: Path, file_type: str) -> Optional[str]:
|
284
|
-
"""
|
285
|
-
Try to load a file with error handling.
|
286
|
-
|
287
|
-
Args:
|
288
|
-
file_path: Path to the file to load
|
289
|
-
file_type: Description of file type for logging
|
290
|
-
|
291
|
-
Returns:
|
292
|
-
File content if successful, None otherwise
|
293
|
-
"""
|
294
|
-
try:
|
295
|
-
content = file_path.read_text()
|
296
|
-
if hasattr(self.logger, "level") and self.logger.level <= logging.INFO:
|
297
|
-
self.logger.info(f"Loaded {file_type} from: {file_path}")
|
298
|
-
|
299
|
-
# Extract metadata if present
|
300
|
-
import re
|
301
|
-
|
302
|
-
version_match = re.search(r"<!-- FRAMEWORK_VERSION: (\d+) -->", content)
|
303
|
-
if version_match:
|
304
|
-
version = version_match.group(
|
305
|
-
1
|
306
|
-
) # Keep as string to preserve leading zeros
|
307
|
-
self.logger.info(f"Framework version: {version}")
|
308
|
-
# Store framework version if this is the main INSTRUCTIONS.md
|
309
|
-
if "INSTRUCTIONS.md" in str(file_path):
|
310
|
-
self.framework_version = version
|
311
|
-
|
312
|
-
# Extract modification timestamp
|
313
|
-
timestamp_match = re.search(r"<!-- LAST_MODIFIED: ([^>]+) -->", content)
|
314
|
-
if timestamp_match:
|
315
|
-
timestamp = timestamp_match.group(1).strip()
|
316
|
-
self.logger.info(f"Last modified: {timestamp}")
|
317
|
-
# Store timestamp if this is the main INSTRUCTIONS.md
|
318
|
-
if "INSTRUCTIONS.md" in str(file_path):
|
319
|
-
self.framework_last_modified = timestamp
|
320
|
-
|
321
|
-
return content
|
322
|
-
except Exception as e:
|
323
|
-
if hasattr(self.logger, "level") and self.logger.level <= logging.ERROR:
|
324
|
-
self.logger.error(f"Failed to load {file_type}: {e}")
|
325
|
-
return None
|
326
|
-
|
327
|
-
def _load_instructions_file(self, content: Dict[str, Any]) -> None:
|
328
|
-
"""
|
329
|
-
Load custom INSTRUCTIONS.md from .claude-mpm directories.
|
330
|
-
|
331
|
-
Precedence (highest to lowest):
|
332
|
-
1. Project-specific: ./.claude-mpm/INSTRUCTIONS.md
|
333
|
-
2. User-specific: ~/.claude-mpm/INSTRUCTIONS.md
|
334
|
-
|
335
|
-
NOTE: We do NOT load CLAUDE.md files since Claude Code already picks them up automatically.
|
336
|
-
This prevents duplication of instructions.
|
337
|
-
|
338
|
-
Args:
|
339
|
-
content: Dictionary to update with loaded instructions
|
340
|
-
"""
|
341
|
-
# Check for project-specific INSTRUCTIONS.md first
|
342
|
-
project_instructions_path = Path.cwd() / ".claude-mpm" / "INSTRUCTIONS.md"
|
343
|
-
if project_instructions_path.exists():
|
344
|
-
loaded_content = self._try_load_file(
|
345
|
-
project_instructions_path, "project-specific INSTRUCTIONS.md"
|
346
|
-
)
|
347
|
-
if loaded_content:
|
348
|
-
content["custom_instructions"] = loaded_content
|
349
|
-
content["custom_instructions_level"] = "project"
|
350
|
-
self.logger.info(
|
351
|
-
"Using project-specific PM instructions from .claude-mpm/INSTRUCTIONS.md"
|
352
|
-
)
|
353
|
-
return
|
354
|
-
|
355
|
-
# Check for user-specific INSTRUCTIONS.md
|
356
|
-
user_instructions_path = Path.home() / ".claude-mpm" / "INSTRUCTIONS.md"
|
357
|
-
if user_instructions_path.exists():
|
358
|
-
loaded_content = self._try_load_file(
|
359
|
-
user_instructions_path, "user-specific INSTRUCTIONS.md"
|
360
|
-
)
|
361
|
-
if loaded_content:
|
362
|
-
content["custom_instructions"] = loaded_content
|
363
|
-
content["custom_instructions_level"] = "user"
|
364
|
-
self.logger.info(
|
365
|
-
"Using user-specific PM instructions from ~/.claude-mpm/INSTRUCTIONS.md"
|
366
|
-
)
|
367
|
-
return
|
368
|
-
|
369
|
-
def _load_workflow_instructions(self, content: Dict[str, Any]) -> None:
|
370
|
-
"""
|
371
|
-
Load WORKFLOW.md from .claude-mpm directories.
|
372
|
-
|
373
|
-
Precedence (highest to lowest):
|
374
|
-
1. Project-specific: ./.claude-mpm/WORKFLOW.md
|
375
|
-
2. User-specific: ~/.claude-mpm/WORKFLOW.md
|
376
|
-
3. System default: src/claude_mpm/agents/WORKFLOW.md or packaged
|
377
|
-
|
378
|
-
NOTE: We do NOT load from .claude/ directories to avoid conflicts.
|
379
|
-
|
380
|
-
Args:
|
381
|
-
content: Dictionary to update with workflow instructions
|
382
|
-
"""
|
383
|
-
# Check for project-specific WORKFLOW.md first (highest priority)
|
384
|
-
project_workflow_path = Path.cwd() / ".claude-mpm" / "WORKFLOW.md"
|
385
|
-
if project_workflow_path.exists():
|
386
|
-
loaded_content = self._try_load_file(
|
387
|
-
project_workflow_path, "project-specific WORKFLOW.md"
|
388
|
-
)
|
389
|
-
if loaded_content:
|
390
|
-
content["workflow_instructions"] = loaded_content
|
391
|
-
content["workflow_instructions_level"] = "project"
|
392
|
-
self.logger.info(
|
393
|
-
"Using project-specific workflow instructions from .claude-mpm/WORKFLOW.md"
|
394
|
-
)
|
395
|
-
return
|
396
|
-
|
397
|
-
# Check for user-specific WORKFLOW.md (medium priority)
|
398
|
-
user_workflow_path = Path.home() / ".claude-mpm" / "WORKFLOW.md"
|
399
|
-
if user_workflow_path.exists():
|
400
|
-
loaded_content = self._try_load_file(
|
401
|
-
user_workflow_path, "user-specific WORKFLOW.md"
|
402
|
-
)
|
403
|
-
if loaded_content:
|
404
|
-
content["workflow_instructions"] = loaded_content
|
405
|
-
content["workflow_instructions_level"] = "user"
|
406
|
-
self.logger.info(
|
407
|
-
"Using user-specific workflow instructions from ~/.claude-mpm/WORKFLOW.md"
|
408
|
-
)
|
409
|
-
return
|
410
|
-
|
411
|
-
# Fall back to system workflow (lowest priority)
|
412
|
-
if self.framework_path and self.framework_path != Path("__PACKAGED__"):
|
413
|
-
system_workflow_path = (
|
414
|
-
self.framework_path / "src" / "claude_mpm" / "agents" / "WORKFLOW.md"
|
415
|
-
)
|
416
|
-
if system_workflow_path.exists():
|
417
|
-
loaded_content = self._try_load_file(
|
418
|
-
system_workflow_path, "system WORKFLOW.md"
|
419
|
-
)
|
420
|
-
if loaded_content:
|
421
|
-
content["workflow_instructions"] = loaded_content
|
422
|
-
content["workflow_instructions_level"] = "system"
|
423
|
-
self.logger.info("Using system workflow instructions")
|
424
|
-
|
425
|
-
def _load_memory_instructions(self, content: Dict[str, Any]) -> None:
|
426
|
-
"""
|
427
|
-
Load MEMORY.md from .claude-mpm directories.
|
428
|
-
|
429
|
-
Precedence (highest to lowest):
|
430
|
-
1. Project-specific: ./.claude-mpm/MEMORY.md
|
431
|
-
2. User-specific: ~/.claude-mpm/MEMORY.md
|
432
|
-
3. System default: src/claude_mpm/agents/MEMORY.md or packaged
|
433
|
-
|
434
|
-
NOTE: We do NOT load from .claude/ directories to avoid conflicts.
|
435
|
-
|
436
|
-
Args:
|
437
|
-
content: Dictionary to update with memory instructions
|
438
|
-
"""
|
439
|
-
# Check for project-specific MEMORY.md first (highest priority)
|
440
|
-
project_memory_path = Path.cwd() / ".claude-mpm" / "MEMORY.md"
|
441
|
-
if project_memory_path.exists():
|
442
|
-
loaded_content = self._try_load_file(
|
443
|
-
project_memory_path, "project-specific MEMORY.md"
|
444
|
-
)
|
445
|
-
if loaded_content:
|
446
|
-
content["memory_instructions"] = loaded_content
|
447
|
-
content["memory_instructions_level"] = "project"
|
448
|
-
self.logger.info(
|
449
|
-
"Using project-specific memory instructions from .claude-mpm/MEMORY.md"
|
450
|
-
)
|
451
|
-
return
|
452
|
-
|
453
|
-
# Check for user-specific MEMORY.md (medium priority)
|
454
|
-
user_memory_path = Path.home() / ".claude-mpm" / "MEMORY.md"
|
455
|
-
if user_memory_path.exists():
|
456
|
-
loaded_content = self._try_load_file(
|
457
|
-
user_memory_path, "user-specific MEMORY.md"
|
458
|
-
)
|
459
|
-
if loaded_content:
|
460
|
-
content["memory_instructions"] = loaded_content
|
461
|
-
content["memory_instructions_level"] = "user"
|
462
|
-
self.logger.info(
|
463
|
-
"Using user-specific memory instructions from ~/.claude-mpm/MEMORY.md"
|
464
|
-
)
|
465
|
-
return
|
466
|
-
|
467
|
-
# Fall back to system memory instructions (lowest priority)
|
468
|
-
if self.framework_path and self.framework_path != Path("__PACKAGED__"):
|
469
|
-
system_memory_path = (
|
470
|
-
self.framework_path / "src" / "claude_mpm" / "agents" / "MEMORY.md"
|
471
|
-
)
|
472
|
-
if system_memory_path.exists():
|
473
|
-
loaded_content = self._try_load_file(
|
474
|
-
system_memory_path, "system MEMORY.md"
|
475
|
-
)
|
476
|
-
if loaded_content:
|
477
|
-
content["memory_instructions"] = loaded_content
|
478
|
-
content["memory_instructions_level"] = "system"
|
479
|
-
self.logger.info("Using system memory instructions")
|
480
|
-
|
481
|
-
def _get_deployed_agents(self) -> set:
|
482
|
-
"""
|
483
|
-
Get a set of deployed agent names from .claude/agents/ directories.
|
484
|
-
Uses caching to avoid repeated filesystem scans.
|
485
|
-
|
486
|
-
Returns:
|
487
|
-
Set of agent names (file stems) that are deployed
|
488
|
-
"""
|
489
|
-
# Try to get from cache first
|
490
|
-
cached = self._cache_manager.get_deployed_agents()
|
491
|
-
if cached is not None:
|
492
|
-
return cached
|
493
|
-
|
494
|
-
# Cache miss or expired - perform actual scan
|
495
|
-
self.logger.debug("Scanning for deployed agents (cache miss or expired)")
|
496
|
-
deployed = set()
|
497
|
-
|
498
|
-
# Check multiple locations for deployed agents
|
499
|
-
agents_dirs = [
|
500
|
-
Path.cwd() / ".claude" / "agents", # Project-specific agents
|
501
|
-
Path.home() / ".claude" / "agents", # User's system agents
|
502
|
-
]
|
503
|
-
|
504
|
-
for agents_dir in agents_dirs:
|
505
|
-
if agents_dir.exists():
|
506
|
-
for agent_file in agents_dir.glob("*.md"):
|
507
|
-
if not agent_file.name.startswith("."):
|
508
|
-
# Use stem to get agent name without extension
|
509
|
-
deployed.add(agent_file.stem)
|
510
|
-
self.logger.debug(
|
511
|
-
f"Found deployed agent: {agent_file.stem} in {agents_dir}"
|
512
|
-
)
|
513
|
-
|
514
|
-
self.logger.debug(f"Total deployed agents found: {len(deployed)}")
|
515
|
-
|
516
|
-
# Update cache
|
517
|
-
self._cache_manager.set_deployed_agents(deployed)
|
518
|
-
|
519
|
-
return deployed
|
520
|
-
|
521
|
-
def _load_actual_memories(self, content: Dict[str, Any]) -> None:
|
522
|
-
"""
|
523
|
-
Load actual memories using the MemoryManager service.
|
524
|
-
|
525
|
-
This method delegates all memory loading operations to the MemoryManager,
|
526
|
-
which handles caching, aggregation, deduplication, and legacy format migration.
|
527
|
-
|
528
|
-
Args:
|
529
|
-
content: Dictionary to update with actual memories
|
530
|
-
"""
|
531
|
-
# Use MemoryManager to load all memories
|
532
|
-
memories = self._memory_manager.load_memories()
|
533
|
-
|
534
|
-
# Apply loaded memories to content
|
535
|
-
if "actual_memories" in memories:
|
536
|
-
content["actual_memories"] = memories["actual_memories"]
|
537
|
-
if "agent_memories" in memories:
|
538
|
-
content["agent_memories"] = memories["agent_memories"]
|
539
|
-
|
540
|
-
def _load_single_agent(
|
541
|
-
self, agent_file: Path
|
542
|
-
) -> tuple[Optional[str], Optional[str]]:
|
543
|
-
"""
|
544
|
-
Load a single agent file.
|
545
|
-
|
546
|
-
Args:
|
547
|
-
agent_file: Path to the agent file
|
548
|
-
|
549
|
-
Returns:
|
550
|
-
Tuple of (agent_name, agent_content) or (None, None) on failure
|
551
|
-
"""
|
552
|
-
try:
|
553
|
-
agent_name = agent_file.stem
|
554
|
-
# Skip README files
|
555
|
-
if agent_name.upper() == "README":
|
556
|
-
return None, None
|
557
|
-
content = agent_file.read_text()
|
558
|
-
self.logger.debug(f"Loaded agent: {agent_name}")
|
559
|
-
return agent_name, content
|
560
|
-
except Exception as e:
|
561
|
-
self.logger.error(f"Failed to load agent {agent_file}: {e}")
|
562
|
-
return None, None
|
563
|
-
|
564
|
-
def _load_base_agent_fallback(
|
565
|
-
self, content: Dict[str, Any], main_dir: Optional[Path]
|
566
|
-
) -> None:
|
567
|
-
"""
|
568
|
-
Load base_agent.md from main directory as fallback.
|
569
|
-
|
570
|
-
Args:
|
571
|
-
content: Dictionary to update with base agent
|
572
|
-
main_dir: Main agents directory path
|
573
|
-
"""
|
574
|
-
if main_dir and main_dir.exists() and "base_agent" not in content["agents"]:
|
575
|
-
base_agent_file = main_dir / "base_agent.md"
|
576
|
-
if base_agent_file.exists():
|
577
|
-
agent_name, agent_content = self._load_single_agent(base_agent_file)
|
578
|
-
if agent_name and agent_content:
|
579
|
-
content["agents"][agent_name] = agent_content
|
580
|
-
|
581
|
-
def _load_agents_directory(
|
582
|
-
self,
|
583
|
-
content: Dict[str, Any],
|
584
|
-
agents_dir: Optional[Path],
|
585
|
-
templates_dir: Optional[Path],
|
586
|
-
main_dir: Optional[Path],
|
587
|
-
) -> None:
|
588
|
-
"""
|
589
|
-
Load agent definitions from the appropriate directory.
|
590
|
-
|
591
|
-
Args:
|
592
|
-
content: Dictionary to update with loaded agents
|
593
|
-
agents_dir: Primary agents directory to load from
|
594
|
-
templates_dir: Templates directory path
|
595
|
-
main_dir: Main agents directory path
|
596
|
-
"""
|
597
|
-
if not agents_dir or not agents_dir.exists():
|
598
|
-
return
|
599
|
-
|
600
|
-
content["loaded"] = True
|
601
|
-
|
602
|
-
# Load all agent files
|
603
|
-
for agent_file in agents_dir.glob("*.md"):
|
604
|
-
agent_name, agent_content = self._load_single_agent(agent_file)
|
605
|
-
if agent_name and agent_content:
|
606
|
-
content["agents"][agent_name] = agent_content
|
607
|
-
|
608
|
-
# If we used templates dir, also check main dir for base_agent.md
|
609
|
-
if agents_dir == templates_dir:
|
610
|
-
self._load_base_agent_fallback(content, main_dir)
|
206
|
+
# === Content Loading Methods ===
|
611
207
|
|
612
208
|
def _load_framework_content(self) -> Dict[str, Any]:
|
613
|
-
"""Load framework content."""
|
209
|
+
"""Load framework content using modular components."""
|
614
210
|
content = {
|
615
211
|
"claude_md": "",
|
616
212
|
"agents": {},
|
@@ -619,290 +215,98 @@ class FrameworkLoader:
|
|
619
215
|
"working_claude_md": "",
|
620
216
|
"framework_instructions": "",
|
621
217
|
"workflow_instructions": "",
|
622
|
-
"workflow_instructions_level": "",
|
218
|
+
"workflow_instructions_level": "",
|
623
219
|
"memory_instructions": "",
|
624
|
-
"memory_instructions_level": "",
|
625
|
-
"project_workflow": "", # Deprecated
|
626
|
-
"project_memory": "",
|
627
|
-
"actual_memories": "",
|
220
|
+
"memory_instructions_level": "",
|
221
|
+
"project_workflow": "", # Deprecated
|
222
|
+
"project_memory": "", # Deprecated
|
223
|
+
"actual_memories": "",
|
224
|
+
"agent_memories": {},
|
628
225
|
}
|
629
226
|
|
630
|
-
# Load instructions
|
631
|
-
self.
|
227
|
+
# Load all instructions
|
228
|
+
self.instruction_loader.load_all_instructions(content)
|
632
229
|
|
633
|
-
|
634
|
-
|
230
|
+
# Transfer metadata from loaders
|
231
|
+
if self.file_loader.framework_version:
|
232
|
+
self.framework_version = self.file_loader.framework_version
|
233
|
+
content["version"] = self.framework_version
|
234
|
+
if self.file_loader.framework_last_modified:
|
235
|
+
self.framework_last_modified = self.file_loader.framework_last_modified
|
635
236
|
|
636
|
-
#
|
637
|
-
if self.framework_path == Path("__PACKAGED__"):
|
638
|
-
# Load files using importlib.resources for packaged installations
|
639
|
-
self._load_packaged_framework_content(content)
|
640
|
-
else:
|
641
|
-
# Load from filesystem for development mode
|
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
|
-
)
|
650
|
-
framework_instructions_path = (
|
651
|
-
self.framework_path
|
652
|
-
/ "src"
|
653
|
-
/ "claude_mpm"
|
654
|
-
/ "agents"
|
655
|
-
/ "INSTRUCTIONS.md"
|
656
|
-
)
|
657
|
-
|
658
|
-
# Try loading new consolidated file first
|
659
|
-
if pm_instructions_path.exists():
|
660
|
-
loaded_content = self._try_load_file(
|
661
|
-
pm_instructions_path, "consolidated PM_INSTRUCTIONS.md"
|
662
|
-
)
|
663
|
-
if loaded_content:
|
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
|
-
)
|
676
|
-
content["loaded"] = True
|
677
|
-
# Add framework version to content
|
678
|
-
if self.framework_version:
|
679
|
-
content["instructions_version"] = self.framework_version
|
680
|
-
content["version"] = (
|
681
|
-
self.framework_version
|
682
|
-
) # Update main version key
|
683
|
-
# Add modification timestamp to content
|
684
|
-
if self.framework_last_modified:
|
685
|
-
content["instructions_last_modified"] = (
|
686
|
-
self.framework_last_modified
|
687
|
-
)
|
688
|
-
|
689
|
-
# Load BASE_PM.md for core framework requirements
|
690
|
-
base_pm_path = (
|
691
|
-
self.framework_path / "src" / "claude_mpm" / "agents" / "BASE_PM.md"
|
692
|
-
)
|
693
|
-
if base_pm_path.exists():
|
694
|
-
base_pm_content = self._try_load_file(
|
695
|
-
base_pm_path, "BASE_PM framework requirements"
|
696
|
-
)
|
697
|
-
if base_pm_content:
|
698
|
-
content["base_pm_instructions"] = base_pm_content
|
699
|
-
|
700
|
-
# Load WORKFLOW.md - check for project-specific first, then system
|
701
|
-
self._load_workflow_instructions(content)
|
702
|
-
|
703
|
-
# Load MEMORY.md - check for project-specific first, then system
|
704
|
-
self._load_memory_instructions(content)
|
705
|
-
|
706
|
-
# Load actual memories from .claude-mpm/memories/PM_memories.md
|
237
|
+
# Load memories
|
707
238
|
self._load_actual_memories(content)
|
708
239
|
|
709
|
-
# Discover
|
240
|
+
# Discover and load agents
|
710
241
|
agents_dir, templates_dir, main_dir = self._path_resolver.discover_agent_paths(
|
711
242
|
agents_dir=self.agents_dir, framework_path=self.framework_path
|
712
243
|
)
|
713
|
-
|
714
|
-
|
715
|
-
|
244
|
+
agents = self.agent_loader.load_agents_directory(agents_dir, templates_dir, main_dir)
|
245
|
+
if agents:
|
246
|
+
content["agents"] = agents
|
247
|
+
content["loaded"] = True
|
716
248
|
|
717
249
|
return content
|
718
250
|
|
719
|
-
def
|
720
|
-
"""Load
|
721
|
-
|
722
|
-
self.logger.warning(
|
723
|
-
"importlib.resources not available, cannot load packaged framework"
|
724
|
-
)
|
725
|
-
self.logger.debug(f"files variable is: {files}")
|
726
|
-
# Try alternative import methods
|
727
|
-
try:
|
728
|
-
from importlib import resources
|
729
|
-
|
730
|
-
self.logger.info("Using importlib.resources as fallback")
|
731
|
-
self._load_packaged_framework_content_fallback(content, resources)
|
732
|
-
return
|
733
|
-
except ImportError:
|
734
|
-
self.logger.error(
|
735
|
-
"No importlib.resources available, using minimal framework"
|
736
|
-
)
|
737
|
-
return
|
738
|
-
|
739
|
-
try:
|
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
|
744
|
-
content["loaded"] = True
|
745
|
-
self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md from package")
|
746
|
-
# Extract and store version/timestamp metadata
|
747
|
-
self._extract_metadata_from_content(
|
748
|
-
pm_instructions_content, "PM_INSTRUCTIONS.md"
|
749
|
-
)
|
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
|
767
|
-
|
768
|
-
# Load BASE_PM.md
|
769
|
-
base_pm_content = self._load_packaged_file("BASE_PM.md")
|
770
|
-
if base_pm_content:
|
771
|
-
content["base_pm_instructions"] = base_pm_content
|
772
|
-
|
773
|
-
# Load WORKFLOW.md
|
774
|
-
workflow_content = self._load_packaged_file("WORKFLOW.md")
|
775
|
-
if workflow_content:
|
776
|
-
content["workflow_instructions"] = workflow_content
|
777
|
-
content["project_workflow"] = "system"
|
778
|
-
|
779
|
-
# Load MEMORY.md
|
780
|
-
memory_content = self._load_packaged_file("MEMORY.md")
|
781
|
-
if memory_content:
|
782
|
-
content["memory_instructions"] = memory_content
|
783
|
-
content["project_memory"] = "system"
|
784
|
-
|
785
|
-
except Exception as e:
|
786
|
-
self.logger.error(f"Failed to load packaged framework content: {e}")
|
251
|
+
def _load_actual_memories(self, content: Dict[str, Any]) -> None:
|
252
|
+
"""Load actual memories using the MemoryManager service."""
|
253
|
+
memories = self._memory_manager.load_memories()
|
787
254
|
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
try:
|
793
|
-
# Try new consolidated PM_INSTRUCTIONS.md first
|
794
|
-
pm_instructions_content = self._load_packaged_file_fallback(
|
795
|
-
"PM_INSTRUCTIONS.md", resources
|
796
|
-
)
|
797
|
-
if pm_instructions_content:
|
798
|
-
content["framework_instructions"] = pm_instructions_content
|
799
|
-
content["loaded"] = True
|
800
|
-
self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md via fallback")
|
801
|
-
# Extract and store version/timestamp metadata
|
802
|
-
self._extract_metadata_from_content(
|
803
|
-
pm_instructions_content, "PM_INSTRUCTIONS.md"
|
804
|
-
)
|
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
|
824
|
-
|
825
|
-
# Load BASE_PM.md
|
826
|
-
base_pm_content = self._load_packaged_file_fallback("BASE_PM.md", resources)
|
827
|
-
if base_pm_content:
|
828
|
-
content["base_pm_instructions"] = base_pm_content
|
829
|
-
|
830
|
-
# Load WORKFLOW.md
|
831
|
-
workflow_content = self._load_packaged_file_fallback(
|
832
|
-
"WORKFLOW.md", resources
|
833
|
-
)
|
834
|
-
if workflow_content:
|
835
|
-
content["workflow_instructions"] = workflow_content
|
836
|
-
content["project_workflow"] = "system"
|
255
|
+
if "actual_memories" in memories:
|
256
|
+
content["actual_memories"] = memories["actual_memories"]
|
257
|
+
if "agent_memories" in memories:
|
258
|
+
content["agent_memories"] = memories["agent_memories"]
|
837
259
|
|
838
|
-
|
839
|
-
memory_content = self._load_packaged_file_fallback("MEMORY.md", resources)
|
840
|
-
if memory_content:
|
841
|
-
content["memory_instructions"] = memory_content
|
842
|
-
content["project_memory"] = "system"
|
260
|
+
# === Agent Discovery Methods ===
|
843
261
|
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
262
|
+
def _get_deployed_agents(self) -> Set[str]:
|
263
|
+
"""Get deployed agents with caching."""
|
264
|
+
cached = self._cache_manager.get_deployed_agents()
|
265
|
+
if cached is not None:
|
266
|
+
return cached
|
848
267
|
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
# Try different resource loading methods
|
853
|
-
try:
|
854
|
-
# Method 1: resources.read_text (Python 3.9+)
|
855
|
-
content = resources.read_text("claude_mpm.agents", filename)
|
856
|
-
self.logger.info(f"Loaded {filename} from package using read_text")
|
857
|
-
return content
|
858
|
-
except AttributeError:
|
859
|
-
# Method 2: resources.files (Python 3.9+)
|
860
|
-
agents_files = resources.files("claude_mpm.agents")
|
861
|
-
file_path = agents_files / filename
|
862
|
-
if file_path.is_file():
|
863
|
-
content = file_path.read_text()
|
864
|
-
self.logger.info(f"Loaded {filename} from package using files")
|
865
|
-
return content
|
866
|
-
self.logger.warning(f"File {filename} not found in package")
|
867
|
-
return None
|
868
|
-
except Exception as e:
|
869
|
-
self.logger.error(
|
870
|
-
f"Failed to load {filename} from package with fallback: {e}"
|
871
|
-
)
|
872
|
-
return None
|
268
|
+
deployed = self.agent_loader.get_deployed_agents()
|
269
|
+
self._cache_manager.set_deployed_agents(deployed)
|
270
|
+
return deployed
|
873
271
|
|
874
|
-
def
|
875
|
-
"""
|
876
|
-
|
877
|
-
# Use importlib.resources to load file from package
|
878
|
-
agents_package = files("claude_mpm.agents")
|
879
|
-
file_path = agents_package / filename
|
880
|
-
|
881
|
-
if file_path.is_file():
|
882
|
-
content = file_path.read_text()
|
883
|
-
self.logger.info(f"Loaded {filename} from package")
|
884
|
-
return content
|
885
|
-
self.logger.warning(f"File {filename} not found in package")
|
886
|
-
return None
|
887
|
-
except Exception as e:
|
888
|
-
self.logger.error(f"Failed to load {filename} from package: {e}")
|
889
|
-
return None
|
272
|
+
def _discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
|
273
|
+
"""Discover local JSON agent templates."""
|
274
|
+
return self.agent_loader.discover_local_json_templates()
|
890
275
|
|
891
|
-
def
|
892
|
-
"""
|
893
|
-
|
276
|
+
def _parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
|
277
|
+
"""Parse agent metadata with caching."""
|
278
|
+
cache_key = str(agent_file)
|
279
|
+
file_mtime = agent_file.stat().st_mtime
|
280
|
+
|
281
|
+
# Try cache first
|
282
|
+
cached_result = self._cache_manager.get_agent_metadata(cache_key)
|
283
|
+
if cached_result is not None:
|
284
|
+
cached_data, cached_mtime = cached_result
|
285
|
+
if cached_mtime == file_mtime:
|
286
|
+
self.logger.debug(f"Using cached metadata for {agent_file.name}")
|
287
|
+
return cached_data
|
288
|
+
|
289
|
+
# Cache miss - parse the file
|
290
|
+
agent_data = self.metadata_processor.parse_agent_metadata(agent_file)
|
291
|
+
|
292
|
+
# Add routing information if not present
|
293
|
+
if agent_data and "routing" not in agent_data:
|
294
|
+
template_data = self.template_processor.load_template(agent_file.stem)
|
295
|
+
if template_data:
|
296
|
+
routing = self.template_processor.extract_routing(template_data)
|
297
|
+
if routing:
|
298
|
+
agent_data["routing"] = routing
|
299
|
+
memory_routing = self.template_processor.extract_memory_routing(template_data)
|
300
|
+
if memory_routing:
|
301
|
+
agent_data["memory_routing"] = memory_routing
|
302
|
+
|
303
|
+
# Cache the result
|
304
|
+
if agent_data:
|
305
|
+
self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
|
894
306
|
|
895
|
-
|
896
|
-
version_match = re.search(r"<!-- FRAMEWORK_VERSION: (\d+) -->", content)
|
897
|
-
if version_match and "INSTRUCTIONS.md" in filename:
|
898
|
-
self.framework_version = version_match.group(1)
|
899
|
-
self.logger.info(f"Framework version: {self.framework_version}")
|
307
|
+
return agent_data
|
900
308
|
|
901
|
-
|
902
|
-
timestamp_match = re.search(r"<!-- LAST_MODIFIED: ([^>]+) -->", content)
|
903
|
-
if timestamp_match and "INSTRUCTIONS.md" in filename:
|
904
|
-
self.framework_last_modified = timestamp_match.group(1).strip()
|
905
|
-
self.logger.info(f"Last modified: {self.framework_last_modified}")
|
309
|
+
# === Framework Instructions Generation ===
|
906
310
|
|
907
311
|
def get_framework_instructions(self) -> str:
|
908
312
|
"""
|
@@ -911,1128 +315,203 @@ class FrameworkLoader:
|
|
911
315
|
Returns:
|
912
316
|
Complete framework instructions ready for injection
|
913
317
|
"""
|
914
|
-
#
|
915
|
-
|
916
|
-
from .log_manager import get_log_manager
|
917
|
-
|
918
|
-
log_manager = get_log_manager()
|
919
|
-
except ImportError:
|
920
|
-
log_manager = None
|
318
|
+
# Log the system prompt if needed
|
319
|
+
self._log_system_prompt()
|
921
320
|
|
922
321
|
# Generate the instructions
|
923
322
|
if self.framework_content["loaded"]:
|
924
|
-
|
925
|
-
instructions = self._format_full_framework()
|
323
|
+
return self._format_full_framework()
|
926
324
|
else:
|
927
|
-
|
928
|
-
instructions = self._format_minimal_framework()
|
929
|
-
|
930
|
-
# Log the system prompt if LogManager is available
|
931
|
-
if log_manager:
|
932
|
-
try:
|
933
|
-
import asyncio
|
934
|
-
import os
|
935
|
-
|
936
|
-
# Get or create event loop
|
937
|
-
try:
|
938
|
-
loop = asyncio.get_running_loop()
|
939
|
-
except RuntimeError:
|
940
|
-
loop = asyncio.new_event_loop()
|
941
|
-
asyncio.set_event_loop(loop)
|
942
|
-
|
943
|
-
# Prepare metadata
|
944
|
-
metadata = {
|
945
|
-
"framework_version": self.framework_version,
|
946
|
-
"framework_loaded": self.framework_content.get("loaded", False),
|
947
|
-
"session_id": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
|
948
|
-
"instructions_length": len(instructions),
|
949
|
-
}
|
950
|
-
|
951
|
-
# Log the prompt asynchronously
|
952
|
-
if loop.is_running():
|
953
|
-
asyncio.create_task(
|
954
|
-
log_manager.log_prompt("system_prompt", instructions, metadata)
|
955
|
-
)
|
956
|
-
else:
|
957
|
-
loop.run_until_complete(
|
958
|
-
log_manager.log_prompt("system_prompt", instructions, metadata)
|
959
|
-
)
|
960
|
-
|
961
|
-
self.logger.debug("System prompt logged to prompts directory")
|
962
|
-
except Exception as e:
|
963
|
-
self.logger.debug(f"Could not log system prompt: {e}")
|
964
|
-
|
965
|
-
return instructions
|
966
|
-
|
967
|
-
def _strip_metadata_comments(self, content: str) -> str:
|
968
|
-
"""Strip metadata HTML comments from content.
|
969
|
-
|
970
|
-
Removes comments like:
|
971
|
-
<!-- FRAMEWORK_VERSION: 0010 -->
|
972
|
-
<!-- LAST_MODIFIED: 2025-08-10T00:00:00Z -->
|
973
|
-
"""
|
974
|
-
import re
|
975
|
-
|
976
|
-
# Remove HTML comments that contain metadata
|
977
|
-
cleaned = re.sub(
|
978
|
-
r"<!--\s*(FRAMEWORK_VERSION|LAST_MODIFIED|WORKFLOW_VERSION|PROJECT_WORKFLOW_VERSION|CUSTOM_PROJECT_WORKFLOW)[^>]*-->\n?",
|
979
|
-
"",
|
980
|
-
content,
|
981
|
-
)
|
982
|
-
# Also remove any leading blank lines that might result
|
983
|
-
return cleaned.lstrip("\n")
|
325
|
+
return self._format_minimal_framework()
|
984
326
|
|
985
327
|
def _format_full_framework(self) -> str:
|
986
|
-
"""Format full framework instructions."""
|
987
|
-
|
988
|
-
# Initialize output style manager on first use (ensures content is loaded)
|
328
|
+
"""Format full framework instructions using modular components."""
|
329
|
+
# Initialize output style manager on first use
|
989
330
|
if self.output_style_manager is None:
|
990
331
|
self._initialize_output_style()
|
991
332
|
|
992
|
-
# Check if we need to inject output style
|
333
|
+
# Check if we need to inject output style
|
993
334
|
inject_output_style = False
|
335
|
+
output_style_content = None
|
994
336
|
if self.output_style_manager:
|
995
337
|
inject_output_style = self.output_style_manager.should_inject_content()
|
996
338
|
if inject_output_style:
|
997
|
-
self.
|
998
|
-
|
339
|
+
output_style_content = self.output_style_manager.get_injectable_content(
|
340
|
+
framework_loader=self
|
999
341
|
)
|
342
|
+
self.logger.info("Injecting output style content for Claude < 1.0.83")
|
1000
343
|
|
1001
|
-
#
|
1002
|
-
|
1003
|
-
|
1004
|
-
self.framework_content["framework_instructions"]
|
1005
|
-
)
|
344
|
+
# Generate dynamic sections
|
345
|
+
capabilities_section = self._generate_agent_capabilities_section()
|
346
|
+
context_section = self.context_generator.generate_temporal_user_context()
|
1006
347
|
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
instructions += f"\n\n## Custom PM Instructions ({level} level)\n\n"
|
1016
|
-
instructions += "**The following custom instructions override or extend the framework defaults:**\n\n"
|
1017
|
-
instructions += self._strip_metadata_comments(
|
1018
|
-
self.framework_content["custom_instructions"]
|
1019
|
-
)
|
1020
|
-
instructions += "\n"
|
1021
|
-
|
1022
|
-
# Add WORKFLOW.md after instructions
|
1023
|
-
if self.framework_content.get("workflow_instructions"):
|
1024
|
-
workflow_content = self._strip_metadata_comments(
|
1025
|
-
self.framework_content["workflow_instructions"]
|
1026
|
-
)
|
1027
|
-
level = self.framework_content.get(
|
1028
|
-
"workflow_instructions_level", "system"
|
1029
|
-
)
|
1030
|
-
if level != "system":
|
1031
|
-
instructions += f"\n\n## Workflow Instructions ({level} level)\n\n"
|
1032
|
-
instructions += "**The following workflow instructions override system defaults:**\n\n"
|
1033
|
-
instructions += f"{workflow_content}\n"
|
1034
|
-
|
1035
|
-
# Add MEMORY.md after workflow instructions
|
1036
|
-
if self.framework_content.get("memory_instructions"):
|
1037
|
-
memory_content = self._strip_metadata_comments(
|
1038
|
-
self.framework_content["memory_instructions"]
|
1039
|
-
)
|
1040
|
-
level = self.framework_content.get(
|
1041
|
-
"memory_instructions_level", "system"
|
1042
|
-
)
|
1043
|
-
if level != "system":
|
1044
|
-
instructions += f"\n\n## Memory Instructions ({level} level)\n\n"
|
1045
|
-
instructions += "**The following memory instructions override system defaults:**\n\n"
|
1046
|
-
instructions += f"{memory_content}\n"
|
1047
|
-
|
1048
|
-
# Add actual PM memories after memory instructions
|
1049
|
-
if self.framework_content.get("actual_memories"):
|
1050
|
-
instructions += "\n\n## Current PM Memories\n\n"
|
1051
|
-
instructions += "**The following are your accumulated memories and knowledge from this project:**\n\n"
|
1052
|
-
instructions += self.framework_content["actual_memories"]
|
1053
|
-
instructions += "\n"
|
1054
|
-
|
1055
|
-
# Add agent memories if available
|
1056
|
-
if self.framework_content.get("agent_memories"):
|
1057
|
-
agent_memories = self.framework_content["agent_memories"]
|
1058
|
-
if agent_memories:
|
1059
|
-
instructions += "\n\n## Agent Memories\n\n"
|
1060
|
-
instructions += "**The following are accumulated memories from specialized agents:**\n\n"
|
1061
|
-
|
1062
|
-
for agent_name in sorted(agent_memories.keys()):
|
1063
|
-
memory_content = agent_memories[agent_name]
|
1064
|
-
if memory_content:
|
1065
|
-
instructions += f"### {agent_name.replace('_', ' ').title()} Agent Memory\n\n"
|
1066
|
-
instructions += memory_content
|
1067
|
-
instructions += "\n\n"
|
1068
|
-
|
1069
|
-
# Add dynamic agent capabilities section
|
1070
|
-
instructions += self._generate_agent_capabilities_section()
|
1071
|
-
|
1072
|
-
# Add enhanced temporal and user context for better awareness
|
1073
|
-
instructions += self._generate_temporal_user_context()
|
1074
|
-
|
1075
|
-
# Add BASE_PM.md framework requirements AFTER INSTRUCTIONS.md
|
1076
|
-
if self.framework_content.get("base_pm_instructions"):
|
1077
|
-
base_pm = self._strip_metadata_comments(
|
1078
|
-
self.framework_content["base_pm_instructions"]
|
1079
|
-
)
|
1080
|
-
instructions += f"\n\n{base_pm}"
|
348
|
+
# Format the complete framework
|
349
|
+
return self.content_formatter.format_full_framework(
|
350
|
+
self.framework_content,
|
351
|
+
capabilities_section,
|
352
|
+
context_section,
|
353
|
+
inject_output_style,
|
354
|
+
output_style_content,
|
355
|
+
)
|
1081
356
|
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
framework_loader=self
|
1086
|
-
)
|
1087
|
-
if output_style_content:
|
1088
|
-
instructions += "\n\n## Output Style Configuration\n"
|
1089
|
-
instructions += "**Note: The following output style is injected for Claude < 1.0.83**\n\n"
|
1090
|
-
instructions += output_style_content
|
1091
|
-
instructions += "\n"
|
1092
|
-
|
1093
|
-
# Clean up any trailing whitespace
|
1094
|
-
return instructions.rstrip() + "\n"
|
1095
|
-
|
1096
|
-
# Otherwise fall back to generating framework
|
1097
|
-
instructions = """# Claude MPM Framework Instructions
|
1098
|
-
|
1099
|
-
You are operating within the Claude Multi-Agent Project Manager (MPM) framework.
|
1100
|
-
|
1101
|
-
## Core Role
|
1102
|
-
You are a multi-agent orchestrator. Your primary responsibilities are:
|
1103
|
-
- Delegate all implementation work to specialized agents via Task Tool
|
1104
|
-
- Coordinate multi-agent workflows and cross-agent collaboration
|
1105
|
-
- Extract and track TODO/BUG/FEATURE items for ticket creation
|
1106
|
-
- Maintain project visibility and strategic oversight
|
1107
|
-
- NEVER perform direct implementation work yourself
|
1108
|
-
|
1109
|
-
"""
|
1110
|
-
|
1111
|
-
# Note: We don't add working directory CLAUDE.md here since Claude Code
|
1112
|
-
# already picks it up automatically. This prevents duplication.
|
1113
|
-
|
1114
|
-
# Add agent definitions
|
1115
|
-
if self.framework_content["agents"]:
|
1116
|
-
instructions += "## Available Agents\n\n"
|
1117
|
-
instructions += "You have the following specialized agents available for delegation:\n\n"
|
1118
|
-
|
1119
|
-
# List agents with brief descriptions and correct IDs
|
1120
|
-
agent_list = []
|
1121
|
-
for agent_name in sorted(self.framework_content["agents"].keys()):
|
1122
|
-
# Use the actual agent_name as the ID (it's the filename stem)
|
1123
|
-
agent_id = agent_name
|
1124
|
-
clean_name = agent_name.replace("-", " ").replace("_", " ").title()
|
1125
|
-
if (
|
1126
|
-
"engineer" in agent_name.lower()
|
1127
|
-
and "data" not in agent_name.lower()
|
1128
|
-
):
|
1129
|
-
agent_list.append(
|
1130
|
-
f"- **Engineer Agent** (`{agent_id}`): Code implementation and development"
|
1131
|
-
)
|
1132
|
-
elif "qa" in agent_name.lower():
|
1133
|
-
agent_list.append(
|
1134
|
-
f"- **QA Agent** (`{agent_id}`): Testing and quality assurance"
|
1135
|
-
)
|
1136
|
-
elif "documentation" in agent_name.lower():
|
1137
|
-
agent_list.append(
|
1138
|
-
f"- **Documentation Agent** (`{agent_id}`): Documentation creation and maintenance"
|
1139
|
-
)
|
1140
|
-
elif "research" in agent_name.lower():
|
1141
|
-
agent_list.append(
|
1142
|
-
f"- **Research Agent** (`{agent_id}`): Investigation and analysis"
|
1143
|
-
)
|
1144
|
-
elif "security" in agent_name.lower():
|
1145
|
-
agent_list.append(
|
1146
|
-
f"- **Security Agent** (`{agent_id}`): Security analysis and protection"
|
1147
|
-
)
|
1148
|
-
elif "version" in agent_name.lower():
|
1149
|
-
agent_list.append(
|
1150
|
-
f"- **Version Control Agent** (`{agent_id}`): Git operations and version management"
|
1151
|
-
)
|
1152
|
-
elif "ops" in agent_name.lower():
|
1153
|
-
agent_list.append(
|
1154
|
-
f"- **Ops Agent** (`{agent_id}`): Deployment and operations"
|
1155
|
-
)
|
1156
|
-
elif "data" in agent_name.lower():
|
1157
|
-
agent_list.append(
|
1158
|
-
f"- **Data Engineer Agent** (`{agent_id}`): Data management and AI API integration"
|
1159
|
-
)
|
1160
|
-
else:
|
1161
|
-
agent_list.append(
|
1162
|
-
f"- **{clean_name}** (`{agent_id}`): Available for specialized tasks"
|
1163
|
-
)
|
1164
|
-
|
1165
|
-
instructions += "\n".join(agent_list) + "\n\n"
|
1166
|
-
|
1167
|
-
# Add full agent details
|
1168
|
-
instructions += "### Agent Details\n\n"
|
1169
|
-
for agent_name, agent_content in sorted(
|
1170
|
-
self.framework_content["agents"].items()
|
1171
|
-
):
|
1172
|
-
instructions += f"#### {agent_name.replace('-', ' ').title()}\n"
|
1173
|
-
instructions += agent_content + "\n\n"
|
1174
|
-
|
1175
|
-
# Add orchestration principles
|
1176
|
-
instructions += """
|
1177
|
-
## Orchestration Principles
|
1178
|
-
1. **Always Delegate**: Never perform direct work - use Task Tool for all implementation
|
1179
|
-
2. **Comprehensive Context**: Provide rich, filtered context to each agent
|
1180
|
-
3. **Track Everything**: Extract all TODO/BUG/FEATURE items systematically
|
1181
|
-
4. **Cross-Agent Coordination**: Orchestrate workflows spanning multiple agents
|
1182
|
-
5. **Results Integration**: Actively receive and integrate agent results
|
1183
|
-
|
1184
|
-
## Task Tool Format
|
1185
|
-
```
|
1186
|
-
**[Agent Name]**: [Clear task description with deliverables]
|
1187
|
-
|
1188
|
-
TEMPORAL CONTEXT: Today is [date]. Apply date awareness to [specific considerations].
|
1189
|
-
|
1190
|
-
**Task**: [Detailed task breakdown]
|
1191
|
-
1. [Specific action item 1]
|
1192
|
-
2. [Specific action item 2]
|
1193
|
-
3. [Specific action item 3]
|
1194
|
-
|
1195
|
-
**Context**: [Comprehensive filtered context for this agent]
|
1196
|
-
**Authority**: [Agent's decision-making scope]
|
1197
|
-
**Expected Results**: [Specific deliverables needed]
|
1198
|
-
**Integration**: [How results integrate with other work]
|
1199
|
-
```
|
1200
|
-
|
1201
|
-
## Ticket Extraction Patterns
|
1202
|
-
Extract tickets from these patterns:
|
1203
|
-
- TODO: [description] → TODO ticket
|
1204
|
-
- BUG: [description] → BUG ticket
|
1205
|
-
- FEATURE: [description] → FEATURE ticket
|
1206
|
-
- ISSUE: [description] → ISSUE ticket
|
1207
|
-
- FIXME: [description] → BUG ticket
|
1208
|
-
|
1209
|
-
---
|
1210
|
-
"""
|
1211
|
-
|
1212
|
-
return instructions
|
357
|
+
def _format_minimal_framework(self) -> str:
|
358
|
+
"""Format minimal framework instructions."""
|
359
|
+
return self.content_formatter.format_minimal_framework(self.framework_content)
|
1213
360
|
|
1214
361
|
def _generate_agent_capabilities_section(self) -> str:
|
1215
|
-
"""Generate
|
1216
|
-
|
1217
|
-
|
1218
|
-
Now includes support for local JSON templates with proper priority:
|
1219
|
-
1. Project local agents (.claude-mpm/agents/*.json) - highest priority
|
1220
|
-
2. Deployed project agents (.claude/agents/*.md)
|
1221
|
-
3. User local agents (~/.claude-mpm/agents/*.json)
|
1222
|
-
4. Deployed user agents (~/.claude/agents/*.md)
|
1223
|
-
5. System agents - lowest priority
|
1224
|
-
"""
|
1225
|
-
|
1226
|
-
# Try to get from cache first
|
362
|
+
"""Generate agent capabilities section with caching."""
|
363
|
+
# Try cache first
|
1227
364
|
cached_capabilities = self._cache_manager.get_capabilities()
|
1228
365
|
if cached_capabilities is not None:
|
1229
366
|
return cached_capabilities
|
1230
367
|
|
1231
|
-
|
1232
|
-
current_time = time.time()
|
1233
|
-
|
1234
|
-
# Cache miss or expired - generate capabilities
|
1235
|
-
self.logger.debug("Generating agent capabilities (cache miss or expired)")
|
368
|
+
self.logger.debug("Generating agent capabilities (cache miss)")
|
1236
369
|
|
1237
370
|
try:
|
1238
|
-
|
1239
|
-
|
1240
|
-
# First check for local JSON templates (highest priority)
|
371
|
+
# Discover local JSON templates
|
1241
372
|
local_agents = self._discover_local_json_templates()
|
1242
373
|
|
1243
|
-
#
|
1244
|
-
|
1245
|
-
# Priority order: local templates > project > user home > fallback
|
374
|
+
# Get deployed agents from .claude/agents/
|
375
|
+
deployed_agents = []
|
1246
376
|
agents_dirs = [
|
1247
|
-
Path.cwd() / ".claude" / "agents",
|
1248
|
-
Path.home() / ".claude" / "agents",
|
377
|
+
Path.cwd() / ".claude" / "agents",
|
378
|
+
Path.home() / ".claude" / "agents",
|
1249
379
|
]
|
1250
380
|
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
all_agents[agent_id] = (agent_data, -1) # Priority -1 for local agents
|
1259
|
-
|
1260
|
-
for priority, potential_dir in enumerate(agents_dirs):
|
1261
|
-
if potential_dir.exists() and any(potential_dir.glob("*.md")):
|
1262
|
-
self.logger.debug(f"Found agents directory at: {potential_dir}")
|
1263
|
-
|
1264
|
-
# Collect agents from this directory
|
1265
|
-
for agent_file in potential_dir.glob("*.md"):
|
1266
|
-
if agent_file.name.startswith("."):
|
1267
|
-
continue
|
1268
|
-
|
1269
|
-
# Parse agent metadata (with caching)
|
1270
|
-
agent_data = self._parse_agent_metadata(agent_file)
|
1271
|
-
if agent_data:
|
1272
|
-
agent_id = agent_data["id"]
|
1273
|
-
# Only add if not already present (project has priority 0, user has priority 1)
|
1274
|
-
# Lower priority number wins (project > user)
|
1275
|
-
if (
|
1276
|
-
agent_id not in all_agents
|
1277
|
-
or priority < all_agents[agent_id][1]
|
1278
|
-
):
|
1279
|
-
all_agents[agent_id] = (agent_data, priority)
|
1280
|
-
self.logger.debug(
|
1281
|
-
f"Added/Updated agent {agent_id} from {potential_dir} (priority {priority})"
|
1282
|
-
)
|
1283
|
-
|
1284
|
-
if not all_agents:
|
1285
|
-
self.logger.warning(f"No agents found in any location: {agents_dirs}")
|
1286
|
-
result = self._get_fallback_capabilities()
|
1287
|
-
# Cache the fallback result too
|
1288
|
-
self._cache_manager.set_capabilities(result)
|
1289
|
-
return result
|
1290
|
-
|
1291
|
-
# Log agent collection summary
|
1292
|
-
project_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 0]
|
1293
|
-
user_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 1]
|
1294
|
-
|
1295
|
-
# Include local agents in logging
|
1296
|
-
local_json_agents = [
|
1297
|
-
aid for aid, (_, pri) in all_agents.items() if pri == -1
|
1298
|
-
]
|
1299
|
-
if local_json_agents:
|
1300
|
-
self.logger.info(
|
1301
|
-
f"Loaded {len(local_json_agents)} local JSON agents: {', '.join(sorted(local_json_agents))}"
|
1302
|
-
)
|
1303
|
-
if project_agents:
|
1304
|
-
self.logger.info(
|
1305
|
-
f"Loaded {len(project_agents)} project agents: {', '.join(sorted(project_agents))}"
|
1306
|
-
)
|
1307
|
-
if user_agents:
|
1308
|
-
self.logger.info(
|
1309
|
-
f"Loaded {len(user_agents)} user agents: {', '.join(sorted(user_agents))}"
|
1310
|
-
)
|
1311
|
-
|
1312
|
-
# Build capabilities section
|
1313
|
-
section = "\n\n## Available Agent Capabilities\n\n"
|
1314
|
-
|
1315
|
-
# Extract just the agent data (drop priority info) and sort
|
1316
|
-
deployed_agents = [agent_data for agent_data, _ in all_agents.values()]
|
1317
|
-
|
1318
|
-
if not deployed_agents:
|
1319
|
-
result = self._get_fallback_capabilities()
|
1320
|
-
# Cache the fallback result
|
1321
|
-
self._cache_manager.set_capabilities(result)
|
1322
|
-
return result
|
1323
|
-
|
1324
|
-
# Sort agents alphabetically by ID
|
1325
|
-
deployed_agents.sort(key=lambda x: x["id"])
|
1326
|
-
|
1327
|
-
# Display all agents with their rich descriptions
|
1328
|
-
for agent in deployed_agents:
|
1329
|
-
# Clean up display name - handle common acronyms
|
1330
|
-
display_name = agent["display_name"]
|
1331
|
-
display_name = (
|
1332
|
-
display_name.replace("Qa ", "QA ")
|
1333
|
-
.replace("Ui ", "UI ")
|
1334
|
-
.replace("Api ", "API ")
|
1335
|
-
)
|
1336
|
-
if display_name.lower() == "qa agent":
|
1337
|
-
display_name = "QA Agent"
|
381
|
+
for agents_dir in agents_dirs:
|
382
|
+
if agents_dir.exists():
|
383
|
+
for agent_file in agents_dir.glob("*.md"):
|
384
|
+
if not agent_file.name.startswith("."):
|
385
|
+
agent_data = self._parse_agent_metadata(agent_file)
|
386
|
+
if agent_data:
|
387
|
+
deployed_agents.append(agent_data)
|
1338
388
|
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
section += f"\n### {display_name} (`{agent['id']}`) {tier_label}\n"
|
1343
|
-
else:
|
1344
|
-
section += f"\n### {display_name} (`{agent['id']}`)\n"
|
1345
|
-
section += f"{agent['description']}\n"
|
1346
|
-
|
1347
|
-
# Add routing information if available
|
1348
|
-
if agent.get("routing"):
|
1349
|
-
routing = agent["routing"]
|
1350
|
-
|
1351
|
-
# Format routing hints for PM usage
|
1352
|
-
routing_hints = []
|
1353
|
-
|
1354
|
-
if routing.get("keywords"):
|
1355
|
-
# Show first 5 keywords for brevity
|
1356
|
-
keywords = routing["keywords"][:5]
|
1357
|
-
routing_hints.append(f"Keywords: {', '.join(keywords)}")
|
1358
|
-
|
1359
|
-
if routing.get("paths"):
|
1360
|
-
# Show first 3 paths for brevity
|
1361
|
-
paths = routing["paths"][:3]
|
1362
|
-
routing_hints.append(f"Paths: {', '.join(paths)}")
|
1363
|
-
|
1364
|
-
if routing.get("priority"):
|
1365
|
-
routing_hints.append(f"Priority: {routing['priority']}")
|
1366
|
-
|
1367
|
-
if routing_hints:
|
1368
|
-
section += f"- **Routing**: {' | '.join(routing_hints)}\n"
|
1369
|
-
|
1370
|
-
# Add when_to_use if present
|
1371
|
-
if routing.get("when_to_use"):
|
1372
|
-
section += f"- **When to use**: {routing['when_to_use']}\n"
|
1373
|
-
|
1374
|
-
# Add any additional metadata if present
|
1375
|
-
if agent.get("authority"):
|
1376
|
-
section += f"- **Authority**: {agent['authority']}\n"
|
1377
|
-
if agent.get("primary_function"):
|
1378
|
-
section += f"- **Primary Function**: {agent['primary_function']}\n"
|
1379
|
-
if agent.get("handoff_to"):
|
1380
|
-
section += f"- **Handoff To**: {agent['handoff_to']}\n"
|
1381
|
-
if agent.get("tools") and agent["tools"] != "standard":
|
1382
|
-
section += f"- **Tools**: {agent['tools']}\n"
|
1383
|
-
if agent.get("model") and agent["model"] != "opus":
|
1384
|
-
section += f"- **Model**: {agent['model']}\n"
|
1385
|
-
|
1386
|
-
# Add memory routing information if available
|
1387
|
-
if agent.get("memory_routing"):
|
1388
|
-
memory_routing = agent["memory_routing"]
|
1389
|
-
if memory_routing.get("description"):
|
1390
|
-
section += (
|
1391
|
-
f"- **Memory Routing**: {memory_routing['description']}\n"
|
1392
|
-
)
|
1393
|
-
|
1394
|
-
# Add simple Context-Aware Agent Selection
|
1395
|
-
section += "\n## Context-Aware Agent Selection\n\n"
|
1396
|
-
section += (
|
1397
|
-
"Select agents based on their descriptions above. Key principles:\n"
|
1398
|
-
)
|
1399
|
-
section += "- **PM questions** → Answer directly (only exception)\n"
|
1400
|
-
section += "- Match task requirements to agent descriptions and authority\n"
|
1401
|
-
section += "- Consider agent handoff recommendations\n"
|
1402
|
-
section += (
|
1403
|
-
"- Use the agent ID in parentheses when delegating via Task tool\n"
|
389
|
+
# Generate capabilities section
|
390
|
+
section = self.capability_generator.generate_capabilities_section(
|
391
|
+
deployed_agents, local_agents
|
1404
392
|
)
|
1405
393
|
|
1406
|
-
#
|
1407
|
-
section += f"\n**Total Available Agents**: {len(deployed_agents)}\n"
|
1408
|
-
|
1409
|
-
# Cache the generated capabilities
|
394
|
+
# Cache the result
|
1410
395
|
self._cache_manager.set_capabilities(section)
|
1411
|
-
self.logger.debug(
|
1412
|
-
f"Cached agent capabilities section ({len(section)} chars)"
|
1413
|
-
)
|
396
|
+
self.logger.debug(f"Cached agent capabilities ({len(section)} chars)")
|
1414
397
|
|
1415
398
|
return section
|
1416
399
|
|
1417
400
|
except Exception as e:
|
1418
|
-
self.logger.warning(f"Could not generate
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
self._agent_capabilities_cache_time = current_time
|
1423
|
-
return result
|
401
|
+
self.logger.warning(f"Could not generate agent capabilities: {e}")
|
402
|
+
fallback = self.content_formatter.get_fallback_capabilities()
|
403
|
+
self._cache_manager.set_capabilities(fallback)
|
404
|
+
return fallback
|
1424
405
|
|
1425
|
-
|
1426
|
-
"""Generate enhanced temporal and user context for better PM awareness.
|
1427
|
-
|
1428
|
-
Returns:
|
1429
|
-
str: Formatted context string with datetime, user, and system information
|
1430
|
-
"""
|
1431
|
-
context_lines = ["\n\n## Temporal & User Context\n"]
|
406
|
+
# === Output Style Management ===
|
1432
407
|
|
408
|
+
def _initialize_output_style(self) -> None:
|
409
|
+
"""Initialize output style management."""
|
1433
410
|
try:
|
1434
|
-
|
1435
|
-
now = datetime.now(timezone.utc)
|
1436
|
-
|
1437
|
-
# Try to get timezone info - fallback to UTC offset if timezone name not available
|
1438
|
-
try:
|
1439
|
-
import time as time_module
|
1440
|
-
|
1441
|
-
if hasattr(time_module, "tzname"):
|
1442
|
-
tz_name = time_module.tzname[time_module.daylight]
|
1443
|
-
tz_offset = time_module.strftime("%z")
|
1444
|
-
if tz_offset:
|
1445
|
-
# Format UTC offset properly (e.g., -0800 to -08:00)
|
1446
|
-
tz_offset = (
|
1447
|
-
f"{tz_offset[:3]}:{tz_offset[3:]}"
|
1448
|
-
if len(tz_offset) >= 4
|
1449
|
-
else tz_offset
|
1450
|
-
)
|
1451
|
-
tz_info = f"{tz_name} (UTC{tz_offset})"
|
1452
|
-
else:
|
1453
|
-
tz_info = tz_name
|
1454
|
-
else:
|
1455
|
-
tz_info = "Local Time"
|
1456
|
-
except Exception:
|
1457
|
-
tz_info = "Local Time"
|
1458
|
-
|
1459
|
-
# Format datetime components
|
1460
|
-
date_str = now.strftime("%Y-%m-%d")
|
1461
|
-
time_str = now.strftime("%H:%M:%S")
|
1462
|
-
day_name = now.strftime("%A")
|
411
|
+
from claude_mpm.core.output_style_manager import OutputStyleManager
|
1463
412
|
|
1464
|
-
|
1465
|
-
|
1466
|
-
)
|
1467
|
-
context_lines.append(f"**Day**: {day_name}\n")
|
413
|
+
self.output_style_manager = OutputStyleManager()
|
414
|
+
self._log_output_style_status()
|
1468
415
|
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
context_lines.append(
|
1473
|
-
f"**Today's Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}\n"
|
416
|
+
# Extract and save output style content
|
417
|
+
output_style_content = self.output_style_manager.extract_output_style_content(
|
418
|
+
framework_loader=self
|
1474
419
|
)
|
420
|
+
self.output_style_manager.save_output_style(output_style_content)
|
1475
421
|
|
1476
|
-
|
1477
|
-
|
1478
|
-
username = None
|
1479
|
-
|
1480
|
-
# Try multiple methods to get username
|
1481
|
-
methods = [
|
1482
|
-
lambda: os.environ.get("USER"),
|
1483
|
-
lambda: os.environ.get("USERNAME"), # Windows fallback
|
1484
|
-
lambda: getpass.getuser(),
|
1485
|
-
]
|
1486
|
-
|
1487
|
-
for method in methods:
|
1488
|
-
try:
|
1489
|
-
username = method()
|
1490
|
-
if username:
|
1491
|
-
break
|
1492
|
-
except Exception:
|
1493
|
-
continue
|
1494
|
-
|
1495
|
-
if username:
|
1496
|
-
context_lines.append(f"**User**: {username}\n")
|
1497
|
-
|
1498
|
-
# Add home directory if available
|
1499
|
-
try:
|
1500
|
-
home_dir = os.path.expanduser("~")
|
1501
|
-
if home_dir and home_dir != "~":
|
1502
|
-
context_lines.append(f"**Home Directory**: {home_dir}\n")
|
1503
|
-
except Exception:
|
1504
|
-
pass
|
1505
|
-
|
1506
|
-
except Exception as e:
|
1507
|
-
# User detection is optional, don't fail
|
1508
|
-
self.logger.debug(f"Could not detect user information: {e}")
|
422
|
+
# Deploy to Claude Code if supported
|
423
|
+
deployed = self.output_style_manager.deploy_output_style(output_style_content)
|
1509
424
|
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
# Enhance system name for common platforms
|
1515
|
-
system_names = {
|
1516
|
-
"Darwin": "Darwin (macOS)",
|
1517
|
-
"Linux": "Linux",
|
1518
|
-
"Windows": "Windows",
|
1519
|
-
}
|
1520
|
-
system_display = system_names.get(system_info, system_info)
|
1521
|
-
context_lines.append(f"**System**: {system_display}\n")
|
1522
|
-
|
1523
|
-
# Add platform version if available
|
1524
|
-
try:
|
1525
|
-
platform_version = platform.release()
|
1526
|
-
if platform_version:
|
1527
|
-
context_lines.append(
|
1528
|
-
f"**System Version**: {platform_version}\n"
|
1529
|
-
)
|
1530
|
-
except Exception:
|
1531
|
-
pass
|
425
|
+
if deployed:
|
426
|
+
self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
|
427
|
+
else:
|
428
|
+
self.logger.info("📝 Output style will be injected into instructions")
|
1532
429
|
|
1533
430
|
except Exception as e:
|
1534
|
-
|
1535
|
-
self.logger.debug(f"Could not detect system information: {e}")
|
1536
|
-
|
1537
|
-
try:
|
1538
|
-
# Add current working directory
|
1539
|
-
cwd = os.getcwd()
|
1540
|
-
if cwd:
|
1541
|
-
context_lines.append(f"**Working Directory**: {cwd}\n")
|
1542
|
-
except Exception:
|
1543
|
-
pass
|
1544
|
-
|
1545
|
-
try:
|
1546
|
-
# Add locale information if available
|
1547
|
-
current_locale = locale.getlocale()
|
1548
|
-
if current_locale and current_locale[0]:
|
1549
|
-
context_lines.append(f"**Locale**: {current_locale[0]}\n")
|
1550
|
-
except Exception:
|
1551
|
-
# Locale is optional
|
1552
|
-
pass
|
1553
|
-
|
1554
|
-
# Add instruction for applying context
|
1555
|
-
context_lines.append(
|
1556
|
-
"\nApply temporal and user awareness to all tasks, "
|
1557
|
-
"decisions, and interactions.\n"
|
1558
|
-
)
|
1559
|
-
context_lines.append(
|
1560
|
-
"Use this context for personalized responses and "
|
1561
|
-
"time-sensitive operations.\n"
|
1562
|
-
)
|
1563
|
-
|
1564
|
-
return "".join(context_lines)
|
1565
|
-
|
1566
|
-
def _parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
|
1567
|
-
"""Parse agent metadata from deployed agent file.
|
1568
|
-
Uses caching based on file path and modification time.
|
1569
|
-
|
1570
|
-
Returns:
|
1571
|
-
Dictionary with agent metadata directly from YAML frontmatter.
|
1572
|
-
"""
|
1573
|
-
try:
|
1574
|
-
# Check cache based on file path and modification time
|
1575
|
-
cache_key = str(agent_file)
|
1576
|
-
file_mtime = agent_file.stat().st_mtime
|
1577
|
-
time.time()
|
1578
|
-
|
1579
|
-
# Try to get from cache first
|
1580
|
-
cached_result = self._cache_manager.get_agent_metadata(cache_key)
|
1581
|
-
if cached_result is not None:
|
1582
|
-
cached_data, cached_mtime = cached_result
|
1583
|
-
# Use cache if file hasn't been modified and cache isn't too old
|
1584
|
-
if cached_mtime == file_mtime:
|
1585
|
-
self.logger.debug(f"Using cached metadata for {agent_file.name}")
|
1586
|
-
return cached_data
|
1587
|
-
|
1588
|
-
# Cache miss or expired - parse the file
|
1589
|
-
self.logger.debug(
|
1590
|
-
f"Parsing metadata for {agent_file.name} (cache miss or expired)"
|
1591
|
-
)
|
1592
|
-
|
1593
|
-
import yaml
|
1594
|
-
|
1595
|
-
with open(agent_file) as f:
|
1596
|
-
content = f.read()
|
1597
|
-
|
1598
|
-
# Default values
|
1599
|
-
agent_data = {
|
1600
|
-
"id": agent_file.stem,
|
1601
|
-
"display_name": agent_file.stem.replace("_", " ")
|
1602
|
-
.replace("-", " ")
|
1603
|
-
.title(),
|
1604
|
-
"description": "Specialized agent",
|
1605
|
-
}
|
1606
|
-
|
1607
|
-
# Extract YAML frontmatter if present
|
1608
|
-
if content.startswith("---"):
|
1609
|
-
end_marker = content.find("---", 3)
|
1610
|
-
if end_marker > 0:
|
1611
|
-
frontmatter = content[3:end_marker]
|
1612
|
-
metadata = yaml.safe_load(frontmatter)
|
1613
|
-
if metadata:
|
1614
|
-
# Use name as ID for Task tool
|
1615
|
-
agent_data["id"] = metadata.get("name", agent_data["id"])
|
1616
|
-
agent_data["display_name"] = (
|
1617
|
-
metadata.get("name", agent_data["display_name"])
|
1618
|
-
.replace("-", " ")
|
1619
|
-
.title()
|
1620
|
-
)
|
1621
|
-
|
1622
|
-
# Copy all metadata fields directly
|
1623
|
-
for key, value in metadata.items():
|
1624
|
-
if key not in ["name"]: # Skip already processed fields
|
1625
|
-
agent_data[key] = value
|
1626
|
-
|
1627
|
-
# IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
|
1628
|
-
# Tools must remain as comma-separated without spaces: "Read,Write,Edit"
|
1629
|
-
|
1630
|
-
# Try to load routing metadata from JSON template if not in YAML frontmatter
|
1631
|
-
if "routing" not in agent_data:
|
1632
|
-
routing_data = self._load_routing_from_template(agent_file.stem)
|
1633
|
-
if routing_data:
|
1634
|
-
agent_data["routing"] = routing_data
|
1635
|
-
|
1636
|
-
# Try to load memory routing metadata from JSON template if not in YAML frontmatter
|
1637
|
-
if "memory_routing" not in agent_data:
|
1638
|
-
memory_routing_data = self._load_memory_routing_from_template(
|
1639
|
-
agent_file.stem
|
1640
|
-
)
|
1641
|
-
if memory_routing_data:
|
1642
|
-
agent_data["memory_routing"] = memory_routing_data
|
1643
|
-
|
1644
|
-
# Cache the parsed metadata
|
1645
|
-
self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
|
431
|
+
self.logger.warning(f"❌ Failed to initialize output style manager: {e}")
|
1646
432
|
|
1647
|
-
|
433
|
+
def _log_output_style_status(self) -> None:
|
434
|
+
"""Log output style status information."""
|
435
|
+
if not self.output_style_manager:
|
436
|
+
return
|
1648
437
|
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
438
|
+
claude_version = self.output_style_manager.claude_version
|
439
|
+
if claude_version:
|
440
|
+
self.logger.info(f"Claude Code version detected: {claude_version}")
|
1652
441
|
|
1653
|
-
|
1654
|
-
|
1655
|
-
|
1656
|
-
|
442
|
+
if self.output_style_manager.supports_output_styles():
|
443
|
+
self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
|
444
|
+
output_style_path = self.output_style_manager.output_style_path
|
445
|
+
if output_style_path.exists():
|
446
|
+
self.logger.info(f"📁 Output style file exists: {output_style_path}")
|
447
|
+
else:
|
448
|
+
self.logger.info(f"📝 Output style will be created at: {output_style_path}")
|
449
|
+
else:
|
450
|
+
self.logger.info(f"⚠️ Claude Code {claude_version} does not support output styles")
|
451
|
+
self.logger.info("📝 Output style will be injected into framework instructions")
|
452
|
+
else:
|
453
|
+
self.logger.info("⚠️ Claude Code not detected or version unknown")
|
454
|
+
self.logger.info("📝 Output style will be injected as fallback")
|
1657
455
|
|
1658
|
-
|
1659
|
-
agent_name: Name of the agent (stem of the file)
|
456
|
+
# === Logging Methods ===
|
1660
457
|
|
1661
|
-
|
1662
|
-
|
1663
|
-
"""
|
458
|
+
def _log_system_prompt(self) -> None:
|
459
|
+
"""Log the system prompt if LogManager is available."""
|
1664
460
|
try:
|
1665
|
-
import
|
1666
|
-
|
1667
|
-
# Check if we have a framework path
|
1668
|
-
if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
|
1669
|
-
# For packaged installations, try to load from package resources
|
1670
|
-
if files:
|
1671
|
-
try:
|
1672
|
-
templates_package = files("claude_mpm.agents.templates")
|
1673
|
-
template_file = templates_package / f"{agent_name}.json"
|
1674
|
-
|
1675
|
-
if template_file.is_file():
|
1676
|
-
template_content = template_file.read_text()
|
1677
|
-
template_data = json.loads(template_content)
|
1678
|
-
return template_data.get("memory_routing")
|
1679
|
-
except Exception as e:
|
1680
|
-
self.logger.debug(
|
1681
|
-
f"Could not load memory routing from packaged template for {agent_name}: {e}"
|
1682
|
-
)
|
1683
|
-
return None
|
1684
|
-
|
1685
|
-
# For development mode, load from filesystem
|
1686
|
-
templates_dir = (
|
1687
|
-
self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
|
1688
|
-
)
|
1689
|
-
template_file = templates_dir / f"{agent_name}.json"
|
1690
|
-
|
1691
|
-
if template_file.exists():
|
1692
|
-
with open(template_file) as f:
|
1693
|
-
template_data = json.load(f)
|
1694
|
-
return template_data.get("memory_routing")
|
1695
|
-
|
1696
|
-
# Also check for variations in naming (underscore vs dash)
|
1697
|
-
# Handle common naming variations between deployed .md files and .json templates
|
1698
|
-
# Remove duplicates by using a set
|
1699
|
-
alternative_names = list(
|
1700
|
-
{
|
1701
|
-
agent_name.replace("-", "_"), # api-qa -> api_qa
|
1702
|
-
agent_name.replace("_", "-"), # api_qa -> api-qa
|
1703
|
-
agent_name.replace("-", ""), # api-qa -> apiqa
|
1704
|
-
agent_name.replace("_", ""), # api_qa -> apiqa
|
1705
|
-
agent_name.replace("-agent", ""), # research-agent -> research
|
1706
|
-
agent_name.replace("_agent", ""), # research_agent -> research
|
1707
|
-
agent_name + "_agent", # research -> research_agent
|
1708
|
-
agent_name + "-agent", # research -> research-agent
|
1709
|
-
}
|
1710
|
-
)
|
1711
|
-
|
1712
|
-
for alt_name in alternative_names:
|
1713
|
-
if alt_name != agent_name: # Skip the original name we already tried
|
1714
|
-
alt_file = templates_dir / f"{alt_name}.json"
|
1715
|
-
if alt_file.exists():
|
1716
|
-
with open(alt_file) as f:
|
1717
|
-
template_data = json.load(f)
|
1718
|
-
return template_data.get("memory_routing")
|
1719
|
-
|
1720
|
-
return None
|
1721
|
-
|
1722
|
-
except Exception as e:
|
1723
|
-
self.logger.debug(
|
1724
|
-
f"Could not load memory routing from template for {agent_name}: {e}"
|
1725
|
-
)
|
1726
|
-
return None
|
1727
|
-
|
1728
|
-
def _discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
|
1729
|
-
"""Discover local JSON agent templates from .claude-mpm/agents/ directories.
|
1730
|
-
|
1731
|
-
Returns:
|
1732
|
-
Dictionary mapping agent IDs to agent metadata
|
1733
|
-
"""
|
1734
|
-
import json
|
1735
|
-
from pathlib import Path
|
1736
|
-
|
1737
|
-
local_agents = {}
|
1738
|
-
|
1739
|
-
# Check for local JSON templates in priority order
|
1740
|
-
template_dirs = [
|
1741
|
-
Path.cwd()
|
1742
|
-
/ ".claude-mpm"
|
1743
|
-
/ "agents", # Project local agents (highest priority)
|
1744
|
-
Path.home() / ".claude-mpm" / "agents", # User local agents
|
1745
|
-
]
|
1746
|
-
|
1747
|
-
for priority, template_dir in enumerate(template_dirs):
|
1748
|
-
if not template_dir.exists():
|
1749
|
-
continue
|
1750
|
-
|
1751
|
-
for json_file in template_dir.glob("*.json"):
|
1752
|
-
try:
|
1753
|
-
with open(json_file) as f:
|
1754
|
-
template_data = json.load(f)
|
1755
|
-
|
1756
|
-
# Extract agent metadata
|
1757
|
-
agent_id = template_data.get("agent_id", json_file.stem)
|
1758
|
-
|
1759
|
-
# Skip if already found at higher priority
|
1760
|
-
if agent_id in local_agents:
|
1761
|
-
continue
|
1762
|
-
|
1763
|
-
# Extract metadata
|
1764
|
-
metadata = template_data.get("metadata", {})
|
1765
|
-
|
1766
|
-
# Build agent data in expected format
|
1767
|
-
agent_data = {
|
1768
|
-
"id": agent_id,
|
1769
|
-
"display_name": metadata.get(
|
1770
|
-
"name", agent_id.replace("_", " ").title()
|
1771
|
-
),
|
1772
|
-
"description": metadata.get(
|
1773
|
-
"description", f"Local {agent_id} agent"
|
1774
|
-
),
|
1775
|
-
"tools": self._extract_tools_from_template(template_data),
|
1776
|
-
"is_local": True,
|
1777
|
-
"tier": "project" if priority == 0 else "user",
|
1778
|
-
"author": template_data.get("author", "local"),
|
1779
|
-
"version": template_data.get("agent_version", "1.0.0"),
|
1780
|
-
}
|
1781
|
-
|
1782
|
-
local_agents[agent_id] = agent_data
|
1783
|
-
self.logger.debug(
|
1784
|
-
f"Discovered local JSON agent: {agent_id} from {template_dir}"
|
1785
|
-
)
|
1786
|
-
|
1787
|
-
except Exception as e:
|
1788
|
-
self.logger.warning(
|
1789
|
-
f"Failed to parse local JSON template {json_file}: {e}"
|
1790
|
-
)
|
1791
|
-
|
1792
|
-
return local_agents
|
1793
|
-
|
1794
|
-
def _extract_tools_from_template(self, template_data: Dict[str, Any]) -> str:
|
1795
|
-
"""Extract tools string from template data.
|
1796
|
-
|
1797
|
-
Args:
|
1798
|
-
template_data: JSON template data
|
1799
|
-
|
1800
|
-
Returns:
|
1801
|
-
Tools string for display
|
1802
|
-
"""
|
1803
|
-
capabilities = template_data.get("capabilities", {})
|
1804
|
-
tools = capabilities.get("tools", "*")
|
1805
|
-
|
1806
|
-
if tools == "*":
|
1807
|
-
return "All Tools"
|
1808
|
-
if isinstance(tools, list):
|
1809
|
-
return ", ".join(tools) if tools else "Standard Tools"
|
1810
|
-
if isinstance(tools, str):
|
1811
|
-
if "," in tools:
|
1812
|
-
return tools
|
1813
|
-
return tools
|
1814
|
-
return "Standard Tools"
|
1815
|
-
|
1816
|
-
def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
|
1817
|
-
"""Load routing metadata from agent JSON template.
|
461
|
+
from .log_manager import get_log_manager
|
1818
462
|
|
1819
|
-
|
1820
|
-
|
463
|
+
log_manager = get_log_manager()
|
464
|
+
except ImportError:
|
465
|
+
return
|
1821
466
|
|
1822
|
-
Returns:
|
1823
|
-
Dictionary with routing metadata or None if not found
|
1824
|
-
"""
|
1825
467
|
try:
|
1826
|
-
|
1827
|
-
|
1828
|
-
|
1829
|
-
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
1836
|
-
|
1837
|
-
|
1838
|
-
|
1839
|
-
return template_data.get("routing")
|
1840
|
-
except Exception as e:
|
1841
|
-
self.logger.debug(
|
1842
|
-
f"Could not load routing from packaged template for {agent_name}: {e}"
|
1843
|
-
)
|
1844
|
-
return None
|
1845
|
-
|
1846
|
-
# For development mode, load from filesystem
|
1847
|
-
templates_dir = (
|
1848
|
-
self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
|
1849
|
-
)
|
1850
|
-
template_file = templates_dir / f"{agent_name}.json"
|
1851
|
-
|
1852
|
-
if template_file.exists():
|
1853
|
-
with open(template_file) as f:
|
1854
|
-
template_data = json.load(f)
|
1855
|
-
return template_data.get("routing")
|
1856
|
-
|
1857
|
-
# Also check for variations in naming (underscore vs dash)
|
1858
|
-
# Handle common naming variations between deployed .md files and .json templates
|
1859
|
-
# Remove duplicates by using a set
|
1860
|
-
alternative_names = list(
|
1861
|
-
{
|
1862
|
-
agent_name.replace("-", "_"), # api-qa -> api_qa
|
1863
|
-
agent_name.replace("_", "-"), # api_qa -> api-qa
|
1864
|
-
agent_name.replace("-", ""), # api-qa -> apiqa
|
1865
|
-
agent_name.replace("_", ""), # api_qa -> apiqa
|
1866
|
-
}
|
1867
|
-
)
|
468
|
+
# Get or create event loop
|
469
|
+
try:
|
470
|
+
loop = asyncio.get_running_loop()
|
471
|
+
except RuntimeError:
|
472
|
+
loop = asyncio.new_event_loop()
|
473
|
+
asyncio.set_event_loop(loop)
|
474
|
+
|
475
|
+
# Prepare metadata
|
476
|
+
metadata = {
|
477
|
+
"framework_version": self.framework_version,
|
478
|
+
"framework_loaded": self.framework_content.get("loaded", False),
|
479
|
+
"session_id": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
|
480
|
+
}
|
1868
481
|
|
1869
|
-
|
1870
|
-
|
1871
|
-
|
1872
|
-
if alt_file.exists():
|
1873
|
-
with open(alt_file) as f:
|
1874
|
-
template_data = json.load(f)
|
1875
|
-
return template_data.get("routing")
|
482
|
+
# Log the prompt asynchronously
|
483
|
+
instructions = self._format_full_framework() if self.framework_content["loaded"] else self._format_minimal_framework()
|
484
|
+
metadata["instructions_length"] = len(instructions)
|
1876
485
|
|
1877
|
-
|
1878
|
-
|
486
|
+
if loop.is_running():
|
487
|
+
asyncio.create_task(log_manager.log_prompt("system_prompt", instructions, metadata))
|
488
|
+
else:
|
489
|
+
loop.run_until_complete(log_manager.log_prompt("system_prompt", instructions, metadata))
|
1879
490
|
|
491
|
+
self.logger.debug("System prompt logged to prompts directory")
|
1880
492
|
except Exception as e:
|
1881
|
-
self.logger.debug(f"Could not
|
1882
|
-
return None
|
1883
|
-
|
1884
|
-
def _generate_agent_selection_guide(self, deployed_agents: list) -> str:
|
1885
|
-
"""Generate Context-Aware Agent Selection guide from deployed agents.
|
1886
|
-
|
1887
|
-
Creates a mapping of task types to appropriate agents based on their
|
1888
|
-
descriptions and capabilities.
|
1889
|
-
"""
|
1890
|
-
guide = ""
|
1891
|
-
|
1892
|
-
# Build selection mapping based on deployed agents
|
1893
|
-
selection_map = {}
|
493
|
+
self.logger.debug(f"Could not log system prompt: {e}")
|
1894
494
|
|
1895
|
-
|
1896
|
-
agent_id = agent["id"]
|
1897
|
-
desc_lower = agent["description"].lower()
|
495
|
+
# === Agent Registry Methods (backward compatibility) ===
|
1898
496
|
|
1899
|
-
|
1900
|
-
if "implementation" in desc_lower or (
|
1901
|
-
"engineer" in agent_id and "data" not in agent_id
|
1902
|
-
):
|
1903
|
-
selection_map["Implementation tasks"] = (
|
1904
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1905
|
-
)
|
1906
|
-
if "codebase analysis" in desc_lower or "research" in agent_id:
|
1907
|
-
selection_map["Codebase analysis"] = (
|
1908
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1909
|
-
)
|
1910
|
-
if "testing" in desc_lower or "qa" in agent_id:
|
1911
|
-
selection_map["Testing/quality"] = (
|
1912
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1913
|
-
)
|
1914
|
-
if "documentation" in desc_lower:
|
1915
|
-
selection_map["Documentation"] = (
|
1916
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1917
|
-
)
|
1918
|
-
if "security" in desc_lower or "sast" in desc_lower:
|
1919
|
-
selection_map["Security operations"] = (
|
1920
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1921
|
-
)
|
1922
|
-
if (
|
1923
|
-
"deployment" in desc_lower
|
1924
|
-
or "infrastructure" in desc_lower
|
1925
|
-
or "ops" in agent_id
|
1926
|
-
):
|
1927
|
-
selection_map["Deployment/infrastructure"] = (
|
1928
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1929
|
-
)
|
1930
|
-
if "data" in desc_lower and (
|
1931
|
-
"pipeline" in desc_lower or "etl" in desc_lower
|
1932
|
-
):
|
1933
|
-
selection_map["Data pipeline/ETL"] = (
|
1934
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1935
|
-
)
|
1936
|
-
if "git" in desc_lower or "version control" in desc_lower:
|
1937
|
-
selection_map["Version control"] = (
|
1938
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1939
|
-
)
|
1940
|
-
if "ticket" in desc_lower or "epic" in desc_lower:
|
1941
|
-
selection_map["Ticket/issue management"] = (
|
1942
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1943
|
-
)
|
1944
|
-
if "browser" in desc_lower or "e2e" in desc_lower:
|
1945
|
-
selection_map["Browser/E2E testing"] = (
|
1946
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1947
|
-
)
|
1948
|
-
if "frontend" in desc_lower or "ui" in desc_lower or "html" in desc_lower:
|
1949
|
-
selection_map["Frontend/UI development"] = (
|
1950
|
-
f"{agent['display_name']} (`{agent_id}`)"
|
1951
|
-
)
|
1952
|
-
|
1953
|
-
# Always include PM questions
|
1954
|
-
selection_map["PM questions"] = "Answer directly (only exception)"
|
1955
|
-
|
1956
|
-
# Format the selection guide
|
1957
|
-
for task_type, agent_info in selection_map.items():
|
1958
|
-
guide += f"- **{task_type}** → {agent_info}\n"
|
1959
|
-
|
1960
|
-
return guide
|
1961
|
-
|
1962
|
-
def _get_fallback_capabilities(self) -> str:
|
1963
|
-
"""Return fallback capabilities when dynamic discovery fails."""
|
1964
|
-
return """
|
1965
|
-
|
1966
|
-
## Available Agent Capabilities
|
1967
|
-
|
1968
|
-
You have the following specialized agents available for delegation:
|
1969
|
-
|
1970
|
-
- **Engineer** (`engineer`): Code implementation and development
|
1971
|
-
- **Research** (`research-agent`): Investigation and analysis
|
1972
|
-
- **QA** (`qa-agent`): Testing and quality assurance
|
1973
|
-
- **Documentation** (`documentation-agent`): Documentation creation and maintenance
|
1974
|
-
- **Security** (`security-agent`): Security analysis and protection
|
1975
|
-
- **Data Engineer** (`data-engineer`): Data management and pipelines
|
1976
|
-
- **Ops** (`ops-agent`): Deployment and operations
|
1977
|
-
- **Version Control** (`version-control`): Git operations and version management
|
1978
|
-
|
1979
|
-
**IMPORTANT**: Use the exact agent ID in parentheses when delegating tasks.
|
1980
|
-
"""
|
1981
|
-
|
1982
|
-
def _format_minimal_framework(self) -> str:
|
1983
|
-
"""Format minimal framework instructions when full framework not available."""
|
1984
|
-
return """
|
1985
|
-
# Claude PM Framework Instructions
|
1986
|
-
|
1987
|
-
You are operating within a Claude PM Framework deployment.
|
1988
|
-
|
1989
|
-
## Role
|
1990
|
-
You are a multi-agent orchestrator. Your primary responsibilities:
|
1991
|
-
- Delegate tasks to specialized agents via Task Tool
|
1992
|
-
- Coordinate multi-agent workflows
|
1993
|
-
- Extract TODO/BUG/FEATURE items for ticket creation
|
1994
|
-
- NEVER perform direct implementation work
|
1995
|
-
|
1996
|
-
## Core Agents
|
1997
|
-
- Documentation Agent - Documentation tasks
|
1998
|
-
- Engineer Agent - Code implementation
|
1999
|
-
- QA Agent - Testing and validation
|
2000
|
-
- Research Agent - Investigation and analysis
|
2001
|
-
- Version Control Agent - Git operations
|
2002
|
-
|
2003
|
-
## Important Rules
|
2004
|
-
1. Always delegate work via Task Tool
|
2005
|
-
2. Provide comprehensive context to agents
|
2006
|
-
3. Track all TODO/BUG/FEATURE items
|
2007
|
-
4. Maintain project visibility
|
2008
|
-
|
2009
|
-
---
|
2010
|
-
"""
|
2011
|
-
|
2012
|
-
def get_agent_list(self) -> list:
|
497
|
+
def get_agent_list(self) -> List[str]:
|
2013
498
|
"""Get list of available agents."""
|
2014
|
-
# First try agent registry
|
2015
499
|
if self.agent_registry:
|
2016
500
|
agents = self.agent_registry.list_agents()
|
2017
501
|
if agents:
|
2018
502
|
return list(agents.keys())
|
2019
|
-
|
2020
|
-
# Fallback to loaded content
|
2021
503
|
return list(self.framework_content["agents"].keys())
|
2022
504
|
|
2023
505
|
def get_agent_definition(self, agent_name: str) -> Optional[str]:
|
2024
506
|
"""Get specific agent definition."""
|
2025
|
-
# First try agent registry
|
2026
507
|
if self.agent_registry:
|
2027
508
|
definition = self.agent_registry.get_agent_definition(agent_name)
|
2028
509
|
if definition:
|
2029
510
|
return definition
|
2030
|
-
|
2031
|
-
# Fallback to loaded content
|
2032
511
|
return self.framework_content["agents"].get(agent_name)
|
2033
512
|
|
2034
|
-
def get_agent_hierarchy(self) -> Dict[str,
|
513
|
+
def get_agent_hierarchy(self) -> Dict[str, List]:
|
2035
514
|
"""Get agent hierarchy from registry."""
|
2036
515
|
if self.agent_registry:
|
2037
516
|
return self.agent_registry.get_agent_hierarchy()
|
2038
|
-
return {"project": [], "user": [], "system": []}
|
517
|
+
return {"project": [], "user": [], "system": []}
|