claude-mpm 4.0.31__py3-none-any.whl → 4.0.34__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 (71) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +33 -25
  3. claude_mpm/agents/INSTRUCTIONS.md +14 -10
  4. claude_mpm/agents/templates/documentation.json +51 -34
  5. claude_mpm/agents/templates/research.json +0 -11
  6. claude_mpm/cli/__init__.py +63 -26
  7. claude_mpm/cli/commands/agent_manager.py +10 -8
  8. claude_mpm/core/framework_loader.py +272 -113
  9. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  10. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  13. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  14. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  15. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  16. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  17. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  18. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  19. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  20. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  21. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  22. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  23. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  24. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  25. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  26. claude_mpm/dashboard/templates/index.html +1 -0
  27. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  30. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  31. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +86 -37
  33. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  34. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  35. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  36. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  37. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  38. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  39. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -13
  40. claude_mpm/services/agents/memory/agent_memory_manager.py +141 -184
  41. claude_mpm/services/agents/memory/content_manager.py +182 -232
  42. claude_mpm/services/agents/memory/template_generator.py +4 -40
  43. claude_mpm/services/event_bus/__init__.py +18 -0
  44. claude_mpm/services/event_bus/event_bus.py +334 -0
  45. claude_mpm/services/event_bus/relay.py +301 -0
  46. claude_mpm/services/events/__init__.py +44 -0
  47. claude_mpm/services/events/consumers/__init__.py +18 -0
  48. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  49. claude_mpm/services/events/consumers/logging.py +183 -0
  50. claude_mpm/services/events/consumers/metrics.py +242 -0
  51. claude_mpm/services/events/consumers/socketio.py +376 -0
  52. claude_mpm/services/events/core.py +470 -0
  53. claude_mpm/services/events/interfaces.py +230 -0
  54. claude_mpm/services/events/producers/__init__.py +14 -0
  55. claude_mpm/services/events/producers/hook.py +269 -0
  56. claude_mpm/services/events/producers/system.py +327 -0
  57. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  58. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  59. claude_mpm/services/monitor_build_service.py +345 -0
  60. claude_mpm/services/socketio/event_normalizer.py +667 -0
  61. claude_mpm/services/socketio/handlers/connection.py +78 -20
  62. claude_mpm/services/socketio/handlers/hook.py +14 -5
  63. claude_mpm/services/socketio/migration_utils.py +329 -0
  64. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  65. claude_mpm/services/socketio/server/core.py +4 -3
  66. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  67. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +71 -50
  68. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  69. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  70. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  71. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,10 @@
3
3
  import logging
4
4
  import os
5
5
  import sys
6
+ import time
6
7
  from datetime import datetime
7
8
  from pathlib import Path
8
- from typing import Any, Dict, Optional
9
+ from typing import Any, Dict, Optional, Set, Tuple
9
10
 
10
11
  # Import resource handling for packaged installations
11
12
  try:
@@ -34,9 +35,41 @@ class FrameworkLoader:
34
35
 
35
36
  This component handles:
36
37
  1. Finding the framework (claude-multiagent-pm)
37
- 2. Loading INSTRUCTIONS.md instructions
38
+ 2. Loading custom instructions from .claude-mpm/ directories
38
39
  3. Preparing agent definitions
39
40
  4. Formatting for injection
41
+
42
+ Custom Instructions Loading:
43
+ The framework loader supports custom instructions through .claude-mpm/ directories.
44
+ It NEVER reads from .claude/ directories to avoid conflicts with Claude Code.
45
+
46
+ File Loading Precedence (highest to lowest):
47
+
48
+ INSTRUCTIONS.md:
49
+ 1. Project: ./.claude-mpm/INSTRUCTIONS.md
50
+ 2. User: ~/.claude-mpm/INSTRUCTIONS.md
51
+ 3. System: (built-in framework instructions)
52
+
53
+ WORKFLOW.md:
54
+ 1. Project: ./.claude-mpm/WORKFLOW.md
55
+ 2. User: ~/.claude-mpm/WORKFLOW.md
56
+ 3. System: src/claude_mpm/agents/WORKFLOW.md
57
+
58
+ MEMORY.md:
59
+ 1. Project: ./.claude-mpm/MEMORY.md
60
+ 2. User: ~/.claude-mpm/MEMORY.md
61
+ 3. System: src/claude_mpm/agents/MEMORY.md
62
+
63
+ Actual Memories:
64
+ - User: ~/.claude-mpm/memories/PM_memories.md
65
+ - Project: ./.claude-mpm/memories/PM_memories.md (overrides user)
66
+ - Agent memories: *_memories.md files (only loaded if agent is deployed)
67
+
68
+ Important Notes:
69
+ - Project-level files always override user-level files
70
+ - User-level files always override system defaults
71
+ - The framework NEVER reads from .claude/ directories
72
+ - Custom instructions are clearly labeled with their source level
40
73
  """
41
74
 
42
75
  def __init__(
@@ -54,6 +87,22 @@ class FrameworkLoader:
54
87
  self.agents_dir = agents_dir
55
88
  self.framework_version = None
56
89
  self.framework_last_modified = None
90
+
91
+ # Performance optimization: Initialize caches
92
+ self._agent_capabilities_cache: Optional[str] = None
93
+ self._agent_capabilities_cache_time: float = 0
94
+ self._deployed_agents_cache: Optional[Set[str]] = None
95
+ self._deployed_agents_cache_time: float = 0
96
+ self._agent_metadata_cache: Dict[str, Tuple[Optional[Dict[str, Any]], float]] = {}
97
+ self._memories_cache: Optional[Dict[str, Any]] = None
98
+ self._memories_cache_time: float = 0
99
+
100
+ # Cache TTL settings (in seconds)
101
+ self.CAPABILITIES_CACHE_TTL = 60 # 60 seconds for capabilities
102
+ self.DEPLOYED_AGENTS_CACHE_TTL = 30 # 30 seconds for deployed agents
103
+ self.METADATA_CACHE_TTL = 60 # 60 seconds for agent metadata
104
+ self.MEMORIES_CACHE_TTL = 60 # 60 seconds for memories
105
+
57
106
  self.framework_content = self._load_framework_content()
58
107
 
59
108
  # Initialize agent registry
@@ -62,6 +111,32 @@ class FrameworkLoader:
62
111
  # Initialize output style manager (must be after content is loaded)
63
112
  self.output_style_manager = None
64
113
  # Defer initialization until first use to ensure content is loaded
114
+
115
+ def clear_all_caches(self) -> None:
116
+ """Clear all caches to force reload on next access."""
117
+ self.logger.info("Clearing all framework loader caches")
118
+ self._agent_capabilities_cache = None
119
+ self._agent_capabilities_cache_time = 0
120
+ self._deployed_agents_cache = None
121
+ self._deployed_agents_cache_time = 0
122
+ self._agent_metadata_cache.clear()
123
+ self._memories_cache = None
124
+ self._memories_cache_time = 0
125
+
126
+ def clear_agent_caches(self) -> None:
127
+ """Clear agent-related caches (capabilities, deployed agents, metadata)."""
128
+ self.logger.info("Clearing agent-related caches")
129
+ self._agent_capabilities_cache = None
130
+ self._agent_capabilities_cache_time = 0
131
+ self._deployed_agents_cache = None
132
+ self._deployed_agents_cache_time = 0
133
+ self._agent_metadata_cache.clear()
134
+
135
+ def clear_memory_caches(self) -> None:
136
+ """Clear memory-related caches."""
137
+ self.logger.info("Clearing memory caches")
138
+ self._memories_cache = None
139
+ self._memories_cache_time = 0
65
140
 
66
141
  def _initialize_output_style(self) -> None:
67
142
  """Initialize output style management and deploy if applicable."""
@@ -349,28 +424,43 @@ class FrameworkLoader:
349
424
 
350
425
  def _load_workflow_instructions(self, content: Dict[str, Any]) -> None:
351
426
  """
352
- Load WORKFLOW.md with project-specific override support.
427
+ Load WORKFLOW.md from .claude-mpm directories.
353
428
 
354
- Precedence:
355
- 1. Project-specific: .claude-mpm/agents/WORKFLOW.md
356
- 2. System default: src/claude_mpm/agents/WORKFLOW.md or packaged
429
+ Precedence (highest to lowest):
430
+ 1. Project-specific: ./.claude-mpm/WORKFLOW.md
431
+ 2. User-specific: ~/.claude-mpm/WORKFLOW.md
432
+ 3. System default: src/claude_mpm/agents/WORKFLOW.md or packaged
433
+
434
+ NOTE: We do NOT load from .claude/ directories to avoid conflicts.
357
435
 
358
436
  Args:
359
437
  content: Dictionary to update with workflow instructions
360
438
  """
361
- # Check for project-specific workflow first
362
- project_workflow_path = Path.cwd() / ".claude-mpm" / "agents" / "WORKFLOW.md"
439
+ # Check for project-specific WORKFLOW.md first (highest priority)
440
+ project_workflow_path = Path.cwd() / ".claude-mpm" / "WORKFLOW.md"
363
441
  if project_workflow_path.exists():
364
442
  loaded_content = self._try_load_file(
365
443
  project_workflow_path, "project-specific WORKFLOW.md"
366
444
  )
367
445
  if loaded_content:
368
446
  content["workflow_instructions"] = loaded_content
369
- content["project_workflow"] = "project"
370
- self.logger.info("Using project-specific WORKFLOW.md")
447
+ content["workflow_instructions_level"] = "project"
448
+ self.logger.info("Using project-specific workflow instructions from .claude-mpm/WORKFLOW.md")
449
+ return
450
+
451
+ # Check for user-specific WORKFLOW.md (medium priority)
452
+ user_workflow_path = Path.home() / ".claude-mpm" / "WORKFLOW.md"
453
+ if user_workflow_path.exists():
454
+ loaded_content = self._try_load_file(
455
+ user_workflow_path, "user-specific WORKFLOW.md"
456
+ )
457
+ if loaded_content:
458
+ content["workflow_instructions"] = loaded_content
459
+ content["workflow_instructions_level"] = "user"
460
+ self.logger.info("Using user-specific workflow instructions from ~/.claude-mpm/WORKFLOW.md")
371
461
  return
372
462
 
373
- # Fall back to system workflow
463
+ # Fall back to system workflow (lowest priority)
374
464
  if self.framework_path and self.framework_path != Path("__PACKAGED__"):
375
465
  system_workflow_path = (
376
466
  self.framework_path / "src" / "claude_mpm" / "agents" / "WORKFLOW.md"
@@ -381,33 +471,48 @@ class FrameworkLoader:
381
471
  )
382
472
  if loaded_content:
383
473
  content["workflow_instructions"] = loaded_content
384
- content["project_workflow"] = "system"
385
- self.logger.info("Using system WORKFLOW.md")
474
+ content["workflow_instructions_level"] = "system"
475
+ self.logger.info("Using system workflow instructions")
386
476
 
387
477
  def _load_memory_instructions(self, content: Dict[str, Any]) -> None:
388
478
  """
389
- Load MEMORY.md with project-specific override support.
479
+ Load MEMORY.md from .claude-mpm directories.
390
480
 
391
- Precedence:
392
- 1. Project-specific: .claude-mpm/agents/MEMORY.md
393
- 2. System default: src/claude_mpm/agents/MEMORY.md or packaged
481
+ Precedence (highest to lowest):
482
+ 1. Project-specific: ./.claude-mpm/MEMORY.md
483
+ 2. User-specific: ~/.claude-mpm/MEMORY.md
484
+ 3. System default: src/claude_mpm/agents/MEMORY.md or packaged
485
+
486
+ NOTE: We do NOT load from .claude/ directories to avoid conflicts.
394
487
 
395
488
  Args:
396
489
  content: Dictionary to update with memory instructions
397
490
  """
398
- # Check for project-specific memory instructions first
399
- project_memory_path = Path.cwd() / ".claude-mpm" / "agents" / "MEMORY.md"
491
+ # Check for project-specific MEMORY.md first (highest priority)
492
+ project_memory_path = Path.cwd() / ".claude-mpm" / "MEMORY.md"
400
493
  if project_memory_path.exists():
401
494
  loaded_content = self._try_load_file(
402
495
  project_memory_path, "project-specific MEMORY.md"
403
496
  )
404
497
  if loaded_content:
405
498
  content["memory_instructions"] = loaded_content
406
- content["project_memory"] = "project"
407
- self.logger.info("Using project-specific MEMORY.md")
499
+ content["memory_instructions_level"] = "project"
500
+ self.logger.info("Using project-specific memory instructions from .claude-mpm/MEMORY.md")
501
+ return
502
+
503
+ # Check for user-specific MEMORY.md (medium priority)
504
+ user_memory_path = Path.home() / ".claude-mpm" / "MEMORY.md"
505
+ if user_memory_path.exists():
506
+ loaded_content = self._try_load_file(
507
+ user_memory_path, "user-specific MEMORY.md"
508
+ )
509
+ if loaded_content:
510
+ content["memory_instructions"] = loaded_content
511
+ content["memory_instructions_level"] = "user"
512
+ self.logger.info("Using user-specific memory instructions from ~/.claude-mpm/MEMORY.md")
408
513
  return
409
514
 
410
- # Fall back to system memory instructions
515
+ # Fall back to system memory instructions (lowest priority)
411
516
  if self.framework_path and self.framework_path != Path("__PACKAGED__"):
412
517
  system_memory_path = (
413
518
  self.framework_path / "src" / "claude_mpm" / "agents" / "MEMORY.md"
@@ -418,16 +523,26 @@ class FrameworkLoader:
418
523
  )
419
524
  if loaded_content:
420
525
  content["memory_instructions"] = loaded_content
421
- content["project_memory"] = "system"
422
- self.logger.info("Using system MEMORY.md")
526
+ content["memory_instructions_level"] = "system"
527
+ self.logger.info("Using system memory instructions")
423
528
 
424
529
  def _get_deployed_agents(self) -> set:
425
530
  """
426
531
  Get a set of deployed agent names from .claude/agents/ directories.
532
+ Uses caching to avoid repeated filesystem scans.
427
533
 
428
534
  Returns:
429
535
  Set of agent names (file stems) that are deployed
430
536
  """
537
+ # Check if cache is valid
538
+ current_time = time.time()
539
+ if (self._deployed_agents_cache is not None and
540
+ current_time - self._deployed_agents_cache_time < self.DEPLOYED_AGENTS_CACHE_TTL):
541
+ self.logger.debug(f"Using cached deployed agents (age: {current_time - self._deployed_agents_cache_time:.1f}s)")
542
+ return self._deployed_agents_cache
543
+
544
+ # Cache miss or expired - perform actual scan
545
+ self.logger.debug("Scanning for deployed agents (cache miss or expired)")
431
546
  deployed = set()
432
547
 
433
548
  # Check multiple locations for deployed agents
@@ -445,11 +560,17 @@ class FrameworkLoader:
445
560
  self.logger.debug(f"Found deployed agent: {agent_file.stem} in {agents_dir}")
446
561
 
447
562
  self.logger.debug(f"Total deployed agents found: {len(deployed)}")
563
+
564
+ # Update cache
565
+ self._deployed_agents_cache = deployed
566
+ self._deployed_agents_cache_time = current_time
567
+
448
568
  return deployed
449
569
 
450
570
  def _load_actual_memories(self, content: Dict[str, Any]) -> None:
451
571
  """
452
572
  Load actual memories from both user and project directories.
573
+ Uses caching to avoid repeated file I/O operations.
453
574
 
454
575
  Loading order:
455
576
  1. User-level memories from ~/.claude-mpm/memories/ (global defaults)
@@ -462,6 +583,23 @@ class FrameworkLoader:
462
583
  Args:
463
584
  content: Dictionary to update with actual memories
464
585
  """
586
+ # Check if cache is valid
587
+ current_time = time.time()
588
+ if (self._memories_cache is not None and
589
+ current_time - self._memories_cache_time < self.MEMORIES_CACHE_TTL):
590
+ cache_age = current_time - self._memories_cache_time
591
+ self.logger.debug(f"Using cached memories (age: {cache_age:.1f}s)")
592
+
593
+ # Apply cached memories to content
594
+ if "actual_memories" in self._memories_cache:
595
+ content["actual_memories"] = self._memories_cache["actual_memories"]
596
+ if "agent_memories" in self._memories_cache:
597
+ content["agent_memories"] = self._memories_cache["agent_memories"]
598
+ return
599
+
600
+ # Cache miss or expired - perform actual loading
601
+ self.logger.debug("Loading memories from disk (cache miss or expired)")
602
+
465
603
  # Define memory directories in priority order (user first, then project)
466
604
  user_memories_dir = Path.home() / ".claude-mpm" / "memories"
467
605
  project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
@@ -513,6 +651,14 @@ class FrameworkLoader:
513
651
  memory_size = len(memory_content.encode('utf-8'))
514
652
  self.logger.debug(f"Aggregated {agent_name} memory: {memory_size:,} bytes")
515
653
 
654
+ # Update cache with loaded memories
655
+ self._memories_cache = {}
656
+ if "actual_memories" in content:
657
+ self._memories_cache["actual_memories"] = content["actual_memories"]
658
+ if "agent_memories" in content:
659
+ self._memories_cache["agent_memories"] = content["agent_memories"]
660
+ self._memories_cache_time = current_time
661
+
516
662
  # Log detailed summary
517
663
  if loaded_count > 0 or skipped_count > 0:
518
664
  # Count unique agents with memories
@@ -664,11 +810,10 @@ class FrameworkLoader:
664
810
  Aggregate multiple memory entries into a single memory string.
665
811
 
666
812
  Strategy:
667
- - Support both sectioned and non-sectioned memories
668
- - Preserve all bullet-point items (lines starting with -)
669
- - Merge sections when present, with project-level taking precedence
670
- - Remove exact duplicates within sections and unsectioned items
671
- - Preserve unique entries from both sources
813
+ - Simplified to support list-based memories only
814
+ - Preserve all unique bullet-point items (lines starting with -)
815
+ - Remove exact duplicates
816
+ - Project-level memories take precedence over user-level
672
817
 
673
818
  Args:
674
819
  memory_entries: List of memory entries with source, content, and path
@@ -683,97 +828,52 @@ class FrameworkLoader:
683
828
  if len(memory_entries) == 1:
684
829
  return memory_entries[0]["content"]
685
830
 
686
- # Parse all memories into sections and unsectioned items
687
- all_sections = {}
688
- unsectioned_items = {} # Items without a section header
831
+ # Parse all memories into a simple list
832
+ all_items = {} # Dict to track items and their source
689
833
  metadata_lines = []
834
+ agent_id = None
690
835
 
691
836
  for entry in memory_entries:
692
837
  content = entry["content"]
693
838
  source = entry["source"]
694
839
 
695
- # Parse content into sections and unsectioned items
696
- current_section = None
697
- current_items = []
698
-
699
840
  for line in content.split('\n'):
841
+ # Check for header to extract agent_id
842
+ if line.startswith('# Agent Memory:'):
843
+ agent_id = line.replace('# Agent Memory:', '').strip()
700
844
  # Check for metadata lines
701
- if line.startswith('<!-- ') and line.endswith(' -->'):
845
+ elif line.startswith('<!-- ') and line.endswith(' -->'):
702
846
  # Only keep metadata from project source or if not already present
703
847
  if source == "project" or line not in metadata_lines:
704
848
  metadata_lines.append(line)
705
- # Check for section headers (## Level 2 headers)
706
- elif line.startswith('## '):
707
- # Save previous section if exists
708
- if current_section and current_items:
709
- if current_section not in all_sections:
710
- all_sections[current_section] = {}
711
- # Store items with their source
712
- for item in current_items:
713
- # Use content as key to detect duplicates
714
- all_sections[current_section][item] = source
849
+ # Check for list items
850
+ elif line.strip().startswith('-'):
851
+ # Normalize the item for comparison
852
+ item_text = line.strip()
853
+ normalized = item_text.lstrip('- ').strip().lower()
715
854
 
716
- # Start new section
717
- current_section = line
718
- current_items = []
719
- # Check for content lines (including unsectioned bullet points)
720
- elif line.strip():
721
- # If it's a bullet point or regular content
722
- if current_section:
723
- # Add to current section
724
- current_items.append(line)
725
- elif line.strip().startswith('-'):
726
- # It's an unsectioned bullet point - preserve it
727
- # Use content as key to detect duplicates
728
- # Project source overrides user source
729
- if line not in unsectioned_items or source == "project":
730
- unsectioned_items[line] = source
731
- # Skip other non-bullet unsectioned content (like headers)
732
- elif not line.strip().startswith('#'):
733
- # Include non-header orphaned content in unsectioned items
734
- if line not in unsectioned_items or source == "project":
735
- unsectioned_items[line] = source
736
-
737
- # Save last section if exists
738
- if current_section and current_items:
739
- if current_section not in all_sections:
740
- all_sections[current_section] = {}
741
- for item in current_items:
742
- # Project source overrides user source
743
- if item not in all_sections[current_section] or source == "project":
744
- all_sections[current_section][item] = source
855
+ # Add item if new or if project source overrides user source
856
+ if normalized not in all_items or source == "project":
857
+ all_items[normalized] = (item_text, source)
745
858
 
746
- # Build aggregated content
859
+ # Build aggregated content as simple list
747
860
  lines = []
748
861
 
749
- # Add metadata
750
- if metadata_lines:
751
- lines.extend(metadata_lines)
752
- lines.append("")
753
-
754
862
  # Add header
755
- lines.append("# Aggregated Memory")
756
- lines.append("")
757
- lines.append("*This memory combines user-level and project-level memories.*")
758
- lines.append("")
863
+ if agent_id:
864
+ lines.append(f"# Agent Memory: {agent_id}")
865
+ else:
866
+ lines.append("# Agent Memory")
759
867
 
760
- # Add unsectioned items first (if any)
761
- if unsectioned_items:
762
- # Sort items to ensure consistent output
763
- for item in sorted(unsectioned_items.keys()):
764
- lines.append(item)
765
- lines.append("") # Empty line after unsectioned items
868
+ # Add latest timestamp from metadata
869
+ from datetime import datetime
870
+ lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
871
+ lines.append("")
766
872
 
767
- # Add sections
768
- for section_header in sorted(all_sections.keys()):
769
- lines.append(section_header)
770
- section_items = all_sections[section_header]
771
-
772
- # Sort items to ensure consistent output
773
- for item in sorted(section_items.keys()):
774
- lines.append(item)
775
-
776
- lines.append("") # Empty line after section
873
+ # Add all unique items (sorted for consistency)
874
+ for normalized_key in sorted(all_items.keys()):
875
+ item_text, source = all_items[normalized_key]
876
+ lines.append(item_text)
777
877
 
778
878
  return '\n'.join(lines)
779
879
 
@@ -859,9 +959,11 @@ class FrameworkLoader:
859
959
  "working_claude_md": "",
860
960
  "framework_instructions": "",
861
961
  "workflow_instructions": "",
862
- "project_workflow": "",
962
+ "workflow_instructions_level": "", # Track source level
863
963
  "memory_instructions": "",
864
- "project_memory": "",
964
+ "memory_instructions_level": "", # Track source level
965
+ "project_workflow": "", # Deprecated, use workflow_instructions_level
966
+ "project_memory": "", # Deprecated, use memory_instructions_level
865
967
  "actual_memories": "", # Add field for actual memories from PM_memories.md
866
968
  }
867
969
 
@@ -1141,16 +1243,22 @@ class FrameworkLoader:
1141
1243
  workflow_content = self._strip_metadata_comments(
1142
1244
  self.framework_content["workflow_instructions"]
1143
1245
  )
1144
- instructions += f"\n\n{workflow_content}\n"
1145
- # Note: project-specific workflow is being used (logged elsewhere)
1246
+ level = self.framework_content.get("workflow_instructions_level", "system")
1247
+ if level != "system":
1248
+ instructions += f"\n\n## Workflow Instructions ({level} level)\n\n"
1249
+ instructions += "**The following workflow instructions override system defaults:**\n\n"
1250
+ instructions += f"{workflow_content}\n"
1146
1251
 
1147
1252
  # Add MEMORY.md after workflow instructions
1148
1253
  if self.framework_content.get("memory_instructions"):
1149
1254
  memory_content = self._strip_metadata_comments(
1150
1255
  self.framework_content["memory_instructions"]
1151
1256
  )
1152
- instructions += f"\n\n{memory_content}\n"
1153
- # Note: project-specific memory instructions being used (logged elsewhere)
1257
+ level = self.framework_content.get("memory_instructions_level", "system")
1258
+ if level != "system":
1259
+ instructions += f"\n\n## Memory Instructions ({level} level)\n\n"
1260
+ instructions += "**The following memory instructions override system defaults:**\n\n"
1261
+ instructions += f"{memory_content}\n"
1154
1262
 
1155
1263
  # Add actual PM memories after memory instructions
1156
1264
  if self.framework_content.get("actual_memories"):
@@ -1322,7 +1430,20 @@ Extract tickets from these patterns:
1322
1430
  return instructions
1323
1431
 
1324
1432
  def _generate_agent_capabilities_section(self) -> str:
1325
- """Generate dynamic agent capabilities section from deployed agents."""
1433
+ """Generate dynamic agent capabilities section from deployed agents.
1434
+ Uses caching to avoid repeated file I/O and parsing operations."""
1435
+
1436
+ # Check if cache is valid
1437
+ current_time = time.time()
1438
+ if (self._agent_capabilities_cache is not None and
1439
+ current_time - self._agent_capabilities_cache_time < self.CAPABILITIES_CACHE_TTL):
1440
+ cache_age = current_time - self._agent_capabilities_cache_time
1441
+ self.logger.debug(f"Using cached agent capabilities (age: {cache_age:.1f}s)")
1442
+ return self._agent_capabilities_cache
1443
+
1444
+ # Cache miss or expired - generate capabilities
1445
+ self.logger.debug("Generating agent capabilities (cache miss or expired)")
1446
+
1326
1447
  try:
1327
1448
  from pathlib import Path
1328
1449
 
@@ -1349,7 +1470,7 @@ Extract tickets from these patterns:
1349
1470
  if agent_file.name.startswith("."):
1350
1471
  continue
1351
1472
 
1352
- # Parse agent metadata
1473
+ # Parse agent metadata (with caching)
1353
1474
  agent_data = self._parse_agent_metadata(agent_file)
1354
1475
  if agent_data:
1355
1476
  agent_id = agent_data["id"]
@@ -1361,7 +1482,11 @@ Extract tickets from these patterns:
1361
1482
 
1362
1483
  if not all_agents:
1363
1484
  self.logger.warning(f"No agents found in any location: {agents_dirs}")
1364
- return self._get_fallback_capabilities()
1485
+ result = self._get_fallback_capabilities()
1486
+ # Cache the fallback result too
1487
+ self._agent_capabilities_cache = result
1488
+ self._agent_capabilities_cache_time = current_time
1489
+ return result
1365
1490
 
1366
1491
  # Log agent collection summary
1367
1492
  project_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 0]
@@ -1379,7 +1504,11 @@ Extract tickets from these patterns:
1379
1504
  deployed_agents = [agent_data for agent_data, _ in all_agents.values()]
1380
1505
 
1381
1506
  if not deployed_agents:
1382
- return self._get_fallback_capabilities()
1507
+ result = self._get_fallback_capabilities()
1508
+ # Cache the fallback result
1509
+ self._agent_capabilities_cache = result
1510
+ self._agent_capabilities_cache_time = current_time
1511
+ return result
1383
1512
 
1384
1513
  # Sort agents alphabetically by ID
1385
1514
  deployed_agents.sort(key=lambda x: x["id"])
@@ -1426,19 +1555,46 @@ Extract tickets from these patterns:
1426
1555
  # Add summary
1427
1556
  section += f"\n**Total Available Agents**: {len(deployed_agents)}\n"
1428
1557
 
1558
+ # Cache the generated capabilities
1559
+ self._agent_capabilities_cache = section
1560
+ self._agent_capabilities_cache_time = current_time
1561
+ self.logger.debug(f"Cached agent capabilities section ({len(section)} chars)")
1562
+
1429
1563
  return section
1430
1564
 
1431
1565
  except Exception as e:
1432
1566
  self.logger.warning(f"Could not generate dynamic agent capabilities: {e}")
1433
- return self._get_fallback_capabilities()
1567
+ result = self._get_fallback_capabilities()
1568
+ # Cache even the fallback result
1569
+ self._agent_capabilities_cache = result
1570
+ self._agent_capabilities_cache_time = current_time
1571
+ return result
1434
1572
 
1435
1573
  def _parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
1436
1574
  """Parse agent metadata from deployed agent file.
1575
+ Uses caching based on file path and modification time.
1437
1576
 
1438
1577
  Returns:
1439
1578
  Dictionary with agent metadata directly from YAML frontmatter.
1440
1579
  """
1441
1580
  try:
1581
+ # Check cache based on file path and modification time
1582
+ cache_key = str(agent_file)
1583
+ file_mtime = agent_file.stat().st_mtime
1584
+ current_time = time.time()
1585
+
1586
+ # Check if we have cached data for this file
1587
+ if cache_key in self._agent_metadata_cache:
1588
+ cached_data, cached_mtime = self._agent_metadata_cache[cache_key]
1589
+ # Use cache if file hasn't been modified and cache isn't too old
1590
+ if (cached_mtime == file_mtime and
1591
+ current_time - cached_mtime < self.METADATA_CACHE_TTL):
1592
+ self.logger.debug(f"Using cached metadata for {agent_file.name}")
1593
+ return cached_data
1594
+
1595
+ # Cache miss or expired - parse the file
1596
+ self.logger.debug(f"Parsing metadata for {agent_file.name} (cache miss or expired)")
1597
+
1442
1598
  import yaml
1443
1599
 
1444
1600
  with open(agent_file, "r") as f:
@@ -1476,6 +1632,9 @@ Extract tickets from these patterns:
1476
1632
  # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
1477
1633
  # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
1478
1634
 
1635
+ # Cache the parsed metadata
1636
+ self._agent_metadata_cache[cache_key] = (agent_data, file_mtime)
1637
+
1479
1638
  return agent_data
1480
1639
 
1481
1640
  except Exception as e: