claude-mpm 4.3.22__py3-none-any.whl → 4.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/cli/commands/configure.py +2 -29
  4. claude_mpm/cli/commands/doctor.py +2 -2
  5. claude_mpm/cli/commands/mpm_init.py +3 -3
  6. claude_mpm/cli/parsers/configure_parser.py +4 -15
  7. claude_mpm/core/framework/__init__.py +38 -0
  8. claude_mpm/core/framework/formatters/__init__.py +11 -0
  9. claude_mpm/core/framework/formatters/capability_generator.py +356 -0
  10. claude_mpm/core/framework/formatters/content_formatter.py +283 -0
  11. claude_mpm/core/framework/formatters/context_generator.py +180 -0
  12. claude_mpm/core/framework/loaders/__init__.py +13 -0
  13. claude_mpm/core/framework/loaders/agent_loader.py +202 -0
  14. claude_mpm/core/framework/loaders/file_loader.py +213 -0
  15. claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
  16. claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
  17. claude_mpm/core/framework/processors/__init__.py +11 -0
  18. claude_mpm/core/framework/processors/memory_processor.py +222 -0
  19. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  20. claude_mpm/core/framework/processors/template_processor.py +238 -0
  21. claude_mpm/core/framework_loader.py +277 -1798
  22. claude_mpm/hooks/__init__.py +9 -1
  23. claude_mpm/hooks/kuzu_memory_hook.py +352 -0
  24. claude_mpm/hooks/memory_integration_hook.py +1 -1
  25. claude_mpm/services/agents/memory/content_manager.py +5 -2
  26. claude_mpm/services/agents/memory/memory_file_service.py +1 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
  28. claude_mpm/services/core/path_resolver.py +1 -0
  29. claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
  30. claude_mpm/services/mcp_config_manager.py +67 -4
  31. claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
  32. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  33. claude_mpm/services/mcp_gateway/main.py +3 -13
  34. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  35. claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
  36. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
  37. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
  38. claude_mpm/services/shared/__init__.py +2 -1
  39. claude_mpm/services/shared/service_factory.py +8 -5
  40. claude_mpm/services/unified/__init__.py +65 -0
  41. claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
  42. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
  43. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
  44. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
  45. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
  46. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
  47. claude_mpm/services/unified/config_strategies/__init__.py +190 -0
  48. claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
  49. claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
  50. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
  51. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
  52. claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
  53. claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
  54. claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
  55. claude_mpm/services/unified/deployment_strategies/base.py +557 -0
  56. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
  57. claude_mpm/services/unified/deployment_strategies/local.py +594 -0
  58. claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
  59. claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
  60. claude_mpm/services/unified/interfaces.py +499 -0
  61. claude_mpm/services/unified/migration.py +532 -0
  62. claude_mpm/services/unified/strategies.py +551 -0
  63. claude_mpm/services/unified/unified_analyzer.py +534 -0
  64. claude_mpm/services/unified/unified_config.py +688 -0
  65. claude_mpm/services/unified/unified_deployment.py +470 -0
  66. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
  67. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
  68. claude_mpm/cli/commands/configure_tui.py +0 -1927
  69. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  70. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  71. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
  72. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
  73. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
  74. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,28 @@
1
- """Framework loader for Claude MPM."""
1
+ """Framework loader for Claude MPM - Refactored modular version."""
2
2
 
3
- import 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
+ from claude_mpm.core.logging_utils import get_logger
9
+ from claude_mpm.utils.imports import safe_import
10
+
11
+ # Import framework components
12
+ from claude_mpm.core.framework import (
13
+ AgentLoader,
14
+ CapabilityGenerator,
15
+ ContentFormatter,
16
+ ContextGenerator,
17
+ FileLoader,
18
+ InstructionLoader,
19
+ MemoryProcessor,
20
+ MetadataProcessor,
21
+ PackagedLoader,
22
+ TemplateProcessor,
23
+ )
26
24
 
27
- # Import with fallback support - using absolute imports as primary since we're at module level
28
- get_logger = safe_import("claude_mpm.core.logger", "core.logger", ["get_logger"])
25
+ # Import with fallback support
29
26
  AgentRegistryAdapter = safe_import(
30
27
  "claude_mpm.core.agent_registry", "core.agent_registry", ["AgentRegistryAdapter"]
31
28
  )
@@ -67,43 +64,13 @@ class FrameworkLoader:
67
64
  """
68
65
  Load and prepare framework instructions for injection.
69
66
 
70
- This 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
67
+ This refactored version uses modular components for better maintainability
68
+ and testability while maintaining backward compatibility.
69
+
70
+ Components:
71
+ - Loaders: Handle file I/O and resource loading
72
+ - Formatters: Generate and format content sections
73
+ - Processors: Process metadata, templates, and memories
107
74
  """
108
75
 
109
76
  def __init__(
@@ -114,7 +81,7 @@ class FrameworkLoader:
114
81
  config: Optional[Dict[str, Any]] = None,
115
82
  ):
116
83
  """
117
- Initialize framework loader.
84
+ Initialize framework loader with modular components.
118
85
 
119
86
  Args:
120
87
  framework_path: Explicit path to framework (auto-detected if None)
@@ -129,6 +96,37 @@ class FrameworkLoader:
129
96
  self.config = config or {}
130
97
 
131
98
  # Validate API keys on startup (before any other initialization)
99
+ self._validate_api_keys()
100
+
101
+ # Initialize service container
102
+ self.container = service_container or get_global_container()
103
+ self._register_services()
104
+
105
+ # Resolve services from container
106
+ self._cache_manager = self.container.resolve(ICacheManager)
107
+ self._path_resolver = self.container.resolve(IPathResolver)
108
+ self._memory_manager = self.container.resolve(IMemoryManager)
109
+
110
+ # Initialize framework path
111
+ self.framework_path = framework_path or self._path_resolver.detect_framework_path()
112
+
113
+ # Initialize modular components
114
+ self._init_components()
115
+
116
+ # Keep cache TTL constants for backward compatibility
117
+ self._init_cache_ttl()
118
+
119
+ # Load framework content
120
+ self.framework_content = self._load_framework_content()
121
+
122
+ # Initialize agent registry
123
+ self.agent_registry = AgentRegistryAdapter(self.framework_path)
124
+
125
+ # Output style manager (deferred initialization)
126
+ self.output_style_manager = None
127
+
128
+ def _validate_api_keys(self) -> None:
129
+ """Validate API keys if enabled in config."""
132
130
  if self.config.get("validate_api_keys", True):
133
131
  try:
134
132
  self.logger.info("Validating configured API keys...")
@@ -141,21 +139,17 @@ class FrameworkLoader:
141
139
  self.logger.error(f"❌ Unexpected error during API validation: {e}")
142
140
  raise
143
141
 
144
- # Use provided container or get global container
145
- self.container = service_container or get_global_container()
146
-
147
- # Register services if not already registered
142
+ def _register_services(self) -> None:
143
+ """Register services in the container if not already registered."""
148
144
  if not self.container.is_registered(ICacheManager):
149
- self.container.register(ICacheManager, CacheManager, True) # singleton=True
145
+ self.container.register(ICacheManager, CacheManager, True)
150
146
 
151
147
  if not self.container.is_registered(IPathResolver):
152
- # PathResolver depends on CacheManager, so resolve it first
153
148
  cache_manager = self.container.resolve(ICacheManager)
154
149
  path_resolver = PathResolver(cache_manager=cache_manager)
155
150
  self.container.register_instance(IPathResolver, path_resolver)
156
151
 
157
152
  if not self.container.is_registered(IMemoryManager):
158
- # MemoryManager depends on both CacheManager and PathResolver
159
153
  cache_manager = self.container.resolve(ICacheManager)
160
154
  path_resolver = self.container.resolve(IPathResolver)
161
155
  memory_manager = MemoryManager(
@@ -163,18 +157,26 @@ class FrameworkLoader:
163
157
  )
164
158
  self.container.register_instance(IMemoryManager, memory_manager)
165
159
 
166
- # 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
160
+ def _init_components(self) -> None:
161
+ """Initialize modular components."""
162
+ # Loaders
163
+ self.file_loader = FileLoader()
164
+ self.packaged_loader = PackagedLoader()
165
+ self.instruction_loader = InstructionLoader(self.framework_path)
166
+ self.agent_loader = AgentLoader(self.framework_path)
167
+
168
+ # Formatters
169
+ self.content_formatter = ContentFormatter()
170
+ self.capability_generator = CapabilityGenerator()
171
+ self.context_generator = ContextGenerator()
172
+
173
+ # Processors
174
+ self.metadata_processor = MetadataProcessor()
175
+ self.template_processor = TemplateProcessor(self.framework_path)
176
+ self.memory_processor = MemoryProcessor()
177
+
178
+ def _init_cache_ttl(self) -> None:
179
+ """Initialize cache TTL constants for backward compatibility."""
178
180
  if hasattr(self._cache_manager, "capabilities_ttl"):
179
181
  self.CAPABILITIES_CACHE_TTL = self._cache_manager.capabilities_ttl
180
182
  self.DEPLOYED_AGENTS_CACHE_TTL = self._cache_manager.deployed_agents_ttl
@@ -187,430 +189,24 @@ class FrameworkLoader:
187
189
  self.METADATA_CACHE_TTL = 60
188
190
  self.MEMORIES_CACHE_TTL = 60
189
191
 
190
- 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
192
+ # === Cache Management Methods (backward compatibility) ===
198
193
 
199
194
  def clear_all_caches(self) -> None:
200
195
  """Clear all caches to force reload on next access."""
201
196
  self._cache_manager.clear_all()
202
197
 
203
198
  def clear_agent_caches(self) -> None:
204
- """Clear agent-related caches (capabilities, deployed agents, metadata)."""
199
+ """Clear agent-related caches."""
205
200
  self._cache_manager.clear_agent_caches()
206
201
 
207
202
  def clear_memory_caches(self) -> None:
208
203
  """Clear memory-related caches."""
209
204
  self._cache_manager.clear_memory_caches()
210
205
 
211
- 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)
206
+ # === Content Loading Methods ===
611
207
 
612
208
  def _load_framework_content(self) -> Dict[str, Any]:
613
- """Load framework content."""
209
+ """Load framework content using modular components."""
614
210
  content = {
615
211
  "claude_md": "",
616
212
  "agents": {},
@@ -619,290 +215,98 @@ class FrameworkLoader:
619
215
  "working_claude_md": "",
620
216
  "framework_instructions": "",
621
217
  "workflow_instructions": "",
622
- "workflow_instructions_level": "", # Track source level
218
+ "workflow_instructions_level": "",
623
219
  "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
220
+ "memory_instructions_level": "",
221
+ "project_workflow": "", # Deprecated
222
+ "project_memory": "", # Deprecated
223
+ "actual_memories": "",
224
+ "agent_memories": {},
628
225
  }
629
226
 
630
- # Load instructions file from working directory
631
- self._load_instructions_file(content)
227
+ # Load all instructions
228
+ self.instruction_loader.load_all_instructions(content)
632
229
 
633
- if not self.framework_path:
634
- return content
230
+ # Transfer metadata from loaders
231
+ if self.file_loader.framework_version:
232
+ self.framework_version = self.file_loader.framework_version
233
+ content["version"] = self.framework_version
234
+ if self.file_loader.framework_last_modified:
235
+ self.framework_last_modified = self.file_loader.framework_last_modified
635
236
 
636
- # 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
- )
657
-
658
- # Try loading new consolidated file first
659
- if pm_instructions_path.exists():
660
- loaded_content = self._try_load_file(
661
- pm_instructions_path, "consolidated PM_INSTRUCTIONS.md"
662
- )
663
- if loaded_content:
664
- content["framework_instructions"] = loaded_content
665
- self.logger.info("Loaded consolidated PM_INSTRUCTIONS.md")
666
- # Fall back to legacy file for backward compatibility
667
- elif framework_instructions_path.exists():
668
- loaded_content = self._try_load_file(
669
- framework_instructions_path, "framework INSTRUCTIONS.md (legacy)"
670
- )
671
- if loaded_content:
672
- content["framework_instructions"] = loaded_content
673
- self.logger.warning(
674
- "Using legacy INSTRUCTIONS.md - consider migrating to PM_INSTRUCTIONS.md"
675
- )
676
- content["loaded"] = True
677
- # Add framework version to content
678
- if self.framework_version:
679
- content["instructions_version"] = self.framework_version
680
- content["version"] = (
681
- self.framework_version
682
- ) # Update main version key
683
- # Add modification timestamp to content
684
- if self.framework_last_modified:
685
- content["instructions_last_modified"] = (
686
- self.framework_last_modified
687
- )
688
-
689
- # Load BASE_PM.md for core framework requirements
690
- base_pm_path = (
691
- self.framework_path / "src" / "claude_mpm" / "agents" / "BASE_PM.md"
692
- )
693
- if base_pm_path.exists():
694
- base_pm_content = self._try_load_file(
695
- base_pm_path, "BASE_PM framework requirements"
696
- )
697
- if base_pm_content:
698
- content["base_pm_instructions"] = base_pm_content
699
-
700
- # Load WORKFLOW.md - check for project-specific first, then system
701
- self._load_workflow_instructions(content)
702
-
703
- # Load MEMORY.md - check for project-specific first, then system
704
- self._load_memory_instructions(content)
705
-
706
- # Load actual memories from .claude-mpm/memories/PM_memories.md
237
+ # Load memories
707
238
  self._load_actual_memories(content)
708
239
 
709
- # Discover agent directories using PathResolver
240
+ # Discover and load agents
710
241
  agents_dir, templates_dir, main_dir = self._path_resolver.discover_agent_paths(
711
242
  agents_dir=self.agents_dir, framework_path=self.framework_path
712
243
  )
713
-
714
- # Load agents from discovered directory
715
- self._load_agents_directory(content, agents_dir, templates_dir, main_dir)
244
+ agents = self.agent_loader.load_agents_directory(agents_dir, templates_dir, main_dir)
245
+ if agents:
246
+ content["agents"] = agents
247
+ content["loaded"] = True
716
248
 
717
249
  return content
718
250
 
719
- def _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}")
251
+ def _load_actual_memories(self, content: Dict[str, Any]) -> None:
252
+ """Load actual memories using the MemoryManager service."""
253
+ memories = self._memory_manager.load_memories()
787
254
 
788
- 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
829
-
830
- # Load WORKFLOW.md
831
- workflow_content = self._load_packaged_file_fallback(
832
- "WORKFLOW.md", resources
833
- )
834
- if workflow_content:
835
- content["workflow_instructions"] = workflow_content
836
- content["project_workflow"] = "system"
255
+ if "actual_memories" in memories:
256
+ content["actual_memories"] = memories["actual_memories"]
257
+ if "agent_memories" in memories:
258
+ content["agent_memories"] = memories["agent_memories"]
837
259
 
838
- # 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"
260
+ # === Agent Discovery Methods ===
843
261
 
844
- except Exception as e:
845
- self.logger.error(
846
- f"Failed to load packaged framework content with fallback: {e}"
847
- )
262
+ def _get_deployed_agents(self) -> Set[str]:
263
+ """Get deployed agents with caching."""
264
+ cached = self._cache_manager.get_deployed_agents()
265
+ if cached is not None:
266
+ return cached
848
267
 
849
- 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
268
+ deployed = self.agent_loader.get_deployed_agents()
269
+ self._cache_manager.set_deployed_agents(deployed)
270
+ return deployed
873
271
 
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
272
+ def _discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
273
+ """Discover local JSON agent templates."""
274
+ return self.agent_loader.discover_local_json_templates()
890
275
 
891
- def _extract_metadata_from_content(self, content: str, filename: str) -> None:
892
- """Extract metadata from content string."""
893
- import re
276
+ def _parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
277
+ """Parse agent metadata with caching."""
278
+ cache_key = str(agent_file)
279
+ file_mtime = agent_file.stat().st_mtime
280
+
281
+ # Try cache first
282
+ cached_result = self._cache_manager.get_agent_metadata(cache_key)
283
+ if cached_result is not None:
284
+ cached_data, cached_mtime = cached_result
285
+ if cached_mtime == file_mtime:
286
+ self.logger.debug(f"Using cached metadata for {agent_file.name}")
287
+ return cached_data
288
+
289
+ # Cache miss - parse the file
290
+ agent_data = self.metadata_processor.parse_agent_metadata(agent_file)
291
+
292
+ # Add routing information if not present
293
+ if agent_data and "routing" not in agent_data:
294
+ template_data = self.template_processor.load_template(agent_file.stem)
295
+ if template_data:
296
+ routing = self.template_processor.extract_routing(template_data)
297
+ if routing:
298
+ agent_data["routing"] = routing
299
+ memory_routing = self.template_processor.extract_memory_routing(template_data)
300
+ if memory_routing:
301
+ agent_data["memory_routing"] = memory_routing
302
+
303
+ # Cache the result
304
+ if agent_data:
305
+ self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
894
306
 
895
- # 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}")
307
+ return agent_data
900
308
 
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}")
309
+ # === Framework Instructions Generation ===
906
310
 
907
311
  def get_framework_instructions(self) -> str:
908
312
  """
@@ -911,1128 +315,203 @@ class FrameworkLoader:
911
315
  Returns:
912
316
  Complete framework instructions ready for injection
913
317
  """
914
- # 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
318
+ # Log the system prompt if needed
319
+ self._log_system_prompt()
921
320
 
922
321
  # Generate the instructions
923
322
  if self.framework_content["loaded"]:
924
- # Build framework from components
925
- instructions = self._format_full_framework()
323
+ return self._format_full_framework()
926
324
  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")
325
+ return self._format_minimal_framework()
984
326
 
985
327
  def _format_full_framework(self) -> str:
986
- """Format full framework instructions."""
987
-
988
- # Initialize output style manager on first use (ensures content is loaded)
328
+ """Format full framework instructions using modular components."""
329
+ # Initialize output style manager on first use
989
330
  if self.output_style_manager is None:
990
331
  self._initialize_output_style()
991
332
 
992
- # Check if we need to inject output style content for older Claude versions
333
+ # Check if we need to inject output style
993
334
  inject_output_style = False
335
+ output_style_content = None
994
336
  if self.output_style_manager:
995
337
  inject_output_style = self.output_style_manager.should_inject_content()
996
338
  if inject_output_style:
997
- self.logger.info(
998
- "Injecting output style content into instructions for Claude < 1.0.83"
339
+ output_style_content = self.output_style_manager.get_injectable_content(
340
+ framework_loader=self
999
341
  )
342
+ self.logger.info("Injecting output style content for Claude < 1.0.83")
1000
343
 
1001
- # 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
- )
344
+ # Generate dynamic sections
345
+ capabilities_section = self._generate_agent_capabilities_section()
346
+ context_section = self.context_generator.generate_temporal_user_context()
1006
347
 
1007
- # 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}"
348
+ # Format the complete framework
349
+ return self.content_formatter.format_full_framework(
350
+ self.framework_content,
351
+ capabilities_section,
352
+ context_section,
353
+ inject_output_style,
354
+ output_style_content,
355
+ )
1081
356
 
1082
- # Inject output style content if needed (for Claude < 1.0.83)
1083
- if inject_output_style and self.output_style_manager:
1084
- output_style_content = self.output_style_manager.get_injectable_content(
1085
- framework_loader=self
1086
- )
1087
- if output_style_content:
1088
- instructions += "\n\n## Output Style Configuration\n"
1089
- instructions += "**Note: The following output style is injected for Claude < 1.0.83**\n\n"
1090
- instructions += output_style_content
1091
- instructions += "\n"
1092
-
1093
- # Clean up any trailing whitespace
1094
- return instructions.rstrip() + "\n"
1095
-
1096
- # Otherwise fall back to generating framework
1097
- instructions = """# Claude MPM Framework Instructions
1098
-
1099
- You are operating within the Claude Multi-Agent Project Manager (MPM) framework.
1100
-
1101
- ## Core Role
1102
- You are a multi-agent orchestrator. Your primary responsibilities are:
1103
- - Delegate all implementation work to specialized agents via Task Tool
1104
- - Coordinate multi-agent workflows and cross-agent collaboration
1105
- - Extract and track TODO/BUG/FEATURE items for ticket creation
1106
- - Maintain project visibility and strategic oversight
1107
- - NEVER perform direct implementation work yourself
1108
-
1109
- """
1110
-
1111
- # Note: We don't add working directory CLAUDE.md here since Claude Code
1112
- # already picks it up automatically. This prevents duplication.
1113
-
1114
- # Add agent definitions
1115
- if self.framework_content["agents"]:
1116
- instructions += "## Available Agents\n\n"
1117
- instructions += "You have the following specialized agents available for delegation:\n\n"
1118
-
1119
- # List agents with brief descriptions and correct IDs
1120
- agent_list = []
1121
- for agent_name in sorted(self.framework_content["agents"].keys()):
1122
- # Use the actual agent_name as the ID (it's the filename stem)
1123
- agent_id = agent_name
1124
- clean_name = agent_name.replace("-", " ").replace("_", " ").title()
1125
- if (
1126
- "engineer" in agent_name.lower()
1127
- and "data" not in agent_name.lower()
1128
- ):
1129
- agent_list.append(
1130
- f"- **Engineer Agent** (`{agent_id}`): Code implementation and development"
1131
- )
1132
- elif "qa" in agent_name.lower():
1133
- agent_list.append(
1134
- f"- **QA Agent** (`{agent_id}`): Testing and quality assurance"
1135
- )
1136
- elif "documentation" in agent_name.lower():
1137
- agent_list.append(
1138
- f"- **Documentation Agent** (`{agent_id}`): Documentation creation and maintenance"
1139
- )
1140
- elif "research" in agent_name.lower():
1141
- agent_list.append(
1142
- f"- **Research Agent** (`{agent_id}`): Investigation and analysis"
1143
- )
1144
- elif "security" in agent_name.lower():
1145
- agent_list.append(
1146
- f"- **Security Agent** (`{agent_id}`): Security analysis and protection"
1147
- )
1148
- elif "version" in agent_name.lower():
1149
- agent_list.append(
1150
- f"- **Version Control Agent** (`{agent_id}`): Git operations and version management"
1151
- )
1152
- elif "ops" in agent_name.lower():
1153
- agent_list.append(
1154
- f"- **Ops Agent** (`{agent_id}`): Deployment and operations"
1155
- )
1156
- elif "data" in agent_name.lower():
1157
- agent_list.append(
1158
- f"- **Data Engineer Agent** (`{agent_id}`): Data management and AI API integration"
1159
- )
1160
- else:
1161
- agent_list.append(
1162
- f"- **{clean_name}** (`{agent_id}`): Available for specialized tasks"
1163
- )
1164
-
1165
- instructions += "\n".join(agent_list) + "\n\n"
1166
-
1167
- # Add full agent details
1168
- instructions += "### Agent Details\n\n"
1169
- for agent_name, agent_content in sorted(
1170
- self.framework_content["agents"].items()
1171
- ):
1172
- instructions += f"#### {agent_name.replace('-', ' ').title()}\n"
1173
- instructions += agent_content + "\n\n"
1174
-
1175
- # Add orchestration principles
1176
- instructions += """
1177
- ## Orchestration Principles
1178
- 1. **Always Delegate**: Never perform direct work - use Task Tool for all implementation
1179
- 2. **Comprehensive Context**: Provide rich, filtered context to each agent
1180
- 3. **Track Everything**: Extract all TODO/BUG/FEATURE items systematically
1181
- 4. **Cross-Agent Coordination**: Orchestrate workflows spanning multiple agents
1182
- 5. **Results Integration**: Actively receive and integrate agent results
1183
-
1184
- ## Task Tool Format
1185
- ```
1186
- **[Agent Name]**: [Clear task description with deliverables]
1187
-
1188
- TEMPORAL CONTEXT: Today is [date]. Apply date awareness to [specific considerations].
1189
-
1190
- **Task**: [Detailed task breakdown]
1191
- 1. [Specific action item 1]
1192
- 2. [Specific action item 2]
1193
- 3. [Specific action item 3]
1194
-
1195
- **Context**: [Comprehensive filtered context for this agent]
1196
- **Authority**: [Agent's decision-making scope]
1197
- **Expected Results**: [Specific deliverables needed]
1198
- **Integration**: [How results integrate with other work]
1199
- ```
1200
-
1201
- ## Ticket Extraction Patterns
1202
- Extract tickets from these patterns:
1203
- - TODO: [description] → TODO ticket
1204
- - BUG: [description] → BUG ticket
1205
- - FEATURE: [description] → FEATURE ticket
1206
- - ISSUE: [description] → ISSUE ticket
1207
- - FIXME: [description] → BUG ticket
1208
-
1209
- ---
1210
- """
1211
-
1212
- return instructions
357
+ def _format_minimal_framework(self) -> str:
358
+ """Format minimal framework instructions."""
359
+ return self.content_formatter.format_minimal_framework(self.framework_content)
1213
360
 
1214
361
  def _generate_agent_capabilities_section(self) -> str:
1215
- """Generate 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
- """
1225
-
1226
- # Try to get from cache first
362
+ """Generate agent capabilities section with caching."""
363
+ # Try cache first
1227
364
  cached_capabilities = self._cache_manager.get_capabilities()
1228
365
  if cached_capabilities is not None:
1229
366
  return cached_capabilities
1230
367
 
1231
- # 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)")
368
+ self.logger.debug("Generating agent capabilities (cache miss)")
1236
369
 
1237
370
  try:
1238
- from pathlib import Path
1239
-
1240
- # First check for local JSON templates (highest priority)
371
+ # Discover local JSON templates
1241
372
  local_agents = self._discover_local_json_templates()
1242
373
 
1243
- # Read directly from deployed agents in .claude/agents/
1244
- # Check multiple locations for deployed agents
1245
- # Priority order: local templates > project > user home > fallback
374
+ # Get deployed agents from .claude/agents/
375
+ deployed_agents = []
1246
376
  agents_dirs = [
1247
- Path.cwd() / ".claude" / "agents", # Project-specific agents
1248
- Path.home() / ".claude" / "agents", # User's system agents
377
+ Path.cwd() / ".claude" / "agents",
378
+ Path.home() / ".claude" / "agents",
1249
379
  ]
1250
380
 
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
- )
1311
-
1312
- # Build capabilities section
1313
- section = "\n\n## Available Agent Capabilities\n\n"
1314
-
1315
- # Extract just the agent data (drop priority info) and sort
1316
- deployed_agents = [agent_data for agent_data, _ in all_agents.values()]
1317
-
1318
- if not deployed_agents:
1319
- result = self._get_fallback_capabilities()
1320
- # Cache the fallback result
1321
- self._cache_manager.set_capabilities(result)
1322
- return result
1323
-
1324
- # Sort agents alphabetically by ID
1325
- deployed_agents.sort(key=lambda x: x["id"])
1326
-
1327
- # Display all agents with their rich descriptions
1328
- for agent in deployed_agents:
1329
- # Clean up display name - handle common acronyms
1330
- display_name = agent["display_name"]
1331
- display_name = (
1332
- display_name.replace("Qa ", "QA ")
1333
- .replace("Ui ", "UI ")
1334
- .replace("Api ", "API ")
1335
- )
1336
- if display_name.lower() == "qa agent":
1337
- display_name = "QA Agent"
381
+ for agents_dir in agents_dirs:
382
+ if agents_dir.exists():
383
+ for agent_file in agents_dir.glob("*.md"):
384
+ if not agent_file.name.startswith("."):
385
+ agent_data = self._parse_agent_metadata(agent_file)
386
+ if agent_data:
387
+ deployed_agents.append(agent_data)
1338
388
 
1339
- # 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"
389
+ # Generate capabilities section
390
+ section = self.capability_generator.generate_capabilities_section(
391
+ deployed_agents, local_agents
1404
392
  )
1405
393
 
1406
- # Add summary
1407
- section += f"\n**Total Available Agents**: {len(deployed_agents)}\n"
1408
-
1409
- # Cache the generated capabilities
394
+ # Cache the result
1410
395
  self._cache_manager.set_capabilities(section)
1411
- self.logger.debug(
1412
- f"Cached agent capabilities section ({len(section)} chars)"
1413
- )
396
+ self.logger.debug(f"Cached agent capabilities ({len(section)} chars)")
1414
397
 
1415
398
  return section
1416
399
 
1417
400
  except Exception as e:
1418
- self.logger.warning(f"Could not generate 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
401
+ self.logger.warning(f"Could not generate agent capabilities: {e}")
402
+ fallback = self.content_formatter.get_fallback_capabilities()
403
+ self._cache_manager.set_capabilities(fallback)
404
+ return fallback
1424
405
 
1425
- 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"]
406
+ # === Output Style Management ===
1432
407
 
408
+ def _initialize_output_style(self) -> None:
409
+ """Initialize output style management."""
1433
410
  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")
411
+ from claude_mpm.core.output_style_manager import OutputStyleManager
1463
412
 
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")
413
+ self.output_style_manager = OutputStyleManager()
414
+ self._log_output_style_status()
1468
415
 
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"
416
+ # Extract and save output style content
417
+ output_style_content = self.output_style_manager.extract_output_style_content(
418
+ framework_loader=self
1474
419
  )
420
+ self.output_style_manager.save_output_style(output_style_content)
1475
421
 
1476
- 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}")
422
+ # Deploy to Claude Code if supported
423
+ deployed = self.output_style_manager.deploy_output_style(output_style_content)
1509
424
 
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
425
+ if deployed:
426
+ self.logger.info("✅ Output style deployed to Claude Code >= 1.0.83")
427
+ else:
428
+ self.logger.info("📝 Output style will be injected into instructions")
1532
429
 
1533
430
  except Exception as e:
1534
- # 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)"
1591
- )
1592
-
1593
- import yaml
1594
-
1595
- with open(agent_file) as f:
1596
- content = f.read()
1597
-
1598
- # Default values
1599
- agent_data = {
1600
- "id": agent_file.stem,
1601
- "display_name": agent_file.stem.replace("_", " ")
1602
- .replace("-", " ")
1603
- .title(),
1604
- "description": "Specialized agent",
1605
- }
1606
-
1607
- # Extract YAML frontmatter if present
1608
- if content.startswith("---"):
1609
- end_marker = content.find("---", 3)
1610
- if end_marker > 0:
1611
- frontmatter = content[3:end_marker]
1612
- metadata = yaml.safe_load(frontmatter)
1613
- if metadata:
1614
- # Use name as ID for Task tool
1615
- agent_data["id"] = metadata.get("name", agent_data["id"])
1616
- agent_data["display_name"] = (
1617
- metadata.get("name", agent_data["display_name"])
1618
- .replace("-", " ")
1619
- .title()
1620
- )
1621
-
1622
- # Copy all metadata fields directly
1623
- for key, value in metadata.items():
1624
- if key not in ["name"]: # Skip already processed fields
1625
- agent_data[key] = value
1626
-
1627
- # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
1628
- # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
1629
-
1630
- # Try to load routing metadata from JSON template if not in YAML frontmatter
1631
- if "routing" not in agent_data:
1632
- routing_data = self._load_routing_from_template(agent_file.stem)
1633
- if routing_data:
1634
- agent_data["routing"] = routing_data
1635
-
1636
- # Try to load memory routing metadata from JSON template if not in YAML frontmatter
1637
- if "memory_routing" not in agent_data:
1638
- memory_routing_data = self._load_memory_routing_from_template(
1639
- agent_file.stem
1640
- )
1641
- if memory_routing_data:
1642
- agent_data["memory_routing"] = memory_routing_data
1643
-
1644
- # Cache the parsed metadata
1645
- self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
431
+ self.logger.warning(f"❌ Failed to initialize output style manager: {e}")
1646
432
 
1647
- return agent_data
433
+ def _log_output_style_status(self) -> None:
434
+ """Log output style status information."""
435
+ if not self.output_style_manager:
436
+ return
1648
437
 
1649
- except Exception as e:
1650
- self.logger.debug(f"Could not parse metadata from {agent_file}: {e}")
1651
- return None
438
+ claude_version = self.output_style_manager.claude_version
439
+ if claude_version:
440
+ self.logger.info(f"Claude Code version detected: {claude_version}")
1652
441
 
1653
- 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.
442
+ if self.output_style_manager.supports_output_styles():
443
+ self.logger.info("✅ Claude Code supports output styles (>= 1.0.83)")
444
+ output_style_path = self.output_style_manager.output_style_path
445
+ if output_style_path.exists():
446
+ self.logger.info(f"📁 Output style file exists: {output_style_path}")
447
+ else:
448
+ self.logger.info(f"📝 Output style will be created at: {output_style_path}")
449
+ else:
450
+ self.logger.info(f"⚠️ Claude Code {claude_version} does not support output styles")
451
+ self.logger.info("📝 Output style will be injected into framework instructions")
452
+ else:
453
+ self.logger.info("⚠️ Claude Code not detected or version unknown")
454
+ self.logger.info("📝 Output style will be injected as fallback")
1657
455
 
1658
- Args:
1659
- agent_name: Name of the agent (stem of the file)
456
+ # === Logging Methods ===
1660
457
 
1661
- Returns:
1662
- Dictionary with memory routing metadata or None if not found
1663
- """
458
+ def _log_system_prompt(self) -> None:
459
+ """Log the system prompt if LogManager is available."""
1664
460
  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
1721
-
1722
- except Exception as e:
1723
- self.logger.debug(
1724
- f"Could not load memory routing from template for {agent_name}: {e}"
1725
- )
1726
- return None
1727
-
1728
- def _discover_local_json_templates(self) -> Dict[str, Dict[str, Any]]:
1729
- """Discover local JSON agent templates from .claude-mpm/agents/ directories.
1730
-
1731
- Returns:
1732
- Dictionary mapping agent IDs to agent metadata
1733
- """
1734
- import json
1735
- from pathlib import Path
1736
-
1737
- local_agents = {}
1738
-
1739
- # Check for local JSON templates in priority order
1740
- template_dirs = [
1741
- Path.cwd()
1742
- / ".claude-mpm"
1743
- / "agents", # Project local agents (highest priority)
1744
- Path.home() / ".claude-mpm" / "agents", # User local agents
1745
- ]
1746
-
1747
- for priority, template_dir in enumerate(template_dirs):
1748
- if not template_dir.exists():
1749
- continue
1750
-
1751
- for json_file in template_dir.glob("*.json"):
1752
- try:
1753
- with open(json_file) as f:
1754
- template_data = json.load(f)
1755
-
1756
- # Extract agent metadata
1757
- agent_id = template_data.get("agent_id", json_file.stem)
1758
-
1759
- # Skip if already found at higher priority
1760
- if agent_id in local_agents:
1761
- continue
1762
-
1763
- # Extract metadata
1764
- metadata = template_data.get("metadata", {})
1765
-
1766
- # Build agent data in expected format
1767
- agent_data = {
1768
- "id": agent_id,
1769
- "display_name": metadata.get(
1770
- "name", agent_id.replace("_", " ").title()
1771
- ),
1772
- "description": metadata.get(
1773
- "description", f"Local {agent_id} agent"
1774
- ),
1775
- "tools": self._extract_tools_from_template(template_data),
1776
- "is_local": True,
1777
- "tier": "project" if priority == 0 else "user",
1778
- "author": template_data.get("author", "local"),
1779
- "version": template_data.get("agent_version", "1.0.0"),
1780
- }
1781
-
1782
- local_agents[agent_id] = agent_data
1783
- self.logger.debug(
1784
- f"Discovered local JSON agent: {agent_id} from {template_dir}"
1785
- )
1786
-
1787
- except Exception as e:
1788
- self.logger.warning(
1789
- f"Failed to parse local JSON template {json_file}: {e}"
1790
- )
1791
-
1792
- return local_agents
1793
-
1794
- def _extract_tools_from_template(self, template_data: Dict[str, Any]) -> str:
1795
- """Extract tools string from template data.
1796
-
1797
- Args:
1798
- template_data: JSON template data
1799
-
1800
- Returns:
1801
- Tools string for display
1802
- """
1803
- capabilities = template_data.get("capabilities", {})
1804
- tools = capabilities.get("tools", "*")
1805
-
1806
- if tools == "*":
1807
- return "All Tools"
1808
- if isinstance(tools, list):
1809
- return ", ".join(tools) if tools else "Standard Tools"
1810
- if isinstance(tools, str):
1811
- if "," in tools:
1812
- return tools
1813
- return tools
1814
- return "Standard Tools"
1815
-
1816
- def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
1817
- """Load routing metadata from agent JSON template.
461
+ from .log_manager import get_log_manager
1818
462
 
1819
- Args:
1820
- agent_name: Name of the agent (stem of the file)
463
+ log_manager = get_log_manager()
464
+ except ImportError:
465
+ return
1821
466
 
1822
- Returns:
1823
- Dictionary with routing metadata or None if not found
1824
- """
1825
467
  try:
1826
- 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
- )
468
+ # Get or create event loop
469
+ try:
470
+ loop = asyncio.get_running_loop()
471
+ except RuntimeError:
472
+ loop = asyncio.new_event_loop()
473
+ asyncio.set_event_loop(loop)
474
+
475
+ # Prepare metadata
476
+ metadata = {
477
+ "framework_version": self.framework_version,
478
+ "framework_loaded": self.framework_content.get("loaded", False),
479
+ "session_id": os.environ.get("CLAUDE_SESSION_ID", "unknown"),
480
+ }
1868
481
 
1869
- 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")
482
+ # Log the prompt asynchronously
483
+ instructions = self._format_full_framework() if self.framework_content["loaded"] else self._format_minimal_framework()
484
+ metadata["instructions_length"] = len(instructions)
1876
485
 
1877
- self.logger.debug(f"No JSON template found for agent: {agent_name}")
1878
- return None
486
+ if loop.is_running():
487
+ asyncio.create_task(log_manager.log_prompt("system_prompt", instructions, metadata))
488
+ else:
489
+ loop.run_until_complete(log_manager.log_prompt("system_prompt", instructions, metadata))
1879
490
 
491
+ self.logger.debug("System prompt logged to prompts directory")
1880
492
  except Exception as e:
1881
- self.logger.debug(f"Could not 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.
1886
-
1887
- Creates a mapping of task types to appropriate agents based on their
1888
- descriptions and capabilities.
1889
- """
1890
- guide = ""
1891
-
1892
- # Build selection mapping based on deployed agents
1893
- selection_map = {}
493
+ self.logger.debug(f"Could not log system prompt: {e}")
1894
494
 
1895
- for agent in deployed_agents:
1896
- agent_id = agent["id"]
1897
- desc_lower = agent["description"].lower()
495
+ # === Agent Registry Methods (backward compatibility) ===
1898
496
 
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}`)"
1935
- )
1936
- if "git" in desc_lower or "version control" in desc_lower:
1937
- selection_map["Version control"] = (
1938
- f"{agent['display_name']} (`{agent_id}`)"
1939
- )
1940
- if "ticket" in desc_lower or "epic" in desc_lower:
1941
- selection_map["Ticket/issue management"] = (
1942
- f"{agent['display_name']} (`{agent_id}`)"
1943
- )
1944
- if "browser" in desc_lower or "e2e" in desc_lower:
1945
- selection_map["Browser/E2E testing"] = (
1946
- f"{agent['display_name']} (`{agent_id}`)"
1947
- )
1948
- if "frontend" in desc_lower or "ui" in desc_lower or "html" in desc_lower:
1949
- selection_map["Frontend/UI development"] = (
1950
- f"{agent['display_name']} (`{agent_id}`)"
1951
- )
1952
-
1953
- # Always include PM questions
1954
- selection_map["PM questions"] = "Answer directly (only exception)"
1955
-
1956
- # Format the selection guide
1957
- for task_type, agent_info in selection_map.items():
1958
- guide += f"- **{task_type}** → {agent_info}\n"
1959
-
1960
- return guide
1961
-
1962
- def _get_fallback_capabilities(self) -> str:
1963
- """Return fallback capabilities when dynamic discovery fails."""
1964
- return """
1965
-
1966
- ## Available Agent Capabilities
1967
-
1968
- You have the following specialized agents available for delegation:
1969
-
1970
- - **Engineer** (`engineer`): Code implementation and development
1971
- - **Research** (`research-agent`): Investigation and analysis
1972
- - **QA** (`qa-agent`): Testing and quality assurance
1973
- - **Documentation** (`documentation-agent`): Documentation creation and maintenance
1974
- - **Security** (`security-agent`): Security analysis and protection
1975
- - **Data Engineer** (`data-engineer`): Data management and pipelines
1976
- - **Ops** (`ops-agent`): Deployment and operations
1977
- - **Version Control** (`version-control`): Git operations and version management
1978
-
1979
- **IMPORTANT**: Use the exact agent ID in parentheses when delegating tasks.
1980
- """
1981
-
1982
- def _format_minimal_framework(self) -> str:
1983
- """Format minimal framework instructions when full framework not available."""
1984
- return """
1985
- # Claude PM Framework Instructions
1986
-
1987
- You are operating within a Claude PM Framework deployment.
1988
-
1989
- ## Role
1990
- You are a multi-agent orchestrator. Your primary responsibilities:
1991
- - Delegate tasks to specialized agents via Task Tool
1992
- - Coordinate multi-agent workflows
1993
- - Extract TODO/BUG/FEATURE items for ticket creation
1994
- - NEVER perform direct implementation work
1995
-
1996
- ## Core Agents
1997
- - Documentation Agent - Documentation tasks
1998
- - Engineer Agent - Code implementation
1999
- - QA Agent - Testing and validation
2000
- - Research Agent - Investigation and analysis
2001
- - Version Control Agent - Git operations
2002
-
2003
- ## Important Rules
2004
- 1. Always delegate work via Task Tool
2005
- 2. Provide comprehensive context to agents
2006
- 3. Track all TODO/BUG/FEATURE items
2007
- 4. Maintain project visibility
2008
-
2009
- ---
2010
- """
2011
-
2012
- def get_agent_list(self) -> list:
497
+ def get_agent_list(self) -> List[str]:
2013
498
  """Get list of available agents."""
2014
- # First try agent registry
2015
499
  if self.agent_registry:
2016
500
  agents = self.agent_registry.list_agents()
2017
501
  if agents:
2018
502
  return list(agents.keys())
2019
-
2020
- # Fallback to loaded content
2021
503
  return list(self.framework_content["agents"].keys())
2022
504
 
2023
505
  def get_agent_definition(self, agent_name: str) -> Optional[str]:
2024
506
  """Get specific agent definition."""
2025
- # First try agent registry
2026
507
  if self.agent_registry:
2027
508
  definition = self.agent_registry.get_agent_definition(agent_name)
2028
509
  if definition:
2029
510
  return definition
2030
-
2031
- # Fallback to loaded content
2032
511
  return self.framework_content["agents"].get(agent_name)
2033
512
 
2034
- def get_agent_hierarchy(self) -> Dict[str, list]:
513
+ def get_agent_hierarchy(self) -> Dict[str, List]:
2035
514
  """Get agent hierarchy from registry."""
2036
515
  if self.agent_registry:
2037
516
  return self.agent_registry.get_agent_hierarchy()
2038
- return {"project": [], "user": [], "system": []}
517
+ return {"project": [], "user": [], "system": []}