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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +33 -25
- claude_mpm/agents/INSTRUCTIONS.md +14 -10
- claude_mpm/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +63 -26
- claude_mpm/cli/commands/agent_manager.py +10 -8
- claude_mpm/core/framework_loader.py +272 -113
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +85 -6
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +86 -37
- claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -13
- claude_mpm/services/agents/memory/agent_memory_manager.py +141 -184
- claude_mpm/services/agents/memory/content_manager.py +182 -232
- claude_mpm/services/agents/memory/template_generator.py +4 -40
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/event_bus.py +334 -0
- claude_mpm/services/event_bus/relay.py +301 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +78 -20
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +71 -50
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
|
427
|
+
Load WORKFLOW.md from .claude-mpm directories.
|
|
353
428
|
|
|
354
|
-
Precedence:
|
|
355
|
-
1. Project-specific:
|
|
356
|
-
2.
|
|
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
|
|
362
|
-
project_workflow_path = Path.cwd() / ".claude-mpm" / "
|
|
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["
|
|
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["
|
|
385
|
-
self.logger.info("Using system
|
|
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
|
|
479
|
+
Load MEMORY.md from .claude-mpm directories.
|
|
390
480
|
|
|
391
|
-
Precedence:
|
|
392
|
-
1. Project-specific:
|
|
393
|
-
2.
|
|
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
|
|
399
|
-
project_memory_path = Path.cwd() / ".claude-mpm" / "
|
|
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["
|
|
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["
|
|
422
|
-
self.logger.info("Using system
|
|
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
|
-
-
|
|
668
|
-
- Preserve all bullet-point items (lines starting with -)
|
|
669
|
-
-
|
|
670
|
-
-
|
|
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
|
|
687
|
-
|
|
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
|
-
|
|
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
|
|
706
|
-
elif line.startswith('
|
|
707
|
-
#
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
#
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
863
|
+
if agent_id:
|
|
864
|
+
lines.append(f"# Agent Memory: {agent_id}")
|
|
865
|
+
else:
|
|
866
|
+
lines.append("# Agent Memory")
|
|
759
867
|
|
|
760
|
-
# Add
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
768
|
-
for
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
"
|
|
962
|
+
"workflow_instructions_level": "", # Track source level
|
|
863
963
|
"memory_instructions": "",
|
|
864
|
-
"
|
|
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
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|