claude-mpm 4.4.0__py3-none-any.whl → 4.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- claude_mpm/agents/agent_loader.py +3 -2
- claude_mpm/agents/agent_loader_integration.py +2 -1
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +1 -0
- claude_mpm/agents/system_agent_config.py +2 -1
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +44 -5
- claude_mpm/cli/commands/mpm_init.py +117 -63
- claude_mpm/cli/parsers/configure_parser.py +6 -15
- claude_mpm/cli/startup_logging.py +1 -3
- claude_mpm/config/agent_config.py +1 -1
- claude_mpm/config/paths.py +2 -1
- claude_mpm/core/agent_name_normalizer.py +1 -0
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -1
- claude_mpm/core/file_utils.py +0 -1
- 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 +367 -0
- claude_mpm/core/framework/formatters/content_formatter.py +288 -0
- claude_mpm/core/framework/formatters/context_generator.py +184 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +206 -0
- claude_mpm/core/framework/loaders/file_loader.py +223 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +230 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +244 -0
- claude_mpm/core/framework_loader.py +298 -1795
- claude_mpm/core/log_manager.py +2 -1
- claude_mpm/core/tool_access_control.py +1 -0
- claude_mpm/core/unified_agent_registry.py +2 -1
- claude_mpm/core/unified_paths.py +1 -0
- claude_mpm/experimental/cli_enhancements.py +1 -0
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/base_hook.py +1 -0
- claude_mpm/hooks/instruction_reinforcement.py +1 -0
- claude_mpm/hooks/kuzu_memory_hook.py +359 -0
- claude_mpm/hooks/validation_hooks.py +1 -1
- claude_mpm/scripts/mpm_doctor.py +1 -0
- claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
- claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
- claude_mpm/services/agents/management/agent_management_service.py +1 -1
- claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
- claude_mpm/services/agents/memory/memory_file_service.py +6 -2
- claude_mpm/services/agents/memory/memory_format_service.py +0 -1
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
- claude_mpm/services/async_session_logger.py +1 -1
- claude_mpm/services/claude_session_logger.py +1 -0
- claude_mpm/services/core/path_resolver.py +2 -0
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +1 -0
- claude_mpm/services/event_bus/relay.py +3 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
- claude_mpm/services/infrastructure/daemon_manager.py +1 -1
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +320 -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 +14 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
- claude_mpm/services/memory/cache/simple_cache.py +1 -1
- claude_mpm/services/project/archive_manager.py +159 -96
- claude_mpm/services/project/documentation_manager.py +64 -45
- claude_mpm/services/project/enhanced_analyzer.py +132 -89
- claude_mpm/services/project/project_organizer.py +225 -131
- claude_mpm/services/response_tracker.py +1 -1
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
- claude_mpm/services/unified/__init__.py +1 -1
- claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
- claude_mpm/services/unified/config_strategies/__init__.py +175 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
- claude_mpm/services/unified/deployment_strategies/base.py +24 -28
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
- claude_mpm/services/unified/deployment_strategies/local.py +49 -34
- claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
- claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
- claude_mpm/services/unified/interfaces.py +0 -26
- claude_mpm/services/unified/migration.py +17 -40
- claude_mpm/services/unified/strategies.py +9 -26
- claude_mpm/services/unified/unified_analyzer.py +48 -44
- claude_mpm/services/unified/unified_config.py +21 -19
- claude_mpm/services/unified/unified_deployment.py +21 -26
- claude_mpm/storage/state_storage.py +1 -0
- claude_mpm/utils/agent_dependency_loader.py +18 -6
- claude_mpm/utils/common.py +14 -12
- claude_mpm/utils/database_connector.py +15 -12
- claude_mpm/utils/error_handler.py +1 -0
- claude_mpm/utils/log_cleanup.py +1 -0
- claude_mpm/utils/path_operations.py +1 -0
- claude_mpm/utils/session_logging.py +1 -1
- claude_mpm/utils/subprocess_utils.py +1 -0
- claude_mpm/validation/agent_validator.py +1 -1
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
- 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.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,27 @@
|
|
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
|
-
# Import
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
6
|
+
from typing import Any, Dict, List, Optional, Set
|
7
|
+
|
8
|
+
# Import framework components
|
9
|
+
from claude_mpm.core.framework import (
|
10
|
+
AgentLoader,
|
11
|
+
CapabilityGenerator,
|
12
|
+
ContentFormatter,
|
13
|
+
ContextGenerator,
|
14
|
+
FileLoader,
|
15
|
+
InstructionLoader,
|
16
|
+
MemoryProcessor,
|
17
|
+
MetadataProcessor,
|
18
|
+
PackagedLoader,
|
19
|
+
TemplateProcessor,
|
20
|
+
)
|
21
|
+
from claude_mpm.core.logging_utils import get_logger
|
22
|
+
from claude_mpm.utils.imports import safe_import
|
26
23
|
|
27
|
-
# Import with fallback support
|
28
|
-
get_logger = safe_import("claude_mpm.core.logger", "core.logger", ["get_logger"])
|
24
|
+
# Import with fallback support
|
29
25
|
AgentRegistryAdapter = safe_import(
|
30
26
|
"claude_mpm.core.agent_registry", "core.agent_registry", ["AgentRegistryAdapter"]
|
31
27
|
)
|
@@ -67,43 +63,13 @@ class FrameworkLoader:
|
|
67
63
|
"""
|
68
64
|
Load and prepare framework instructions for injection.
|
69
65
|
|
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
|
66
|
+
This refactored version uses modular components for better maintainability
|
67
|
+
and testability while maintaining backward compatibility.
|
68
|
+
|
69
|
+
Components:
|
70
|
+
- Loaders: Handle file I/O and resource loading
|
71
|
+
- Formatters: Generate and format content sections
|
72
|
+
- Processors: Process metadata, templates, and memories
|
107
73
|
"""
|
108
74
|
|
109
75
|
def __init__(
|
@@ -114,7 +80,7 @@ class FrameworkLoader:
|
|
114
80
|
config: Optional[Dict[str, Any]] = None,
|
115
81
|
):
|
116
82
|
"""
|
117
|
-
Initialize framework loader.
|
83
|
+
Initialize framework loader with modular components.
|
118
84
|
|
119
85
|
Args:
|
120
86
|
framework_path: Explicit path to framework (auto-detected if None)
|
@@ -129,6 +95,39 @@ class FrameworkLoader:
|
|
129
95
|
self.config = config or {}
|
130
96
|
|
131
97
|
# Validate API keys on startup (before any other initialization)
|
98
|
+
self._validate_api_keys()
|
99
|
+
|
100
|
+
# Initialize service container
|
101
|
+
self.container = service_container or get_global_container()
|
102
|
+
self._register_services()
|
103
|
+
|
104
|
+
# Resolve services from container
|
105
|
+
self._cache_manager = self.container.resolve(ICacheManager)
|
106
|
+
self._path_resolver = self.container.resolve(IPathResolver)
|
107
|
+
self._memory_manager = self.container.resolve(IMemoryManager)
|
108
|
+
|
109
|
+
# Initialize framework path
|
110
|
+
self.framework_path = (
|
111
|
+
framework_path or self._path_resolver.detect_framework_path()
|
112
|
+
)
|
113
|
+
|
114
|
+
# Initialize modular components
|
115
|
+
self._init_components()
|
116
|
+
|
117
|
+
# Keep cache TTL constants for backward compatibility
|
118
|
+
self._init_cache_ttl()
|
119
|
+
|
120
|
+
# Load framework content
|
121
|
+
self.framework_content = self._load_framework_content()
|
122
|
+
|
123
|
+
# Initialize agent registry
|
124
|
+
self.agent_registry = AgentRegistryAdapter(self.framework_path)
|
125
|
+
|
126
|
+
# Output style manager (deferred initialization)
|
127
|
+
self.output_style_manager = None
|
128
|
+
|
129
|
+
def _validate_api_keys(self) -> None:
|
130
|
+
"""Validate API keys if enabled in config."""
|
132
131
|
if self.config.get("validate_api_keys", True):
|
133
132
|
try:
|
134
133
|
self.logger.info("Validating configured API keys...")
|
@@ -141,21 +140,17 @@ class FrameworkLoader:
|
|
141
140
|
self.logger.error(f"❌ Unexpected error during API validation: {e}")
|
142
141
|
raise
|
143
142
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
# Register services if not already registered
|
143
|
+
def _register_services(self) -> None:
|
144
|
+
"""Register services in the container if not already registered."""
|
148
145
|
if not self.container.is_registered(ICacheManager):
|
149
|
-
self.container.register(ICacheManager, CacheManager, True)
|
146
|
+
self.container.register(ICacheManager, CacheManager, True)
|
150
147
|
|
151
148
|
if not self.container.is_registered(IPathResolver):
|
152
|
-
# PathResolver depends on CacheManager, so resolve it first
|
153
149
|
cache_manager = self.container.resolve(ICacheManager)
|
154
150
|
path_resolver = PathResolver(cache_manager=cache_manager)
|
155
151
|
self.container.register_instance(IPathResolver, path_resolver)
|
156
152
|
|
157
153
|
if not self.container.is_registered(IMemoryManager):
|
158
|
-
# MemoryManager depends on both CacheManager and PathResolver
|
159
154
|
cache_manager = self.container.resolve(ICacheManager)
|
160
155
|
path_resolver = self.container.resolve(IPathResolver)
|
161
156
|
memory_manager = MemoryManager(
|
@@ -163,18 +158,26 @@ class FrameworkLoader:
|
|
163
158
|
)
|
164
159
|
self.container.register_instance(IMemoryManager, memory_manager)
|
165
160
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
self.
|
170
|
-
|
171
|
-
|
172
|
-
self.
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
161
|
+
def _init_components(self) -> None:
|
162
|
+
"""Initialize modular components."""
|
163
|
+
# Loaders
|
164
|
+
self.file_loader = FileLoader()
|
165
|
+
self.packaged_loader = PackagedLoader()
|
166
|
+
self.instruction_loader = InstructionLoader(self.framework_path)
|
167
|
+
self.agent_loader = AgentLoader(self.framework_path)
|
168
|
+
|
169
|
+
# Formatters
|
170
|
+
self.content_formatter = ContentFormatter()
|
171
|
+
self.capability_generator = CapabilityGenerator()
|
172
|
+
self.context_generator = ContextGenerator()
|
173
|
+
|
174
|
+
# Processors
|
175
|
+
self.metadata_processor = MetadataProcessor()
|
176
|
+
self.template_processor = TemplateProcessor(self.framework_path)
|
177
|
+
self.memory_processor = MemoryProcessor()
|
178
|
+
|
179
|
+
def _init_cache_ttl(self) -> None:
|
180
|
+
"""Initialize cache TTL constants for backward compatibility."""
|
178
181
|
if hasattr(self._cache_manager, "capabilities_ttl"):
|
179
182
|
self.CAPABILITIES_CACHE_TTL = self._cache_manager.capabilities_ttl
|
180
183
|
self.DEPLOYED_AGENTS_CACHE_TTL = self._cache_manager.deployed_agents_ttl
|
@@ -187,430 +190,24 @@ class FrameworkLoader:
|
|
187
190
|
self.METADATA_CACHE_TTL = 60
|
188
191
|
self.MEMORIES_CACHE_TTL = 60
|
189
192
|
|
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
|
193
|
+
# === Cache Management Methods (backward compatibility) ===
|
198
194
|
|
199
195
|
def clear_all_caches(self) -> None:
|
200
196
|
"""Clear all caches to force reload on next access."""
|
201
197
|
self._cache_manager.clear_all()
|
202
198
|
|
203
199
|
def clear_agent_caches(self) -> None:
|
204
|
-
"""Clear agent-related caches
|
200
|
+
"""Clear agent-related caches."""
|
205
201
|
self._cache_manager.clear_agent_caches()
|
206
202
|
|
207
203
|
def clear_memory_caches(self) -> None:
|
208
204
|
"""Clear memory-related caches."""
|
209
205
|
self._cache_manager.clear_memory_caches()
|
210
206
|
|
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)
|
207
|
+
# === Content Loading Methods ===
|
611
208
|
|
612
209
|
def _load_framework_content(self) -> Dict[str, Any]:
|
613
|
-
"""Load framework content."""
|
210
|
+
"""Load framework content using modular components."""
|
614
211
|
content = {
|
615
212
|
"claude_md": "",
|
616
213
|
"agents": {},
|
@@ -619,290 +216,102 @@ class FrameworkLoader:
|
|
619
216
|
"working_claude_md": "",
|
620
217
|
"framework_instructions": "",
|
621
218
|
"workflow_instructions": "",
|
622
|
-
"workflow_instructions_level": "",
|
219
|
+
"workflow_instructions_level": "",
|
623
220
|
"memory_instructions": "",
|
624
|
-
"memory_instructions_level": "",
|
625
|
-
"project_workflow": "", # Deprecated
|
626
|
-
"project_memory": "", # Deprecated
|
627
|
-
"actual_memories": "",
|
221
|
+
"memory_instructions_level": "",
|
222
|
+
"project_workflow": "", # Deprecated
|
223
|
+
"project_memory": "", # Deprecated
|
224
|
+
"actual_memories": "",
|
225
|
+
"agent_memories": {},
|
628
226
|
}
|
629
227
|
|
630
|
-
# Load instructions
|
631
|
-
self.
|
632
|
-
|
633
|
-
if not self.framework_path:
|
634
|
-
return content
|
228
|
+
# Load all instructions
|
229
|
+
self.instruction_loader.load_all_instructions(content)
|
635
230
|
|
636
|
-
#
|
637
|
-
if self.
|
638
|
-
|
639
|
-
self.
|
640
|
-
|
641
|
-
|
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
|
-
)
|
231
|
+
# Transfer metadata from loaders
|
232
|
+
if self.file_loader.framework_version:
|
233
|
+
self.framework_version = self.file_loader.framework_version
|
234
|
+
content["version"] = self.framework_version
|
235
|
+
if self.file_loader.framework_last_modified:
|
236
|
+
self.framework_last_modified = self.file_loader.framework_last_modified
|
657
237
|
|
658
|
-
|
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
|
238
|
+
# Load memories
|
707
239
|
self._load_actual_memories(content)
|
708
240
|
|
709
|
-
# Discover
|
241
|
+
# Discover and load agents
|
710
242
|
agents_dir, templates_dir, main_dir = self._path_resolver.discover_agent_paths(
|
711
243
|
agents_dir=self.agents_dir, framework_path=self.framework_path
|
712
244
|
)
|
713
|
-
|
714
|
-
|
715
|
-
|
245
|
+
agents = self.agent_loader.load_agents_directory(
|
246
|
+
agents_dir, templates_dir, main_dir
|
247
|
+
)
|
248
|
+
if agents:
|
249
|
+
content["agents"] = agents
|
250
|
+
content["loaded"] = True
|
716
251
|
|
717
252
|
return content
|
718
253
|
|
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}")
|
787
|
-
|
788
|
-
def _load_packaged_framework_content_fallback(
|
789
|
-
self, content: Dict[str, Any], resources
|
790
|
-
) -> None:
|
791
|
-
"""Load framework content using importlib.resources fallback."""
|
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
|
254
|
+
def _load_actual_memories(self, content: Dict[str, Any]) -> None:
|
255
|
+
"""Load actual memories using the MemoryManager service."""
|
256
|
+
memories = self._memory_manager.load_memories()
|
829
257
|
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
if workflow_content:
|
835
|
-
content["workflow_instructions"] = workflow_content
|
836
|
-
content["project_workflow"] = "system"
|
258
|
+
if "actual_memories" in memories:
|
259
|
+
content["actual_memories"] = memories["actual_memories"]
|
260
|
+
if "agent_memories" in memories:
|
261
|
+
content["agent_memories"] = memories["agent_memories"]
|
837
262
|
|
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"
|
263
|
+
# === Agent Discovery Methods ===
|
843
264
|
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
265
|
+
def _get_deployed_agents(self) -> Set[str]:
|
266
|
+
"""Get deployed agents with caching."""
|
267
|
+
cached = self._cache_manager.get_deployed_agents()
|
268
|
+
if cached is not None:
|
269
|
+
return cached
|
848
270
|
|
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
|
271
|
+
deployed = self.agent_loader.get_deployed_agents()
|
272
|
+
self._cache_manager.set_deployed_agents(deployed)
|
273
|
+
return deployed
|
873
274
|
|
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
|
275
|
+
def _discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
|
276
|
+
"""Discover local JSON agent templates."""
|
277
|
+
return self.agent_loader.discover_local_json_templates()
|
890
278
|
|
891
|
-
def
|
892
|
-
"""
|
893
|
-
|
279
|
+
def _parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
|
280
|
+
"""Parse agent metadata with caching."""
|
281
|
+
cache_key = str(agent_file)
|
282
|
+
file_mtime = agent_file.stat().st_mtime
|
283
|
+
|
284
|
+
# Try cache first
|
285
|
+
cached_result = self._cache_manager.get_agent_metadata(cache_key)
|
286
|
+
if cached_result is not None:
|
287
|
+
cached_data, cached_mtime = cached_result
|
288
|
+
if cached_mtime == file_mtime:
|
289
|
+
self.logger.debug(f"Using cached metadata for {agent_file.name}")
|
290
|
+
return cached_data
|
291
|
+
|
292
|
+
# Cache miss - parse the file
|
293
|
+
agent_data = self.metadata_processor.parse_agent_metadata(agent_file)
|
294
|
+
|
295
|
+
# Add routing information if not present
|
296
|
+
if agent_data and "routing" not in agent_data:
|
297
|
+
template_data = self.template_processor.load_template(agent_file.stem)
|
298
|
+
if template_data:
|
299
|
+
routing = self.template_processor.extract_routing(template_data)
|
300
|
+
if routing:
|
301
|
+
agent_data["routing"] = routing
|
302
|
+
memory_routing = self.template_processor.extract_memory_routing(
|
303
|
+
template_data
|
304
|
+
)
|
305
|
+
if memory_routing:
|
306
|
+
agent_data["memory_routing"] = memory_routing
|
307
|
+
|
308
|
+
# Cache the result
|
309
|
+
if agent_data:
|
310
|
+
self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
|
894
311
|
|
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}")
|
312
|
+
return agent_data
|
900
313
|
|
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}")
|
314
|
+
# === Framework Instructions Generation ===
|
906
315
|
|
907
316
|
def get_framework_instructions(self) -> str:
|
908
317
|
"""
|
@@ -911,1127 +320,221 @@ class FrameworkLoader:
|
|
911
320
|
Returns:
|
912
321
|
Complete framework instructions ready for injection
|
913
322
|
"""
|
914
|
-
#
|
915
|
-
|
916
|
-
from .log_manager import get_log_manager
|
917
|
-
|
918
|
-
log_manager = get_log_manager()
|
919
|
-
except ImportError:
|
920
|
-
log_manager = None
|
323
|
+
# Log the system prompt if needed
|
324
|
+
self._log_system_prompt()
|
921
325
|
|
922
326
|
# Generate the instructions
|
923
327
|
if self.framework_content["loaded"]:
|
924
|
-
|
925
|
-
|
926
|
-
else:
|
927
|
-
# Use minimal fallback
|
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")
|
328
|
+
return self._format_full_framework()
|
329
|
+
return self._format_minimal_framework()
|
984
330
|
|
985
331
|
def _format_full_framework(self) -> str:
|
986
|
-
"""Format full framework instructions."""
|
987
|
-
|
988
|
-
# Initialize output style manager on first use (ensures content is loaded)
|
332
|
+
"""Format full framework instructions using modular components."""
|
333
|
+
# Initialize output style manager on first use
|
989
334
|
if self.output_style_manager is None:
|
990
335
|
self._initialize_output_style()
|
991
336
|
|
992
|
-
# Check if we need to inject output style
|
337
|
+
# Check if we need to inject output style
|
993
338
|
inject_output_style = False
|
339
|
+
output_style_content = None
|
994
340
|
if self.output_style_manager:
|
995
341
|
inject_output_style = self.output_style_manager.should_inject_content()
|
996
342
|
if inject_output_style:
|
997
|
-
self.logger.info(
|
998
|
-
"Injecting output style content into instructions for Claude < 1.0.83"
|
999
|
-
)
|
1000
|
-
|
1001
|
-
# If we have the full framework INSTRUCTIONS.md, use it
|
1002
|
-
if self.framework_content.get("framework_instructions"):
|
1003
|
-
instructions = self._strip_metadata_comments(
|
1004
|
-
self.framework_content["framework_instructions"]
|
1005
|
-
)
|
1006
|
-
|
1007
|
-
# Note: We don't add working directory CLAUDE.md here since Claude Code
|
1008
|
-
# already picks it up automatically. This prevents duplication.
|
1009
|
-
|
1010
|
-
# Add custom INSTRUCTIONS.md if present (overrides or extends framework instructions)
|
1011
|
-
if self.framework_content.get("custom_instructions"):
|
1012
|
-
level = self.framework_content.get(
|
1013
|
-
"custom_instructions_level", "unknown"
|
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}"
|
1081
|
-
|
1082
|
-
# Inject output style content if needed (for Claude < 1.0.83)
|
1083
|
-
if inject_output_style and self.output_style_manager:
|
1084
343
|
output_style_content = self.output_style_manager.get_injectable_content(
|
1085
344
|
framework_loader=self
|
1086
345
|
)
|
1087
|
-
|
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
|
-
)
|
346
|
+
self.logger.info("Injecting output style content for Claude < 1.0.83")
|
1164
347
|
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
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
|
348
|
+
# Generate dynamic sections
|
349
|
+
capabilities_section = self._generate_agent_capabilities_section()
|
350
|
+
context_section = self.context_generator.generate_temporal_user_context()
|
1213
351
|
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
"""
|
352
|
+
# Format the complete framework
|
353
|
+
return self.content_formatter.format_full_framework(
|
354
|
+
self.framework_content,
|
355
|
+
capabilities_section,
|
356
|
+
context_section,
|
357
|
+
inject_output_style,
|
358
|
+
output_style_content,
|
359
|
+
)
|
360
|
+
|
361
|
+
def _format_minimal_framework(self) -> str:
|
362
|
+
"""Format minimal framework instructions."""
|
363
|
+
return self.content_formatter.format_minimal_framework(self.framework_content)
|
1225
364
|
|
1226
|
-
|
365
|
+
def _generate_agent_capabilities_section(self) -> str:
|
366
|
+
"""Generate agent capabilities section with caching."""
|
367
|
+
# Try cache first
|
1227
368
|
cached_capabilities = self._cache_manager.get_capabilities()
|
1228
369
|
if cached_capabilities is not None:
|
1229
370
|
return cached_capabilities
|
1230
371
|
|
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)")
|
372
|
+
self.logger.debug("Generating agent capabilities (cache miss)")
|
1236
373
|
|
1237
374
|
try:
|
1238
|
-
|
1239
|
-
|
1240
|
-
# First check for local JSON templates (highest priority)
|
375
|
+
# Discover local JSON templates
|
1241
376
|
local_agents = self._discover_local_json_templates()
|
1242
377
|
|
1243
|
-
#
|
1244
|
-
|
1245
|
-
# Priority order: local templates > project > user home > fallback
|
378
|
+
# Get deployed agents from .claude/agents/
|
379
|
+
deployed_agents = []
|
1246
380
|
agents_dirs = [
|
1247
|
-
Path.cwd() / ".claude" / "agents",
|
1248
|
-
Path.home() / ".claude" / "agents",
|
381
|
+
Path.cwd() / ".claude" / "agents",
|
382
|
+
Path.home() / ".claude" / "agents",
|
1249
383
|
]
|
1250
384
|
|
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
|
-
)
|
385
|
+
for agents_dir in agents_dirs:
|
386
|
+
if agents_dir.exists():
|
387
|
+
for agent_file in agents_dir.glob("*.md"):
|
388
|
+
if not agent_file.name.startswith("."):
|
389
|
+
agent_data = self._parse_agent_metadata(agent_file)
|
390
|
+
if agent_data:
|
391
|
+
deployed_agents.append(agent_data)
|
1311
392
|
|
1312
|
-
#
|
1313
|
-
section =
|
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"
|
1338
|
-
|
1339
|
-
# Add local indicator if this is a local agent
|
1340
|
-
if agent.get("is_local"):
|
1341
|
-
tier_label = f" [LOCAL-{agent.get('tier', 'PROJECT').upper()}]"
|
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"
|
393
|
+
# Generate capabilities section
|
394
|
+
section = self.capability_generator.generate_capabilities_section(
|
395
|
+
deployed_agents, local_agents
|
1404
396
|
)
|
1405
397
|
|
1406
|
-
#
|
1407
|
-
section += f"\n**Total Available Agents**: {len(deployed_agents)}\n"
|
1408
|
-
|
1409
|
-
# Cache the generated capabilities
|
398
|
+
# Cache the result
|
1410
399
|
self._cache_manager.set_capabilities(section)
|
1411
|
-
self.logger.debug(
|
1412
|
-
f"Cached agent capabilities section ({len(section)} chars)"
|
1413
|
-
)
|
400
|
+
self.logger.debug(f"Cached agent capabilities ({len(section)} chars)")
|
1414
401
|
|
1415
402
|
return section
|
1416
403
|
|
1417
404
|
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
|
405
|
+
self.logger.warning(f"Could not generate agent capabilities: {e}")
|
406
|
+
fallback = self.content_formatter.get_fallback_capabilities()
|
407
|
+
self._cache_manager.set_capabilities(fallback)
|
408
|
+
return fallback
|
1424
409
|
|
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"]
|
410
|
+
# === Output Style Management ===
|
1432
411
|
|
412
|
+
def _initialize_output_style(self) -> None:
|
413
|
+
"""Initialize output style management."""
|
1433
414
|
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")
|
415
|
+
from claude_mpm.core.output_style_manager import OutputStyleManager
|
1463
416
|
|
1464
|
-
|
1465
|
-
|
1466
|
-
)
|
1467
|
-
context_lines.append(f"**Day**: {day_name}\n")
|
417
|
+
self.output_style_manager = OutputStyleManager()
|
418
|
+
self._log_output_style_status()
|
1468
419
|
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
420
|
+
# Extract and save output style content
|
421
|
+
output_style_content = (
|
422
|
+
self.output_style_manager.extract_output_style_content(
|
423
|
+
framework_loader=self
|
424
|
+
)
|
1474
425
|
)
|
426
|
+
self.output_style_manager.save_output_style(output_style_content)
|
1475
427
|
|
1476
|
-
|
1477
|
-
|
1478
|
-
|
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}")
|
1509
|
-
|
1510
|
-
try:
|
1511
|
-
# Get system information
|
1512
|
-
system_info = platform.system()
|
1513
|
-
if system_info:
|
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
|
1532
|
-
|
1533
|
-
except Exception as e:
|
1534
|
-
# System info is optional
|
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)"
|
428
|
+
# Deploy to Claude Code if supported
|
429
|
+
deployed = self.output_style_manager.deploy_output_style(
|
430
|
+
output_style_content
|
1591
431
|
)
|
1592
432
|
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
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)
|
1646
|
-
|
1647
|
-
return agent_data
|
433
|
+
if deployed:
|
434
|
+
self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
|
435
|
+
else:
|
436
|
+
self.logger.info("📝 Output style will be injected into instructions")
|
1648
437
|
|
1649
438
|
except Exception as e:
|
1650
|
-
self.logger.
|
1651
|
-
return None
|
1652
|
-
|
1653
|
-
def _load_memory_routing_from_template(
|
1654
|
-
self, agent_name: str
|
1655
|
-
) -> Optional[Dict[str, Any]]:
|
1656
|
-
"""Load memory routing metadata from agent JSON template.
|
1657
|
-
|
1658
|
-
Args:
|
1659
|
-
agent_name: Name of the agent (stem of the file)
|
1660
|
-
|
1661
|
-
Returns:
|
1662
|
-
Dictionary with memory routing metadata or None if not found
|
1663
|
-
"""
|
1664
|
-
try:
|
1665
|
-
import json
|
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
|
439
|
+
self.logger.warning(f"❌ Failed to initialize output style manager: {e}")
|
1721
440
|
|
1722
|
-
|
1723
|
-
|
1724
|
-
|
1725
|
-
|
1726
|
-
return None
|
441
|
+
def _log_output_style_status(self) -> None:
|
442
|
+
"""Log output style status information."""
|
443
|
+
if not self.output_style_manager:
|
444
|
+
return
|
1727
445
|
|
1728
|
-
|
1729
|
-
|
446
|
+
claude_version = self.output_style_manager.claude_version
|
447
|
+
if claude_version:
|
448
|
+
self.logger.info(f"Claude Code version detected: {claude_version}")
|
1730
449
|
|
1731
|
-
|
1732
|
-
|
1733
|
-
|
1734
|
-
|
1735
|
-
|
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}"
|
450
|
+
if self.output_style_manager.supports_output_styles():
|
451
|
+
self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
|
452
|
+
output_style_path = self.output_style_manager.output_style_path
|
453
|
+
if output_style_path.exists():
|
454
|
+
self.logger.info(
|
455
|
+
f"📁 Output style file exists: {output_style_path}"
|
1785
456
|
)
|
1786
|
-
|
1787
|
-
|
1788
|
-
|
1789
|
-
f"Failed to parse local JSON template {json_file}: {e}"
|
457
|
+
else:
|
458
|
+
self.logger.info(
|
459
|
+
f"📝 Output style will be created at: {output_style_path}"
|
1790
460
|
)
|
461
|
+
else:
|
462
|
+
self.logger.info(
|
463
|
+
f"⚠️ Claude Code {claude_version} does not support output styles"
|
464
|
+
)
|
465
|
+
self.logger.info(
|
466
|
+
"📝 Output style will be injected into framework instructions"
|
467
|
+
)
|
468
|
+
else:
|
469
|
+
self.logger.info("⚠️ Claude Code not detected or version unknown")
|
470
|
+
self.logger.info("📝 Output style will be injected as fallback")
|
1791
471
|
|
1792
|
-
|
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.
|
1818
|
-
|
1819
|
-
Args:
|
1820
|
-
agent_name: Name of the agent (stem of the file)
|
472
|
+
# === Logging Methods ===
|
1821
473
|
|
1822
|
-
|
1823
|
-
|
1824
|
-
"""
|
474
|
+
def _log_system_prompt(self) -> None:
|
475
|
+
"""Log the system prompt if LogManager is available."""
|
1825
476
|
try:
|
1826
|
-
import
|
1827
|
-
|
1828
|
-
# Check if we have a framework path
|
1829
|
-
if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
|
1830
|
-
# For packaged installations, try to load from package resources
|
1831
|
-
if files:
|
1832
|
-
try:
|
1833
|
-
templates_package = files("claude_mpm.agents.templates")
|
1834
|
-
template_file = templates_package / f"{agent_name}.json"
|
1835
|
-
|
1836
|
-
if template_file.is_file():
|
1837
|
-
template_content = template_file.read_text()
|
1838
|
-
template_data = json.loads(template_content)
|
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
|
-
)
|
1868
|
-
|
1869
|
-
for alt_name in alternative_names:
|
1870
|
-
if alt_name != agent_name:
|
1871
|
-
alt_file = templates_dir / f"{alt_name}.json"
|
1872
|
-
if alt_file.exists():
|
1873
|
-
with open(alt_file) as f:
|
1874
|
-
template_data = json.load(f)
|
1875
|
-
return template_data.get("routing")
|
1876
|
-
|
1877
|
-
self.logger.debug(f"No JSON template found for agent: {agent_name}")
|
1878
|
-
return None
|
1879
|
-
|
1880
|
-
except Exception as e:
|
1881
|
-
self.logger.debug(f"Could not load routing metadata for {agent_name}: {e}")
|
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.
|
477
|
+
from .log_manager import get_log_manager
|
1886
478
|
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
guide = ""
|
479
|
+
log_manager = get_log_manager()
|
480
|
+
except ImportError:
|
481
|
+
return
|
1891
482
|
|
1892
|
-
|
1893
|
-
|
483
|
+
try:
|
484
|
+
# Get or create event loop
|
485
|
+
try:
|
486
|
+
loop = asyncio.get_running_loop()
|
487
|
+
except RuntimeError:
|
488
|
+
loop = asyncio.new_event_loop()
|
489
|
+
asyncio.set_event_loop(loop)
|
490
|
+
|
491
|
+
# Prepare metadata
|
492
|
+
metadata = {
|
493
|
+
"framework_version": self.framework_version,
|
494
|
+
"framework_loaded": self.framework_content.get("loaded", False),
|
495
|
+
"session_id": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
|
496
|
+
}
|
1894
497
|
|
1895
|
-
|
1896
|
-
|
1897
|
-
|
498
|
+
# Log the prompt asynchronously
|
499
|
+
instructions = (
|
500
|
+
self._format_full_framework()
|
501
|
+
if self.framework_content["loaded"]
|
502
|
+
else self._format_minimal_framework()
|
503
|
+
)
|
504
|
+
metadata["instructions_length"] = len(instructions)
|
1898
505
|
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
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}`)"
|
506
|
+
if loop.is_running():
|
507
|
+
asyncio.create_task(
|
508
|
+
log_manager.log_prompt("system_prompt", instructions, metadata)
|
1935
509
|
)
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
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}`)"
|
510
|
+
else:
|
511
|
+
loop.run_until_complete(
|
512
|
+
log_manager.log_prompt("system_prompt", instructions, metadata)
|
1951
513
|
)
|
1952
514
|
|
1953
|
-
|
1954
|
-
|
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
|
515
|
+
self.logger.debug("System prompt logged to prompts directory")
|
516
|
+
except Exception as e:
|
517
|
+
self.logger.debug(f"Could not log system prompt: {e}")
|
1978
518
|
|
1979
|
-
|
1980
|
-
"""
|
519
|
+
# === Agent Registry Methods (backward compatibility) ===
|
1981
520
|
|
1982
|
-
def
|
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:
|
521
|
+
def get_agent_list(self) -> List[str]:
|
2013
522
|
"""Get list of available agents."""
|
2014
|
-
# First try agent registry
|
2015
523
|
if self.agent_registry:
|
2016
524
|
agents = self.agent_registry.list_agents()
|
2017
525
|
if agents:
|
2018
526
|
return list(agents.keys())
|
2019
|
-
|
2020
|
-
# Fallback to loaded content
|
2021
527
|
return list(self.framework_content["agents"].keys())
|
2022
528
|
|
2023
529
|
def get_agent_definition(self, agent_name: str) -> Optional[str]:
|
2024
530
|
"""Get specific agent definition."""
|
2025
|
-
# First try agent registry
|
2026
531
|
if self.agent_registry:
|
2027
532
|
definition = self.agent_registry.get_agent_definition(agent_name)
|
2028
533
|
if definition:
|
2029
534
|
return definition
|
2030
|
-
|
2031
|
-
# Fallback to loaded content
|
2032
535
|
return self.framework_content["agents"].get(agent_name)
|
2033
536
|
|
2034
|
-
def get_agent_hierarchy(self) -> Dict[str,
|
537
|
+
def get_agent_hierarchy(self) -> Dict[str, List]:
|
2035
538
|
"""Get agent hierarchy from registry."""
|
2036
539
|
if self.agent_registry:
|
2037
540
|
return self.agent_registry.get_agent_hierarchy()
|