claude-mpm 4.1.3__py3-none-any.whl → 4.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +16 -19
  3. claude_mpm/agents/MEMORY.md +21 -49
  4. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +156 -0
  5. claude_mpm/agents/templates/api_qa.json +36 -116
  6. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +42 -9
  7. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +29 -6
  8. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +34 -6
  9. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +41 -9
  10. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +30 -8
  11. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +2 -2
  12. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +29 -6
  13. claude_mpm/agents/templates/backup/research_memory_efficient.json +2 -2
  14. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +41 -9
  15. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +23 -7
  16. claude_mpm/agents/templates/code_analyzer.json +18 -36
  17. claude_mpm/agents/templates/data_engineer.json +43 -14
  18. claude_mpm/agents/templates/documentation.json +55 -74
  19. claude_mpm/agents/templates/engineer.json +56 -61
  20. claude_mpm/agents/templates/imagemagick.json +7 -2
  21. claude_mpm/agents/templates/memory_manager.json +1 -1
  22. claude_mpm/agents/templates/ops.json +36 -4
  23. claude_mpm/agents/templates/project_organizer.json +23 -71
  24. claude_mpm/agents/templates/qa.json +34 -2
  25. claude_mpm/agents/templates/refactoring_engineer.json +9 -5
  26. claude_mpm/agents/templates/research.json +36 -4
  27. claude_mpm/agents/templates/security.json +29 -2
  28. claude_mpm/agents/templates/ticketing.json +3 -3
  29. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  30. claude_mpm/agents/templates/version_control.json +28 -2
  31. claude_mpm/agents/templates/web_qa.json +38 -151
  32. claude_mpm/agents/templates/web_ui.json +2 -2
  33. claude_mpm/cli/commands/agent_manager.py +221 -1
  34. claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
  35. claude_mpm/core/framework_loader.py +91 -0
  36. claude_mpm/core/log_manager.py +49 -1
  37. claude_mpm/services/memory/router.py +116 -10
  38. {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
  39. {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +43 -42
  40. {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
  41. {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
  42. {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
  43. {claude_mpm-4.1.3.dist-info → claude_mpm-4.1.4.dist-info}/top_level.txt +0 -0
@@ -57,6 +57,7 @@ class AgentManagerCommand(AgentCommand):
57
57
  "show": self._show_agent,
58
58
  "test": self._test_agent,
59
59
  "templates": self._list_templates,
60
+ "reset": self._reset_agents,
60
61
  }
61
62
 
62
63
  command = args.agent_manager_command
@@ -330,6 +331,218 @@ class AgentManagerCommand(AgentCommand):
330
331
  output += f" {template['description']}\n"
331
332
  return CommandResult.success_result(output)
332
333
 
334
+ def _reset_agents(self, args) -> CommandResult:
335
+ """Reset by removing claude-mpm authored agents from project and user directories.
336
+
337
+ This command removes any agents with "author: claude-mpm" in their frontmatter,
338
+ preserving user-created agents. This is useful for clean reinstalls or when
339
+ wanting to get fresh versions of system agents.
340
+ """
341
+ try:
342
+ # Determine which directories to clean
343
+ clean_project = not getattr(args, "user_only", False)
344
+ clean_user = not getattr(args, "project_only", False)
345
+ dry_run = getattr(args, "dry_run", False)
346
+ force = getattr(args, "force", False)
347
+ output_format = getattr(args, "format", "text")
348
+
349
+ # Track results
350
+ results = {
351
+ "project": {"checked": False, "removed": [], "preserved": []},
352
+ "user": {"checked": False, "removed": [], "preserved": []},
353
+ "dry_run": dry_run,
354
+ "total_removed": 0,
355
+ "total_preserved": 0,
356
+ }
357
+
358
+ # Check project directory - always scan first to see what's there
359
+ if clean_project:
360
+ project_dir = Path.cwd() / ".claude" / "agents"
361
+ if project_dir.exists():
362
+ results["project"]["checked"] = True
363
+ # Always scan with dry_run=True first to see what's there
364
+ self._scan_and_clean_directory(
365
+ project_dir, results["project"], dry_run=True
366
+ )
367
+
368
+ # Check user directory - always scan first to see what's there
369
+ if clean_user:
370
+ user_dir = Path.home() / ".claude" / "agents"
371
+ if user_dir.exists():
372
+ results["user"]["checked"] = True
373
+ # Always scan with dry_run=True first to see what's there
374
+ self._scan_and_clean_directory(
375
+ user_dir, results["user"], dry_run=True
376
+ )
377
+
378
+ # Calculate totals
379
+ results["total_removed"] = len(results["project"]["removed"]) + len(
380
+ results["user"]["removed"]
381
+ )
382
+ results["total_preserved"] = len(results["project"]["preserved"]) + len(
383
+ results["user"]["preserved"]
384
+ )
385
+
386
+ # Handle output based on format
387
+ if output_format == "json":
388
+ return CommandResult.success_result("Reset completed", data=results)
389
+
390
+ # Generate text output
391
+ output = self._format_reset_results(results, dry_run, force)
392
+
393
+ # If not dry-run, perform actual removal
394
+ if not dry_run and results["total_removed"] > 0:
395
+ # If force mode, remove immediately; otherwise get confirmation
396
+ if not force:
397
+ # Get confirmation first
398
+ print(output)
399
+ print("\n⚠️ This will permanently remove the agents listed above.")
400
+
401
+ # Ensure stdout is flushed before reading input
402
+ sys.stdout.flush()
403
+
404
+ # Get confirmation
405
+ try:
406
+ response = input("Continue? [y/N]: ").strip().lower()
407
+ if response not in ["y", "yes"]:
408
+ return CommandResult.success_result(
409
+ "Reset cancelled by user"
410
+ )
411
+ except (KeyboardInterrupt, EOFError):
412
+ return CommandResult.success_result("\nReset cancelled")
413
+
414
+ # Perform actual removal using the list we already have
415
+ if clean_project and results["project"]["removed"]:
416
+ project_dir = Path.cwd() / ".claude" / "agents"
417
+ for agent in results["project"]["removed"]:
418
+ agent_file = project_dir / agent
419
+ try:
420
+ if agent_file.exists():
421
+ agent_file.unlink()
422
+ self.logger.info(
423
+ f"Removed claude-mpm agent: {agent_file}"
424
+ )
425
+ except Exception as e:
426
+ self.logger.warning(f"Could not remove {agent_file}: {e}")
427
+
428
+ if clean_user and results["user"]["removed"]:
429
+ user_dir = Path.home() / ".claude" / "agents"
430
+ for agent in results["user"]["removed"]:
431
+ agent_file = user_dir / agent
432
+ try:
433
+ if agent_file.exists():
434
+ agent_file.unlink()
435
+ self.logger.info(
436
+ f"Removed claude-mpm agent: {agent_file}"
437
+ )
438
+ except Exception as e:
439
+ self.logger.warning(f"Could not remove {agent_file}: {e}")
440
+
441
+ # Update output to show actual removal
442
+ output = self._format_reset_results(results, dry_run=False, force=force)
443
+
444
+ return CommandResult.success_result(output)
445
+
446
+ except Exception as e:
447
+ self.logger.error(f"Failed to reset agents: {e}", exc_info=True)
448
+ return CommandResult.error_result(f"Failed to reset agents: {e}")
449
+
450
+ def _scan_and_clean_directory(
451
+ self, directory: Path, results: Dict[str, Any], dry_run: bool
452
+ ) -> None:
453
+ """Scan a directory for claude-mpm authored agents and optionally remove them.
454
+
455
+ Args:
456
+ directory: Directory to scan
457
+ results: Results dictionary to update
458
+ dry_run: If True, only scan without removing
459
+ """
460
+ for agent_file in directory.glob("*.md"):
461
+ try:
462
+ content = agent_file.read_text()
463
+ # Check if this is a claude-mpm authored agent
464
+ if "author: claude-mpm" in content.lower():
465
+ results["removed"].append(agent_file.name)
466
+ if not dry_run:
467
+ agent_file.unlink()
468
+ self.logger.info(f"Removed claude-mpm agent: {agent_file}")
469
+ else:
470
+ results["preserved"].append(agent_file.name)
471
+ self.logger.debug(f"Preserved user agent: {agent_file}")
472
+ except Exception as e:
473
+ self.logger.warning(f"Could not process {agent_file}: {e}")
474
+
475
+ def _format_reset_results(
476
+ self, results: Dict[str, Any], dry_run: bool, force: bool
477
+ ) -> str:
478
+ """Format reset results for display.
479
+
480
+ Args:
481
+ results: Results dictionary
482
+ dry_run: Whether this was a dry run
483
+ force: Whether force mode was used
484
+
485
+ Returns:
486
+ Formatted output string
487
+ """
488
+ if dry_run:
489
+ output = "🔍 DRY RUN - No changes will be made\n"
490
+ output += "=" * 50 + "\n\n"
491
+ else:
492
+ output = "🧹 Agent Reset Complete\n"
493
+ output += "=" * 50 + "\n\n"
494
+
495
+ # Show project results
496
+ if results["project"]["checked"]:
497
+ output += "📁 Project Level (.claude/agents):\n"
498
+ if results["project"]["removed"]:
499
+ action = "Would remove" if dry_run else "Removed"
500
+ output += f" {action} {len(results['project']['removed'])} claude-mpm agent(s):\n"
501
+ for agent in results["project"]["removed"][:5]:
502
+ output += f" • {agent}\n"
503
+ if len(results["project"]["removed"]) > 5:
504
+ output += (
505
+ f" ... and {len(results['project']['removed']) - 5} more\n"
506
+ )
507
+ else:
508
+ output += " No claude-mpm agents found\n"
509
+
510
+ if results["project"]["preserved"]:
511
+ output += f" Preserved {len(results['project']['preserved'])} user-created agent(s)\n"
512
+ output += "\n"
513
+
514
+ # Show user results
515
+ if results["user"]["checked"]:
516
+ output += "📁 User Level (~/.claude/agents):\n"
517
+ if results["user"]["removed"]:
518
+ action = "Would remove" if dry_run else "Removed"
519
+ output += f" {action} {len(results['user']['removed'])} claude-mpm agent(s):\n"
520
+ for agent in results["user"]["removed"][:5]:
521
+ output += f" • {agent}\n"
522
+ if len(results["user"]["removed"]) > 5:
523
+ output += (
524
+ f" ... and {len(results['user']['removed']) - 5} more\n"
525
+ )
526
+ else:
527
+ output += " No claude-mpm agents found\n"
528
+
529
+ if results["user"]["preserved"]:
530
+ output += f" Preserved {len(results['user']['preserved'])} user-created agent(s)\n"
531
+ output += "\n"
532
+
533
+ # Show summary
534
+ output += "📊 Summary:\n"
535
+ if dry_run:
536
+ output += f" • Would remove: {results['total_removed']} agent(s)\n"
537
+ else:
538
+ output += f" • Removed: {results['total_removed']} agent(s)\n"
539
+ output += f" • Preserved: {results['total_preserved']} user agent(s)\n"
540
+
541
+ if dry_run and results["total_removed"] > 0:
542
+ output += "\n💡 Run with --force to execute this cleanup immediately"
543
+
544
+ return output
545
+
333
546
  def _interactive_create(self) -> CommandResult:
334
547
  """Interactive agent creation wizard."""
335
548
  print("\n=== Agent Creation Wizard ===\n")
@@ -493,6 +706,7 @@ Commands:
493
706
  show Display detailed agent information
494
707
  test Validate agent configuration
495
708
  templates List available agent templates
709
+ reset Remove claude-mpm authored agents for clean install
496
710
 
497
711
  Examples:
498
712
  claude-mpm agent-manager list
@@ -500,6 +714,8 @@ Examples:
500
714
  claude-mpm agent-manager variant --base research --id research-v2
501
715
  claude-mpm agent-manager deploy --agent-id my-agent --tier user
502
716
  claude-mpm agent-manager customize-pm --level project
717
+ claude-mpm agent-manager reset --dry-run
718
+ claude-mpm agent-manager reset --force --project-only
503
719
 
504
720
  Note: PM customization writes to .claude-mpm/INSTRUCTIONS.md, not CLAUDE.md
505
721
  """
@@ -520,7 +736,11 @@ def manage_agent_manager(args) -> int:
520
736
  result = command.run(args)
521
737
 
522
738
  if result.success:
523
- if result.message:
739
+ # Handle JSON output format
740
+ output_format = getattr(args, "format", "text")
741
+ if output_format == "json" and result.data is not None:
742
+ print(json.dumps(result.data, indent=2))
743
+ elif result.message:
524
744
  print(result.message)
525
745
  return 0
526
746
  if result.message:
@@ -32,6 +32,9 @@ Examples:
32
32
  claude-mpm agent-manager show --id engineer # Show agent details
33
33
  claude-mpm agent-manager test --id my-agent # Test agent configuration
34
34
  claude-mpm agent-manager templates # List available templates
35
+ claude-mpm agent-manager reset --dry-run # Preview agent cleanup
36
+ claude-mpm agent-manager reset --force # Remove all claude-mpm agents
37
+ claude-mpm agent-manager reset --project-only # Clean only project agents
35
38
  """,
36
39
  )
37
40
 
@@ -197,3 +200,34 @@ Examples:
197
200
  default="text",
198
201
  help="Output format (default: text)",
199
202
  )
203
+
204
+ # Reset command
205
+ reset_parser = agent_subparsers.add_parser(
206
+ "reset", help="Remove claude-mpm authored agents for clean install"
207
+ )
208
+ reset_parser.add_argument(
209
+ "--force",
210
+ action="store_true",
211
+ help="Execute cleanup immediately without confirmation",
212
+ )
213
+ reset_parser.add_argument(
214
+ "--dry-run",
215
+ action="store_true",
216
+ help="Preview what would be removed without making changes",
217
+ )
218
+ reset_parser.add_argument(
219
+ "--project-only",
220
+ action="store_true",
221
+ help="Only clean project-level agents (.claude/agents)",
222
+ )
223
+ reset_parser.add_argument(
224
+ "--user-only",
225
+ action="store_true",
226
+ help="Only clean user-level agents (~/.claude/agents)",
227
+ )
228
+ reset_parser.add_argument(
229
+ "--format",
230
+ choices=["text", "json"],
231
+ default="text",
232
+ help="Output format (default: text)",
233
+ )
@@ -1283,6 +1283,14 @@ Extract tickets from these patterns:
1283
1283
  if agent.get("model") and agent["model"] != "opus":
1284
1284
  section += f"- **Model**: {agent['model']}\n"
1285
1285
 
1286
+ # Add memory routing information if available
1287
+ if agent.get("memory_routing"):
1288
+ memory_routing = agent["memory_routing"]
1289
+ if memory_routing.get("description"):
1290
+ section += (
1291
+ f"- **Memory Routing**: {memory_routing['description']}\n"
1292
+ )
1293
+
1286
1294
  # Add simple Context-Aware Agent Selection
1287
1295
  section += "\n## Context-Aware Agent Selection\n\n"
1288
1296
  section += (
@@ -1384,6 +1392,14 @@ Extract tickets from these patterns:
1384
1392
  if routing_data:
1385
1393
  agent_data["routing"] = routing_data
1386
1394
 
1395
+ # Try to load memory routing metadata from JSON template if not in YAML frontmatter
1396
+ if "memory_routing" not in agent_data:
1397
+ memory_routing_data = self._load_memory_routing_from_template(
1398
+ agent_file.stem
1399
+ )
1400
+ if memory_routing_data:
1401
+ agent_data["memory_routing"] = memory_routing_data
1402
+
1387
1403
  # Cache the parsed metadata
1388
1404
  self._cache_manager.set_agent_metadata(cache_key, agent_data, file_mtime)
1389
1405
 
@@ -1393,6 +1409,81 @@ Extract tickets from these patterns:
1393
1409
  self.logger.debug(f"Could not parse metadata from {agent_file}: {e}")
1394
1410
  return None
1395
1411
 
1412
+ def _load_memory_routing_from_template(
1413
+ self, agent_name: str
1414
+ ) -> Optional[Dict[str, Any]]:
1415
+ """Load memory routing metadata from agent JSON template.
1416
+
1417
+ Args:
1418
+ agent_name: Name of the agent (stem of the file)
1419
+
1420
+ Returns:
1421
+ Dictionary with memory routing metadata or None if not found
1422
+ """
1423
+ try:
1424
+ import json
1425
+
1426
+ # Check if we have a framework path
1427
+ if not self.framework_path or self.framework_path == Path("__PACKAGED__"):
1428
+ # For packaged installations, try to load from package resources
1429
+ if files:
1430
+ try:
1431
+ templates_package = files("claude_mpm.agents.templates")
1432
+ template_file = templates_package / f"{agent_name}.json"
1433
+
1434
+ if template_file.is_file():
1435
+ template_content = template_file.read_text()
1436
+ template_data = json.loads(template_content)
1437
+ return template_data.get("memory_routing")
1438
+ except Exception as e:
1439
+ self.logger.debug(
1440
+ f"Could not load memory routing from packaged template for {agent_name}: {e}"
1441
+ )
1442
+ return None
1443
+
1444
+ # For development mode, load from filesystem
1445
+ templates_dir = (
1446
+ self.framework_path / "src" / "claude_mpm" / "agents" / "templates"
1447
+ )
1448
+ template_file = templates_dir / f"{agent_name}.json"
1449
+
1450
+ if template_file.exists():
1451
+ with open(template_file) as f:
1452
+ template_data = json.load(f)
1453
+ return template_data.get("memory_routing")
1454
+
1455
+ # Also check for variations in naming (underscore vs dash)
1456
+ # Handle common naming variations between deployed .md files and .json templates
1457
+ # Remove duplicates by using a set
1458
+ alternative_names = list(
1459
+ {
1460
+ agent_name.replace("-", "_"), # api-qa -> api_qa
1461
+ agent_name.replace("_", "-"), # api_qa -> api-qa
1462
+ agent_name.replace("-", ""), # api-qa -> apiqa
1463
+ agent_name.replace("_", ""), # api_qa -> apiqa
1464
+ agent_name.replace("-agent", ""), # research-agent -> research
1465
+ agent_name.replace("_agent", ""), # research_agent -> research
1466
+ agent_name + "_agent", # research -> research_agent
1467
+ agent_name + "-agent", # research -> research-agent
1468
+ }
1469
+ )
1470
+
1471
+ for alt_name in alternative_names:
1472
+ if alt_name != agent_name: # Skip the original name we already tried
1473
+ alt_file = templates_dir / f"{alt_name}.json"
1474
+ if alt_file.exists():
1475
+ with open(alt_file) as f:
1476
+ template_data = json.load(f)
1477
+ return template_data.get("memory_routing")
1478
+
1479
+ return None
1480
+
1481
+ except Exception as e:
1482
+ self.logger.debug(
1483
+ f"Could not load memory routing from template for {agent_name}: {e}"
1484
+ )
1485
+ return None
1486
+
1396
1487
  def _load_routing_from_template(self, agent_name: str) -> Optional[Dict[str, Any]]:
1397
1488
  """Load routing metadata from agent JSON template.
1398
1489
 
@@ -179,6 +179,11 @@ class LogManager:
179
179
  # Add to cache
180
180
  self._dir_cache[log_type] = log_dir
181
181
 
182
+ # One-time migration for MPM logs from old location to new subdirectory
183
+ if log_type == "mpm" and not hasattr(self, "_mpm_logs_migrated"):
184
+ await self._migrate_mpm_logs()
185
+ self._mpm_logs_migrated = True
186
+
182
187
  # Schedule cleanup for old logs
183
188
  await self.cleanup_old_logs(
184
189
  log_dir,
@@ -206,7 +211,7 @@ class LogManager:
206
211
  # Map log types to directory names
207
212
  dir_mapping = {
208
213
  "startup": "startup",
209
- "mpm": "", # Root of logs directory
214
+ "mpm": "mpm", # MPM logs in dedicated subdirectory
210
215
  "prompts": "prompts",
211
216
  "sessions": "sessions",
212
217
  "agents": "agents",
@@ -343,6 +348,49 @@ class LogManager:
343
348
 
344
349
  return deleted_count
345
350
 
351
+ async def _migrate_mpm_logs(self):
352
+ """
353
+ One-time migration to move existing MPM logs to new subdirectory.
354
+
355
+ Moves mpm_*.log files from .claude-mpm/logs/ to .claude-mpm/logs/mpm/
356
+ """
357
+ try:
358
+ old_location = self.base_log_dir
359
+ new_location = self.base_log_dir / "mpm"
360
+
361
+ # Only proceed if old location exists and has MPM logs
362
+ if not old_location.exists():
363
+ return
364
+
365
+ # Find all MPM log files in the old location
366
+ mpm_logs = list(old_location.glob("mpm_*.log"))
367
+
368
+ if not mpm_logs:
369
+ return # No logs to migrate
370
+
371
+ # Ensure new directory exists
372
+ new_location.mkdir(parents=True, exist_ok=True)
373
+
374
+ migrated_count = 0
375
+ for log_file in mpm_logs:
376
+ try:
377
+ # Move file to new location
378
+ new_path = new_location / log_file.name
379
+ if not new_path.exists(): # Don't overwrite existing files
380
+ log_file.rename(new_path)
381
+ migrated_count += 1
382
+ except Exception as e:
383
+ logger.debug(f"Could not migrate {log_file}: {e}")
384
+
385
+ if migrated_count > 0:
386
+ logger.info(
387
+ f"Migrated {migrated_count} MPM log files to {new_location}"
388
+ )
389
+
390
+ except Exception as e:
391
+ # Migration is best-effort, don't fail if something goes wrong
392
+ logger.debug(f"MPM log migration skipped: {e}")
393
+
346
394
  async def log_prompt(
347
395
  self, prompt_type: str, content: str, metadata: Optional[Dict[str, Any]] = None
348
396
  ) -> Optional[Path]:
@@ -25,6 +25,7 @@ from datetime import datetime
25
25
  from typing import Any, Dict, List, Optional, Tuple
26
26
 
27
27
  from claude_mpm.core.config import Config
28
+ from claude_mpm.core.framework_loader import FrameworkLoader
28
29
  from claude_mpm.core.mixins import LoggerMixin
29
30
 
30
31
 
@@ -467,6 +468,84 @@ class MemoryRouter(LoggerMixin):
467
468
  """
468
469
  super().__init__()
469
470
  self.config = config or Config()
471
+ self._dynamic_patterns_loaded = False
472
+ self._dynamic_patterns = {}
473
+
474
+ def _load_dynamic_patterns(self) -> None:
475
+ """Load memory routing patterns dynamically from agent templates.
476
+
477
+ WHY: Allows agents to define their own memory routing patterns
478
+ in their template files, making the system more flexible and
479
+ maintainable.
480
+ """
481
+ if self._dynamic_patterns_loaded:
482
+ return
483
+
484
+ try:
485
+ # Initialize framework loader to access agent templates
486
+ framework_loader = FrameworkLoader()
487
+
488
+ # Try to load patterns from deployed agents
489
+ from pathlib import Path
490
+
491
+ # Check both project and user agent directories
492
+ agent_dirs = [
493
+ Path(".claude/agents"), # Project agents
494
+ Path.home() / ".claude-mpm/agents", # User agents
495
+ ]
496
+
497
+ for agent_dir in agent_dirs:
498
+ if not agent_dir.exists():
499
+ continue
500
+
501
+ # Look for deployed agent files
502
+ for agent_file in agent_dir.glob("*.md"):
503
+ agent_name = agent_file.stem
504
+
505
+ # Try to load memory routing from template
506
+ memory_routing = (
507
+ framework_loader._load_memory_routing_from_template(agent_name)
508
+ )
509
+
510
+ if memory_routing:
511
+ # Convert agent name to pattern key format
512
+ # e.g., "research-agent" -> "research"
513
+ pattern_key = (
514
+ agent_name.replace("-agent", "")
515
+ .replace("_agent", "")
516
+ .replace("-", "_")
517
+ )
518
+
519
+ # Build pattern structure from memory routing
520
+ pattern_data = {
521
+ "keywords": memory_routing.get("keywords", []),
522
+ "sections": memory_routing.get("categories", []),
523
+ }
524
+
525
+ # Merge with existing patterns or add new
526
+ if pattern_key in self.AGENT_PATTERNS:
527
+ # Merge keywords, keeping unique values
528
+ existing_keywords = set(
529
+ self.AGENT_PATTERNS[pattern_key]["keywords"]
530
+ )
531
+ new_keywords = set(memory_routing.get("keywords", []))
532
+ pattern_data["keywords"] = list(
533
+ existing_keywords | new_keywords
534
+ )
535
+
536
+ self._dynamic_patterns[pattern_key] = pattern_data
537
+ self.logger.debug(
538
+ f"Loaded dynamic memory routing for {pattern_key}"
539
+ )
540
+
541
+ self._dynamic_patterns_loaded = True
542
+ self.logger.info(
543
+ f"Loaded memory routing patterns for {len(self._dynamic_patterns)} agents"
544
+ )
545
+
546
+ except Exception as e:
547
+ self.logger.warning(f"Could not load dynamic memory routing patterns: {e}")
548
+ self._dynamic_patterns_loaded = True # Don't retry
470
549
 
471
550
  def get_supported_agents(self) -> List[str]:
472
551
  """Get list of supported agent types.
@@ -477,7 +556,12 @@ class MemoryRouter(LoggerMixin):
477
556
  Returns:
478
557
  List of supported agent type names
479
558
  """
480
- return list(self.AGENT_PATTERNS.keys())
559
+ self._load_dynamic_patterns()
560
+
561
+ # Combine static and dynamic patterns
562
+ all_agents = set(self.AGENT_PATTERNS.keys())
563
+ all_agents.update(self._dynamic_patterns.keys())
564
+ return list(all_agents)
481
565
 
482
566
  def is_agent_supported(self, agent_type: str) -> bool:
483
567
  """Check if an agent type is supported by the memory router.
@@ -491,7 +575,8 @@ class MemoryRouter(LoggerMixin):
491
575
  Returns:
492
576
  True if agent type is supported, False otherwise
493
577
  """
494
- return agent_type in self.AGENT_PATTERNS
578
+ self._load_dynamic_patterns()
579
+ return agent_type in self.AGENT_PATTERNS or agent_type in self._dynamic_patterns
495
580
 
496
581
  def analyze_and_route(
497
582
  self, content: str, context: Optional[Dict] = None
@@ -605,21 +690,30 @@ class MemoryRouter(LoggerMixin):
605
690
  Returns:
606
691
  Dict containing routing patterns and statistics
607
692
  """
693
+ self._load_dynamic_patterns()
694
+
695
+ # Combine static and dynamic patterns
696
+ all_patterns = dict(self.AGENT_PATTERNS)
697
+ all_patterns.update(self._dynamic_patterns)
698
+
608
699
  return {
609
- "agents": list(self.AGENT_PATTERNS.keys()),
700
+ "agents": list(all_patterns.keys()),
610
701
  "default_agent": self.DEFAULT_AGENT,
702
+ "static_agents": list(self.AGENT_PATTERNS.keys()),
703
+ "dynamic_agents": list(self._dynamic_patterns.keys()),
611
704
  "patterns": {
612
705
  agent: {
613
706
  "keyword_count": len(patterns["keywords"]),
614
707
  "section_count": len(patterns["sections"]),
615
708
  "keywords": patterns["keywords"][:10], # Show first 10
616
709
  "sections": patterns["sections"],
710
+ "source": (
711
+ "dynamic" if agent in self._dynamic_patterns else "static"
712
+ ),
617
713
  }
618
- for agent, patterns in self.AGENT_PATTERNS.items()
714
+ for agent, patterns in all_patterns.items()
619
715
  },
620
- "total_keywords": sum(
621
- len(p["keywords"]) for p in self.AGENT_PATTERNS.values()
622
- ),
716
+ "total_keywords": sum(len(p["keywords"]) for p in all_patterns.values()),
623
717
  }
624
718
 
625
719
  def _normalize_content(self, content: str) -> str:
@@ -663,9 +757,14 @@ class MemoryRouter(LoggerMixin):
663
757
  Returns:
664
758
  Dict mapping agent names to relevance scores
665
759
  """
760
+ self._load_dynamic_patterns()
666
761
  scores = {}
667
762
 
668
- for agent, patterns in self.AGENT_PATTERNS.items():
763
+ # Combine static and dynamic patterns
764
+ all_patterns = dict(self.AGENT_PATTERNS)
765
+ all_patterns.update(self._dynamic_patterns)
766
+
767
+ for agent, patterns in all_patterns.items():
669
768
  score = 0.0
670
769
  matched_keywords = []
671
770
 
@@ -773,10 +872,17 @@ class MemoryRouter(LoggerMixin):
773
872
  Returns:
774
873
  Section name for memory storage
775
874
  """
776
- if agent not in self.AGENT_PATTERNS:
875
+ self._load_dynamic_patterns()
876
+
877
+ # Check both static and dynamic patterns
878
+ if agent in self.AGENT_PATTERNS:
879
+ sections = self.AGENT_PATTERNS[agent]["sections"]
880
+ elif agent in self._dynamic_patterns:
881
+ sections = self._dynamic_patterns[agent]["sections"]
882
+ else:
777
883
  return "Recent Learnings"
778
884
 
779
- sections = self.AGENT_PATTERNS[agent]["sections"]
885
+ sections = sections if sections else []
780
886
 
781
887
  # Simple heuristics for section selection
782
888
  if "mistake" in content or "error" in content or "avoid" in content:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.1.3
3
+ Version: 4.1.4
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team