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.
Files changed (129) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/agents/agent_loader.py +3 -2
  4. claude_mpm/agents/agent_loader_integration.py +2 -1
  5. claude_mpm/agents/async_agent_loader.py +2 -2
  6. claude_mpm/agents/base_agent_loader.py +2 -2
  7. claude_mpm/agents/frontmatter_validator.py +1 -0
  8. claude_mpm/agents/system_agent_config.py +2 -1
  9. claude_mpm/cli/commands/configure.py +2 -29
  10. claude_mpm/cli/commands/doctor.py +44 -5
  11. claude_mpm/cli/commands/mpm_init.py +117 -63
  12. claude_mpm/cli/parsers/configure_parser.py +6 -15
  13. claude_mpm/cli/startup_logging.py +1 -3
  14. claude_mpm/config/agent_config.py +1 -1
  15. claude_mpm/config/paths.py +2 -1
  16. claude_mpm/core/agent_name_normalizer.py +1 -0
  17. claude_mpm/core/config.py +2 -1
  18. claude_mpm/core/config_aliases.py +2 -1
  19. claude_mpm/core/file_utils.py +0 -1
  20. claude_mpm/core/framework/__init__.py +38 -0
  21. claude_mpm/core/framework/formatters/__init__.py +11 -0
  22. claude_mpm/core/framework/formatters/capability_generator.py +367 -0
  23. claude_mpm/core/framework/formatters/content_formatter.py +288 -0
  24. claude_mpm/core/framework/formatters/context_generator.py +184 -0
  25. claude_mpm/core/framework/loaders/__init__.py +13 -0
  26. claude_mpm/core/framework/loaders/agent_loader.py +206 -0
  27. claude_mpm/core/framework/loaders/file_loader.py +223 -0
  28. claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
  29. claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
  30. claude_mpm/core/framework/processors/__init__.py +11 -0
  31. claude_mpm/core/framework/processors/memory_processor.py +230 -0
  32. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  33. claude_mpm/core/framework/processors/template_processor.py +244 -0
  34. claude_mpm/core/framework_loader.py +298 -1795
  35. claude_mpm/core/log_manager.py +2 -1
  36. claude_mpm/core/tool_access_control.py +1 -0
  37. claude_mpm/core/unified_agent_registry.py +2 -1
  38. claude_mpm/core/unified_paths.py +1 -0
  39. claude_mpm/experimental/cli_enhancements.py +1 -0
  40. claude_mpm/hooks/__init__.py +9 -1
  41. claude_mpm/hooks/base_hook.py +1 -0
  42. claude_mpm/hooks/instruction_reinforcement.py +1 -0
  43. claude_mpm/hooks/kuzu_memory_hook.py +359 -0
  44. claude_mpm/hooks/validation_hooks.py +1 -1
  45. claude_mpm/scripts/mpm_doctor.py +1 -0
  46. claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
  47. claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
  48. claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
  49. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
  50. claude_mpm/services/agents/management/agent_management_service.py +1 -1
  51. claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
  52. claude_mpm/services/agents/memory/memory_file_service.py +6 -2
  53. claude_mpm/services/agents/memory/memory_format_service.py +0 -1
  54. claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
  55. claude_mpm/services/async_session_logger.py +1 -1
  56. claude_mpm/services/claude_session_logger.py +1 -0
  57. claude_mpm/services/core/path_resolver.py +2 -0
  58. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  59. claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
  60. claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
  61. claude_mpm/services/diagnostics/diagnostic_runner.py +4 -0
  62. claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
  63. claude_mpm/services/event_bus/direct_relay.py +2 -1
  64. claude_mpm/services/event_bus/event_bus.py +1 -0
  65. claude_mpm/services/event_bus/relay.py +3 -2
  66. claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
  67. claude_mpm/services/infrastructure/daemon_manager.py +1 -1
  68. claude_mpm/services/mcp_config_manager.py +67 -4
  69. claude_mpm/services/mcp_gateway/core/process_pool.py +320 -0
  70. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  71. claude_mpm/services/mcp_gateway/main.py +3 -13
  72. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  73. claude_mpm/services/mcp_gateway/tools/__init__.py +14 -2
  74. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
  75. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
  76. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  77. claude_mpm/services/project/archive_manager.py +159 -96
  78. claude_mpm/services/project/documentation_manager.py +64 -45
  79. claude_mpm/services/project/enhanced_analyzer.py +132 -89
  80. claude_mpm/services/project/project_organizer.py +225 -131
  81. claude_mpm/services/response_tracker.py +1 -1
  82. claude_mpm/services/shared/__init__.py +2 -1
  83. claude_mpm/services/shared/service_factory.py +8 -5
  84. claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
  85. claude_mpm/services/unified/__init__.py +1 -1
  86. claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
  87. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
  88. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
  89. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
  90. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
  91. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
  92. claude_mpm/services/unified/config_strategies/__init__.py +175 -0
  93. claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
  94. claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
  95. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
  96. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
  97. claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
  98. claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
  99. claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
  100. claude_mpm/services/unified/deployment_strategies/base.py +24 -28
  101. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
  102. claude_mpm/services/unified/deployment_strategies/local.py +49 -34
  103. claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
  104. claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
  105. claude_mpm/services/unified/interfaces.py +0 -26
  106. claude_mpm/services/unified/migration.py +17 -40
  107. claude_mpm/services/unified/strategies.py +9 -26
  108. claude_mpm/services/unified/unified_analyzer.py +48 -44
  109. claude_mpm/services/unified/unified_config.py +21 -19
  110. claude_mpm/services/unified/unified_deployment.py +21 -26
  111. claude_mpm/storage/state_storage.py +1 -0
  112. claude_mpm/utils/agent_dependency_loader.py +18 -6
  113. claude_mpm/utils/common.py +14 -12
  114. claude_mpm/utils/database_connector.py +15 -12
  115. claude_mpm/utils/error_handler.py +1 -0
  116. claude_mpm/utils/log_cleanup.py +1 -0
  117. claude_mpm/utils/path_operations.py +1 -0
  118. claude_mpm/utils/session_logging.py +1 -1
  119. claude_mpm/utils/subprocess_utils.py +1 -0
  120. claude_mpm/validation/agent_validator.py +1 -1
  121. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
  122. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
  123. claude_mpm/cli/commands/configure_tui.py +0 -1927
  124. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  125. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  126. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
  127. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
  128. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
  129. {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 getpass
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 resource handling for packaged installations
14
- try:
15
- # Python 3.9+
16
- from importlib.resources import files
17
- except ImportError:
18
- # Python 3.8 fallback
19
- try:
20
- from importlib_resources import files
21
- except ImportError:
22
- # Final fallback for development environments
23
- files = None
24
-
25
- from ..utils.imports import safe_import
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 - using absolute imports as primary since we're at module level
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 component handles:
71
- 1. Finding the framework (claude-multiagent-pm)
72
- 2. Loading custom instructions from .claude-mpm/ directories
73
- 3. Preparing agent definitions
74
- 4. Formatting for injection
75
-
76
- Custom Instructions Loading:
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
- # Use provided container or get global container
145
- self.container = service_container or get_global_container()
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) # singleton=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
- # Resolve services from container
167
- self._cache_manager = self.container.resolve(ICacheManager)
168
- self._path_resolver = self.container.resolve(IPathResolver)
169
- self._memory_manager = self.container.resolve(IMemoryManager)
170
-
171
- # Initialize framework path using PathResolver
172
- self.framework_path = (
173
- framework_path or self._path_resolver.detect_framework_path()
174
- )
175
-
176
- # Keep TTL constants for backward compatibility
177
- # These are implementation-specific, so we use defaults if not available
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
- self.framework_content = self._load_framework_content()
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 (capabilities, deployed agents, metadata)."""
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
- def _initialize_output_style(self) -> None:
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": "", # Track source level
219
+ "workflow_instructions_level": "",
623
220
  "memory_instructions": "",
624
- "memory_instructions_level": "", # Track source level
625
- "project_workflow": "", # Deprecated, use workflow_instructions_level
626
- "project_memory": "", # Deprecated, use memory_instructions_level
627
- "actual_memories": "", # Add field for actual memories from PM_memories.md
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 file from working directory
631
- self._load_instructions_file(content)
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
- # Check if this is a packaged installation
637
- if self.framework_path == Path("__PACKAGED__"):
638
- # Load files using importlib.resources for packaged installations
639
- self._load_packaged_framework_content(content)
640
- else:
641
- # Load from filesystem for development mode
642
- # Try new consolidated PM_INSTRUCTIONS.md first, fall back to INSTRUCTIONS.md
643
- pm_instructions_path = (
644
- self.framework_path
645
- / "src"
646
- / "claude_mpm"
647
- / "agents"
648
- / "PM_INSTRUCTIONS.md"
649
- )
650
- framework_instructions_path = (
651
- self.framework_path
652
- / "src"
653
- / "claude_mpm"
654
- / "agents"
655
- / "INSTRUCTIONS.md"
656
- )
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
- # Try loading new consolidated file first
659
- if pm_instructions_path.exists():
660
- loaded_content = self._try_load_file(
661
- pm_instructions_path, "consolidated PM_INSTRUCTIONS.md"
662
- )
663
- if loaded_content:
664
- content["framework_instructions"] = loaded_content
665
- self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md")
666
- # Fall back to legacy file for backward compatibility
667
- elif framework_instructions_path.exists():
668
- loaded_content = self._try_load_file(
669
- framework_instructions_path, "framework INSTRUCTIONS.md (legacy)"
670
- )
671
- if loaded_content:
672
- content["framework_instructions"] = loaded_content
673
- self.logger.warning(
674
- "Using legacy INSTRUCTIONS.md - consider migrating to PM_INSTRUCTIONS.md"
675
- )
676
- content["loaded"] = True
677
- # Add framework version to content
678
- if self.framework_version:
679
- content["instructions_version"] = self.framework_version
680
- content["version"] = (
681
- self.framework_version
682
- ) # Update main version key
683
- # Add modification timestamp to content
684
- if self.framework_last_modified:
685
- content["instructions_last_modified"] = (
686
- self.framework_last_modified
687
- )
688
-
689
- # Load BASE_PM.md for core framework requirements
690
- base_pm_path = (
691
- self.framework_path / "src" / "claude_mpm" / "agents" / "BASE_PM.md"
692
- )
693
- if base_pm_path.exists():
694
- base_pm_content = self._try_load_file(
695
- base_pm_path, "BASE_PM framework requirements"
696
- )
697
- if base_pm_content:
698
- content["base_pm_instructions"] = base_pm_content
699
-
700
- # Load WORKFLOW.md - check for project-specific first, then system
701
- self._load_workflow_instructions(content)
702
-
703
- # Load MEMORY.md - check for project-specific first, then system
704
- self._load_memory_instructions(content)
705
-
706
- # Load actual memories from .claude-mpm/memories/PM_memories.md
238
+ # Load memories
707
239
  self._load_actual_memories(content)
708
240
 
709
- # Discover agent directories using PathResolver
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
- # Load agents from discovered directory
715
- self._load_agents_directory(content, agents_dir, templates_dir, main_dir)
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 _load_packaged_framework_content(self, content: Dict[str, Any]) -> None:
720
- """Load framework content from packaged installation using importlib.resources."""
721
- if not files:
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
- # Load WORKFLOW.md
831
- workflow_content = self._load_packaged_file_fallback(
832
- "WORKFLOW.md", resources
833
- )
834
- if workflow_content:
835
- content["workflow_instructions"] = workflow_content
836
- content["project_workflow"] = "system"
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
- # Load MEMORY.md
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
- except Exception as e:
845
- self.logger.error(
846
- f"Failed to load packaged framework content with fallback: {e}"
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
- def _load_packaged_file_fallback(self, filename: str, resources) -> Optional[str]:
850
- """Load a file from the packaged installation using importlib.resources fallback."""
851
- try:
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 _load_packaged_file(self, filename: str) -> Optional[str]:
875
- """Load a file from the packaged installation."""
876
- try:
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 _extract_metadata_from_content(self, content: str, filename: str) -> None:
892
- """Extract metadata from content string."""
893
- import re
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
- # Extract version
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
- # Extract timestamp
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
- # Import LogManager for prompt logging
915
- try:
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
- # Build framework from components
925
- instructions = self._format_full_framework()
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 content for older Claude versions
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
- if output_style_content:
1088
- instructions += "\n\n## Output Style Configuration\n"
1089
- instructions += "**Note: The following output style is injected for Claude < 1.0.83**\n\n"
1090
- instructions += output_style_content
1091
- instructions += "\n"
1092
-
1093
- # Clean up any trailing whitespace
1094
- return instructions.rstrip() + "\n"
1095
-
1096
- # Otherwise fall back to generating framework
1097
- instructions = """# Claude MPM Framework Instructions
1098
-
1099
- You are operating within the Claude Multi-Agent Project Manager (MPM) framework.
1100
-
1101
- ## Core Role
1102
- You are a multi-agent orchestrator. Your primary responsibilities are:
1103
- - Delegate all implementation work to specialized agents via Task Tool
1104
- - Coordinate multi-agent workflows and cross-agent collaboration
1105
- - Extract and track TODO/BUG/FEATURE items for ticket creation
1106
- - Maintain project visibility and strategic oversight
1107
- - NEVER perform direct implementation work yourself
1108
-
1109
- """
1110
-
1111
- # Note: We don't add working directory CLAUDE.md here since Claude Code
1112
- # already picks it up automatically. This prevents duplication.
1113
-
1114
- # Add agent definitions
1115
- if self.framework_content["agents"]:
1116
- instructions += "## Available Agents\n\n"
1117
- instructions += "You have the following specialized agents available for delegation:\n\n"
1118
-
1119
- # List agents with brief descriptions and correct IDs
1120
- agent_list = []
1121
- for agent_name in sorted(self.framework_content["agents"].keys()):
1122
- # Use the actual agent_name as the ID (it's the filename stem)
1123
- agent_id = agent_name
1124
- clean_name = agent_name.replace("-", " ").replace("_", " ").title()
1125
- if (
1126
- "engineer" in agent_name.lower()
1127
- and "data" not in agent_name.lower()
1128
- ):
1129
- agent_list.append(
1130
- f"- **Engineer Agent** (`{agent_id}`): Code implementation and development"
1131
- )
1132
- elif "qa" in agent_name.lower():
1133
- agent_list.append(
1134
- f"- **QA Agent** (`{agent_id}`): Testing and quality assurance"
1135
- )
1136
- elif "documentation" in agent_name.lower():
1137
- agent_list.append(
1138
- f"- **Documentation Agent** (`{agent_id}`): Documentation creation and maintenance"
1139
- )
1140
- elif "research" in agent_name.lower():
1141
- agent_list.append(
1142
- f"- **Research Agent** (`{agent_id}`): Investigation and analysis"
1143
- )
1144
- elif "security" in agent_name.lower():
1145
- agent_list.append(
1146
- f"- **Security Agent** (`{agent_id}`): Security analysis and protection"
1147
- )
1148
- elif "version" in agent_name.lower():
1149
- agent_list.append(
1150
- f"- **Version Control Agent** (`{agent_id}`): Git operations and version management"
1151
- )
1152
- elif "ops" in agent_name.lower():
1153
- agent_list.append(
1154
- f"- **Ops Agent** (`{agent_id}`): Deployment and operations"
1155
- )
1156
- elif "data" in agent_name.lower():
1157
- agent_list.append(
1158
- f"- **Data Engineer Agent** (`{agent_id}`): Data management and AI API integration"
1159
- )
1160
- else:
1161
- agent_list.append(
1162
- f"- **{clean_name}** (`{agent_id}`): Available for specialized tasks"
1163
- )
346
+ self.logger.info("Injecting output style content for Claude < 1.0.83")
1164
347
 
1165
- instructions += "\n".join(agent_list) + "\n\n"
1166
-
1167
- # Add full agent details
1168
- instructions += "### Agent Details\n\n"
1169
- for agent_name, agent_content in sorted(
1170
- self.framework_content["agents"].items()
1171
- ):
1172
- instructions += f"#### {agent_name.replace('-', ' ').title()}\n"
1173
- instructions += agent_content + "\n\n"
1174
-
1175
- # Add orchestration principles
1176
- instructions += """
1177
- ## Orchestration Principles
1178
- 1. **Always Delegate**: Never perform direct work - use Task Tool for all implementation
1179
- 2. **Comprehensive Context**: Provide rich, filtered context to each agent
1180
- 3. **Track Everything**: Extract all TODO/BUG/FEATURE items systematically
1181
- 4. **Cross-Agent Coordination**: Orchestrate workflows spanning multiple agents
1182
- 5. **Results Integration**: Actively receive and integrate agent results
1183
-
1184
- ## Task Tool Format
1185
- ```
1186
- **[Agent Name]**: [Clear task description with deliverables]
1187
-
1188
- TEMPORAL CONTEXT: Today is [date]. Apply date awareness to [specific considerations].
1189
-
1190
- **Task**: [Detailed task breakdown]
1191
- 1. [Specific action item 1]
1192
- 2. [Specific action item 2]
1193
- 3. [Specific action item 3]
1194
-
1195
- **Context**: [Comprehensive filtered context for this agent]
1196
- **Authority**: [Agent's decision-making scope]
1197
- **Expected Results**: [Specific deliverables needed]
1198
- **Integration**: [How results integrate with other work]
1199
- ```
1200
-
1201
- ## Ticket Extraction Patterns
1202
- Extract tickets from these patterns:
1203
- - TODO: [description] → TODO ticket
1204
- - BUG: [description] → BUG ticket
1205
- - FEATURE: [description] → FEATURE ticket
1206
- - ISSUE: [description] → ISSUE ticket
1207
- - FIXME: [description] → BUG ticket
1208
-
1209
- ---
1210
- """
1211
-
1212
- return instructions
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
- def _generate_agent_capabilities_section(self) -> str:
1215
- """Generate dynamic agent capabilities section from deployed agents.
1216
- Uses caching to avoid repeated file I/O and parsing operations.
1217
-
1218
- Now includes support for local JSON templates with proper priority:
1219
- 1. Project local agents (.claude-mpm/agents/*.json) - highest priority
1220
- 2. Deployed project agents (.claude/agents/*.md)
1221
- 3. User local agents (~/.claude-mpm/agents/*.json)
1222
- 4. Deployed user agents (~/.claude/agents/*.md)
1223
- 5. System agents - lowest priority
1224
- """
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
- # Try to get from cache first
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
- # Will be used for updating cache later
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
- from pathlib import Path
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
- # Read directly from deployed agents in .claude/agents/
1244
- # Check multiple locations for deployed agents
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", # Project-specific agents
1248
- Path.home() / ".claude" / "agents", # User's system agents
381
+ Path.cwd() / ".claude" / "agents",
382
+ Path.home() / ".claude" / "agents",
1249
383
  ]
1250
384
 
1251
- # Collect agents from all directories with proper precedence
1252
- # Local agents override deployed agents with the same name
1253
- # Project agents override user agents with the same name
1254
- all_agents = {} # key: agent_id, value: (agent_data, priority)
1255
-
1256
- # Add local agents first (highest priority)
1257
- for agent_id, agent_data in local_agents.items():
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
- # Build capabilities section
1313
- section = "\n\n## Available Agent Capabilities\n\n"
1314
-
1315
- # Extract just the agent data (drop priority info) and sort
1316
- deployed_agents = [agent_data for agent_data, _ in all_agents.values()]
1317
-
1318
- if not deployed_agents:
1319
- result = self._get_fallback_capabilities()
1320
- # Cache the fallback result
1321
- self._cache_manager.set_capabilities(result)
1322
- return result
1323
-
1324
- # Sort agents alphabetically by ID
1325
- deployed_agents.sort(key=lambda x: x["id"])
1326
-
1327
- # Display all agents with their rich descriptions
1328
- for agent in deployed_agents:
1329
- # Clean up display name - handle common acronyms
1330
- display_name = agent["display_name"]
1331
- display_name = (
1332
- display_name.replace("Qa ", "QA ")
1333
- .replace("Ui ", "UI ")
1334
- .replace("Api ", "API ")
1335
- )
1336
- if display_name.lower() == "qa agent":
1337
- display_name = "QA Agent"
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
- # Add summary
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 dynamic agent capabilities: {e}")
1419
- result = self._get_fallback_capabilities()
1420
- # Cache even the fallback result
1421
- self._agent_capabilities_cache = result
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
- def _generate_temporal_user_context(self) -> str:
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
- # Get current datetime with timezone awareness
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
- context_lines.append(
1465
- f"**Current DateTime**: {date_str} {time_str} {tz_info}\n"
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
- except Exception as e:
1470
- # Fallback to basic date if enhanced datetime fails
1471
- self.logger.debug(f"Error generating enhanced datetime context: {e}")
1472
- context_lines.append(
1473
- f"**Today's Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}\n"
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
- try:
1477
- # Get user information with safe fallbacks
1478
- username = None
1479
-
1480
- # Try multiple methods to get username
1481
- methods = [
1482
- lambda: os.environ.get("USER"),
1483
- lambda: os.environ.get("USERNAME"), # Windows fallback
1484
- lambda: getpass.getuser(),
1485
- ]
1486
-
1487
- for method in methods:
1488
- try:
1489
- username = method()
1490
- if username:
1491
- break
1492
- except Exception:
1493
- continue
1494
-
1495
- if username:
1496
- context_lines.append(f"**User**: {username}\n")
1497
-
1498
- # Add home directory if available
1499
- try:
1500
- home_dir = os.path.expanduser("~")
1501
- if home_dir and home_dir != "~":
1502
- context_lines.append(f"**Home Directory**: {home_dir}\n")
1503
- except Exception:
1504
- pass
1505
-
1506
- except Exception as e:
1507
- # User detection is optional, don't fail
1508
- self.logger.debug(f"Could not detect user information: {e}")
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
- import yaml
1594
-
1595
- with open(agent_file) as f:
1596
- content = f.read()
1597
-
1598
- # Default values
1599
- agent_data = {
1600
- "id": agent_file.stem,
1601
- "display_name": agent_file.stem.replace("_", " ")
1602
- .replace("-", " ")
1603
- .title(),
1604
- "description": "Specialized agent",
1605
- }
1606
-
1607
- # Extract YAML frontmatter if present
1608
- if content.startswith("---"):
1609
- end_marker = content.find("---", 3)
1610
- if end_marker > 0:
1611
- frontmatter = content[3:end_marker]
1612
- metadata = yaml.safe_load(frontmatter)
1613
- if metadata:
1614
- # Use name as ID for Task tool
1615
- agent_data["id"] = metadata.get("name", agent_data["id"])
1616
- agent_data["display_name"] = (
1617
- metadata.get("name", agent_data["display_name"])
1618
- .replace("-", " ")
1619
- .title()
1620
- )
1621
-
1622
- # Copy all metadata fields directly
1623
- for key, value in metadata.items():
1624
- if key not in ["name"]: # Skip already processed fields
1625
- agent_data[key] = value
1626
-
1627
- # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
1628
- # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
1629
-
1630
- # Try to load routing metadata from JSON template if not in YAML frontmatter
1631
- if "routing" not in agent_data:
1632
- routing_data = self._load_routing_from_template(agent_file.stem)
1633
- if routing_data:
1634
- agent_data["routing"] = routing_data
1635
-
1636
- # Try to load memory routing metadata from JSON template if not in YAML frontmatter
1637
- if "memory_routing" not in agent_data:
1638
- memory_routing_data = self._load_memory_routing_from_template(
1639
- agent_file.stem
1640
- )
1641
- if memory_routing_data:
1642
- agent_data["memory_routing"] = memory_routing_data
1643
-
1644
- # Cache the parsed metadata
1645
- self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
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.debug(f"Could not parse metadata from {agent_file}: {e}")
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
- except Exception as e:
1723
- self.logger.debug(
1724
- f"Could not load memory routing from template for {agent_name}: {e}"
1725
- )
1726
- return None
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
- def _discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
1729
- """Discover local JSON agent templates from .claude-mpm/agents/ directories.
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
- Returns:
1732
- Dictionary mapping agent IDs to agent metadata
1733
- """
1734
- import json
1735
- from pathlib import Path
1736
-
1737
- local_agents = {}
1738
-
1739
- # Check for local JSON templates in priority order
1740
- template_dirs = [
1741
- Path.cwd()
1742
- / ".claude-mpm"
1743
- / "agents", # Project local agents (highest priority)
1744
- Path.home() / ".claude-mpm" / "agents", # User local agents
1745
- ]
1746
-
1747
- for priority, template_dir in enumerate(template_dirs):
1748
- if not template_dir.exists():
1749
- continue
1750
-
1751
- for json_file in template_dir.glob("*.json"):
1752
- try:
1753
- with open(json_file) as f:
1754
- template_data = json.load(f)
1755
-
1756
- # Extract agent metadata
1757
- agent_id = template_data.get("agent_id", json_file.stem)
1758
-
1759
- # Skip if already found at higher priority
1760
- if agent_id in local_agents:
1761
- continue
1762
-
1763
- # Extract metadata
1764
- metadata = template_data.get("metadata", {})
1765
-
1766
- # Build agent data in expected format
1767
- agent_data = {
1768
- "id": agent_id,
1769
- "display_name": metadata.get(
1770
- "name", agent_id.replace("_", " ").title()
1771
- ),
1772
- "description": metadata.get(
1773
- "description", f"Local {agent_id} agent"
1774
- ),
1775
- "tools": self._extract_tools_from_template(template_data),
1776
- "is_local": True,
1777
- "tier": "project" if priority == 0 else "user",
1778
- "author": template_data.get("author", "local"),
1779
- "version": template_data.get("agent_version", "1.0.0"),
1780
- }
1781
-
1782
- local_agents[agent_id] = agent_data
1783
- self.logger.debug(
1784
- f"Discovered local JSON agent: {agent_id} from {template_dir}"
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
- except Exception as e:
1788
- self.logger.warning(
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
- return local_agents
1793
-
1794
- def _extract_tools_from_template(self, template_data: Dict[str, Any]) -> str:
1795
- """Extract tools string from template data.
1796
-
1797
- Args:
1798
- template_data: JSON template data
1799
-
1800
- Returns:
1801
- Tools string for display
1802
- """
1803
- capabilities = template_data.get("capabilities", {})
1804
- tools = capabilities.get("tools", "*")
1805
-
1806
- if tools == "*":
1807
- return "All Tools"
1808
- if isinstance(tools, list):
1809
- return ", ".join(tools) if tools else "Standard Tools"
1810
- if isinstance(tools, str):
1811
- if "," in tools:
1812
- return tools
1813
- return tools
1814
- return "Standard Tools"
1815
-
1816
- def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
1817
- """Load routing metadata from agent JSON template.
1818
-
1819
- Args:
1820
- agent_name: Name of the agent (stem of the file)
472
+ # === Logging Methods ===
1821
473
 
1822
- Returns:
1823
- Dictionary with routing metadata or None if not found
1824
- """
474
+ def _log_system_prompt(self) -> None:
475
+ """Log the system prompt if LogManager is available."""
1825
476
  try:
1826
- import json
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
- Creates a mapping of task types to appropriate agents based on their
1888
- descriptions and capabilities.
1889
- """
1890
- guide = ""
479
+ log_manager = get_log_manager()
480
+ except ImportError:
481
+ return
1891
482
 
1892
- # Build selection mapping based on deployed agents
1893
- selection_map = {}
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
- for agent in deployed_agents:
1896
- agent_id = agent["id"]
1897
- desc_lower = agent["description"].lower()
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
- # Map task types to agents based on their descriptions
1900
- if "implementation" in desc_lower or (
1901
- "engineer" in agent_id and "data" not in agent_id
1902
- ):
1903
- selection_map["Implementation tasks"] = (
1904
- f"{agent['display_name']} (`{agent_id}`)"
1905
- )
1906
- if "codebase analysis" in desc_lower or "research" in agent_id:
1907
- selection_map["Codebase analysis"] = (
1908
- f"{agent['display_name']} (`{agent_id}`)"
1909
- )
1910
- if "testing" in desc_lower or "qa" in agent_id:
1911
- selection_map["Testing/quality"] = (
1912
- f"{agent['display_name']} (`{agent_id}`)"
1913
- )
1914
- if "documentation" in desc_lower:
1915
- selection_map["Documentation"] = (
1916
- f"{agent['display_name']} (`{agent_id}`)"
1917
- )
1918
- if "security" in desc_lower or "sast" in desc_lower:
1919
- selection_map["Security operations"] = (
1920
- f"{agent['display_name']} (`{agent_id}`)"
1921
- )
1922
- if (
1923
- "deployment" in desc_lower
1924
- or "infrastructure" in desc_lower
1925
- or "ops" in agent_id
1926
- ):
1927
- selection_map["Deployment/infrastructure"] = (
1928
- f"{agent['display_name']} (`{agent_id}`)"
1929
- )
1930
- if "data" in desc_lower and (
1931
- "pipeline" in desc_lower or "etl" in desc_lower
1932
- ):
1933
- selection_map["Data pipeline/ETL"] = (
1934
- f"{agent['display_name']} (`{agent_id}`)"
506
+ if loop.is_running():
507
+ asyncio.create_task(
508
+ log_manager.log_prompt("system_prompt", instructions, metadata)
1935
509
  )
1936
- if "git" in desc_lower or "version control" in desc_lower:
1937
- selection_map["Version control"] = (
1938
- f"{agent['display_name']} (`{agent_id}`)"
1939
- )
1940
- if "ticket" in desc_lower or "epic" in desc_lower:
1941
- selection_map["Ticket/issue management"] = (
1942
- f"{agent['display_name']} (`{agent_id}`)"
1943
- )
1944
- if "browser" in desc_lower or "e2e" in desc_lower:
1945
- selection_map["Browser/E2E testing"] = (
1946
- f"{agent['display_name']} (`{agent_id}`)"
1947
- )
1948
- if "frontend" in desc_lower or "ui" in desc_lower or "html" in desc_lower:
1949
- selection_map["Frontend/UI development"] = (
1950
- f"{agent['display_name']} (`{agent_id}`)"
510
+ else:
511
+ loop.run_until_complete(
512
+ log_manager.log_prompt("system_prompt", instructions, metadata)
1951
513
  )
1952
514
 
1953
- # Always include PM questions
1954
- selection_map["PM questions"] = "Answer directly (only exception)"
1955
-
1956
- # Format the selection guide
1957
- for task_type, agent_info in selection_map.items():
1958
- guide += f"- **{task_type}** → {agent_info}\n"
1959
-
1960
- return guide
1961
-
1962
- def _get_fallback_capabilities(self) -> str:
1963
- """Return fallback capabilities when dynamic discovery fails."""
1964
- return """
1965
-
1966
- ## Available Agent Capabilities
1967
-
1968
- You have the following specialized agents available for delegation:
1969
-
1970
- - **Engineer** (`engineer`): Code implementation and development
1971
- - **Research** (`research-agent`): Investigation and analysis
1972
- - **QA** (`qa-agent`): Testing and quality assurance
1973
- - **Documentation** (`documentation-agent`): Documentation creation and maintenance
1974
- - **Security** (`security-agent`): Security analysis and protection
1975
- - **Data Engineer** (`data-engineer`): Data management and pipelines
1976
- - **Ops** (`ops-agent`): Deployment and operations
1977
- - **Version Control** (`version-control`): Git operations and version management
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
- **IMPORTANT**: Use the exact agent ID in parentheses when delegating tasks.
1980
- """
519
+ # === Agent Registry Methods (backward compatibility) ===
1981
520
 
1982
- def _format_minimal_framework(self) -> str:
1983
- """Format minimal framework instructions when full framework not available."""
1984
- return """
1985
- # Claude PM Framework Instructions
1986
-
1987
- You are operating within a Claude PM Framework deployment.
1988
-
1989
- ## Role
1990
- You are a multi-agent orchestrator. Your primary responsibilities:
1991
- - Delegate tasks to specialized agents via Task Tool
1992
- - Coordinate multi-agent workflows
1993
- - Extract TODO/BUG/FEATURE items for ticket creation
1994
- - NEVER perform direct implementation work
1995
-
1996
- ## Core Agents
1997
- - Documentation Agent - Documentation tasks
1998
- - Engineer Agent - Code implementation
1999
- - QA Agent - Testing and validation
2000
- - Research Agent - Investigation and analysis
2001
- - Version Control Agent - Git operations
2002
-
2003
- ## Important Rules
2004
- 1. Always delegate work via Task Tool
2005
- 2. Provide comprehensive context to agents
2006
- 3. Track all TODO/BUG/FEATURE items
2007
- 4. Maintain project visibility
2008
-
2009
- ---
2010
- """
2011
-
2012
- def get_agent_list(self) -> list:
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, list]:
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()