claude-mpm 4.1.4__py3-none-any.whl → 4.1.6__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 (81) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/research.json +39 -13
  3. claude_mpm/cli/__init__.py +2 -0
  4. claude_mpm/cli/commands/__init__.py +2 -0
  5. claude_mpm/cli/commands/configure.py +1221 -0
  6. claude_mpm/cli/commands/configure_tui.py +1921 -0
  7. claude_mpm/cli/commands/tickets.py +365 -784
  8. claude_mpm/cli/parsers/base_parser.py +7 -0
  9. claude_mpm/cli/parsers/configure_parser.py +119 -0
  10. claude_mpm/cli/startup_logging.py +39 -12
  11. claude_mpm/constants.py +1 -0
  12. claude_mpm/core/output_style_manager.py +24 -0
  13. claude_mpm/core/socketio_pool.py +35 -3
  14. claude_mpm/core/unified_agent_registry.py +46 -15
  15. claude_mpm/dashboard/static/css/connection-status.css +370 -0
  16. claude_mpm/dashboard/static/js/components/connection-debug.js +654 -0
  17. claude_mpm/dashboard/static/js/connection-manager.js +536 -0
  18. claude_mpm/dashboard/templates/index.html +11 -0
  19. claude_mpm/hooks/claude_hooks/services/__init__.py +3 -1
  20. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +190 -0
  21. claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
  22. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
  23. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
  24. claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
  25. claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
  26. claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
  27. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
  28. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  29. claude_mpm/services/diagnostics/checks/instructions_check.py +418 -0
  30. claude_mpm/services/diagnostics/diagnostic_runner.py +15 -2
  31. claude_mpm/services/event_bus/direct_relay.py +173 -0
  32. claude_mpm/services/infrastructure/__init__.py +31 -5
  33. claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
  34. claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
  35. claude_mpm/services/infrastructure/monitoring/base.py +130 -0
  36. claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
  37. claude_mpm/services/infrastructure/monitoring/network.py +218 -0
  38. claude_mpm/services/infrastructure/monitoring/process.py +342 -0
  39. claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
  40. claude_mpm/services/infrastructure/monitoring/service.py +367 -0
  41. claude_mpm/services/infrastructure/monitoring.py +67 -1030
  42. claude_mpm/services/project/analyzer.py +13 -4
  43. claude_mpm/services/project/analyzer_refactored.py +450 -0
  44. claude_mpm/services/project/analyzer_v2.py +566 -0
  45. claude_mpm/services/project/architecture_analyzer.py +461 -0
  46. claude_mpm/services/project/dependency_analyzer.py +462 -0
  47. claude_mpm/services/project/language_analyzer.py +265 -0
  48. claude_mpm/services/project/metrics_collector.py +410 -0
  49. claude_mpm/services/socketio/handlers/connection_handler.py +345 -0
  50. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  51. claude_mpm/services/socketio/server/connection_manager.py +516 -0
  52. claude_mpm/services/socketio/server/core.py +63 -0
  53. claude_mpm/services/socketio/server/eventbus_integration.py +20 -9
  54. claude_mpm/services/socketio/server/main.py +27 -1
  55. claude_mpm/services/ticket_manager.py +5 -1
  56. claude_mpm/services/ticket_services/__init__.py +26 -0
  57. claude_mpm/services/ticket_services/crud_service.py +328 -0
  58. claude_mpm/services/ticket_services/formatter_service.py +290 -0
  59. claude_mpm/services/ticket_services/search_service.py +324 -0
  60. claude_mpm/services/ticket_services/validation_service.py +303 -0
  61. claude_mpm/services/ticket_services/workflow_service.py +244 -0
  62. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/METADATA +3 -1
  63. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/RECORD +67 -46
  64. claude_mpm/agents/OUTPUT_STYLE.md +0 -73
  65. claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
  66. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +0 -156
  67. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -79
  68. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -68
  69. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -77
  70. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -78
  71. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -67
  72. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +0 -88
  73. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -72
  74. claude_mpm/agents/templates/backup/research_memory_efficient.json +0 -88
  75. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -78
  76. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -62
  77. claude_mpm/agents/templates/vercel_ops_instructions.md +0 -582
  78. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/WHEEL +0 -0
  79. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/entry_points.txt +0 -0
  80. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/licenses/LICENSE +0 -0
  81. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/top_level.txt +0 -0
@@ -331,6 +331,13 @@ def create_parser(
331
331
  except ImportError:
332
332
  pass
333
333
 
334
+ try:
335
+ from .configure_parser import add_configure_subparser
336
+
337
+ add_configure_subparser(subparsers)
338
+ except ImportError:
339
+ pass
340
+
334
341
  # Import and add additional command parsers from commands module
335
342
  try:
336
343
  from ..commands.aggregate import add_aggregate_parser
@@ -0,0 +1,119 @@
1
+ """
2
+ Configure command parser for claude-mpm CLI.
3
+
4
+ WHY: This module contains all arguments specific to the interactive configuration
5
+ management interface, allowing users to enable/disable agents, edit templates,
6
+ and manage behavior files.
7
+ """
8
+
9
+ import argparse
10
+
11
+ from ...constants import CLICommands
12
+ from .base_parser import add_common_arguments
13
+
14
+
15
+ def add_configure_subparser(subparsers) -> argparse.ArgumentParser:
16
+ """
17
+ Add the configure subparser for interactive configuration management.
18
+
19
+ WHY: Users need an interactive way to manage agent configurations,
20
+ templates, and behavior files through a terminal-based interface.
21
+
22
+ Args:
23
+ subparsers: The subparsers object from the main parser
24
+
25
+ Returns:
26
+ The configured configure subparser
27
+ """
28
+ # Configure command - interactive TUI configuration
29
+ configure_parser = subparsers.add_parser(
30
+ CLICommands.CONFIGURE.value,
31
+ help="Interactive terminal-based configuration interface for managing agents and behaviors",
32
+ description="Launch an interactive Rich-based TUI for configuring claude-mpm agents, templates, and behavior files",
33
+ )
34
+
35
+ # Add common arguments
36
+ add_common_arguments(configure_parser)
37
+
38
+ # Configuration scope options
39
+ scope_group = configure_parser.add_argument_group("configuration scope")
40
+ scope_group.add_argument(
41
+ "--scope",
42
+ choices=["project", "user"],
43
+ default="project",
44
+ help="Configuration scope to manage (default: project)",
45
+ )
46
+ # Note: --project-dir is already defined in base_parser.py
47
+
48
+ # Direct navigation options (skip main menu)
49
+ nav_group = configure_parser.add_argument_group("direct navigation")
50
+ nav_group.add_argument(
51
+ "--agents", action="store_true", help="Jump directly to agent management"
52
+ )
53
+ nav_group.add_argument(
54
+ "--templates", action="store_true", help="Jump directly to template editing"
55
+ )
56
+ nav_group.add_argument(
57
+ "--behaviors",
58
+ action="store_true",
59
+ help="Jump directly to behavior file management",
60
+ )
61
+ nav_group.add_argument(
62
+ "--version-info",
63
+ action="store_true",
64
+ help="Display version information and exit",
65
+ )
66
+
67
+ # Non-interactive options
68
+ noninteractive_group = configure_parser.add_argument_group(
69
+ "non-interactive options"
70
+ )
71
+ noninteractive_group.add_argument(
72
+ "--list-agents", action="store_true", help="List all available agents and exit"
73
+ )
74
+ noninteractive_group.add_argument(
75
+ "--enable-agent",
76
+ type=str,
77
+ metavar="AGENT_NAME",
78
+ help="Enable a specific agent and exit",
79
+ )
80
+ noninteractive_group.add_argument(
81
+ "--disable-agent",
82
+ type=str,
83
+ metavar="AGENT_NAME",
84
+ help="Disable a specific agent and exit",
85
+ )
86
+ noninteractive_group.add_argument(
87
+ "--export-config",
88
+ type=str,
89
+ metavar="FILE",
90
+ help="Export current configuration to a file",
91
+ )
92
+ noninteractive_group.add_argument(
93
+ "--import-config",
94
+ type=str,
95
+ metavar="FILE",
96
+ help="Import configuration from a file",
97
+ )
98
+
99
+ # Display options
100
+ display_group = configure_parser.add_argument_group("display options")
101
+ display_group.add_argument(
102
+ "--no-colors", action="store_true", help="Disable colored output in the TUI"
103
+ )
104
+ display_group.add_argument(
105
+ "--compact", action="store_true", help="Use compact display mode"
106
+ )
107
+ display_group.add_argument(
108
+ "--force-rich",
109
+ action="store_true",
110
+ help="Force use of Rich menu interface instead of full-screen Textual TUI",
111
+ )
112
+ display_group.add_argument(
113
+ "--no-textual",
114
+ dest="use_textual",
115
+ action="store_false",
116
+ help="Disable Textual TUI and use Rich menu interface",
117
+ )
118
+
119
+ return configure_parser
@@ -57,17 +57,34 @@ def log_memory_stats(logger=None, prefix="Memory Usage"):
57
57
  rss_mb = memory_info.rss / (1024 * 1024)
58
58
  vms_mb = memory_info.vms / (1024 * 1024)
59
59
 
60
- # Get percentage of system memory if available
61
- try:
62
- memory_percent = process.memory_percent()
63
- logger.info(
64
- f"{prefix}: RSS={rss_mb:.1f}MB, VMS={vms_mb:.1f}MB, "
65
- f"System={memory_percent:.1f}%"
66
- )
67
- return {"rss_mb": rss_mb, "vms_mb": vms_mb, "percent": memory_percent}
68
- except:
69
- logger.info(f"{prefix}: RSS={rss_mb:.1f}MB, VMS={vms_mb:.1f}MB")
70
- return {"rss_mb": rss_mb, "vms_mb": vms_mb, "percent": None}
60
+ # On macOS, VMS can report misleading values (400+ TB)
61
+ # Skip VMS reporting if it's unreasonably large
62
+ import platform
63
+
64
+ if platform.system() == "Darwin" and vms_mb > 100000: # > 100GB is suspicious
65
+ # Get percentage of system memory if available
66
+ try:
67
+ memory_percent = process.memory_percent()
68
+ logger.info(
69
+ f"{prefix}: RSS={rss_mb:.1f}MB, System={memory_percent:.1f}%"
70
+ )
71
+ return {"rss_mb": rss_mb, "vms_mb": None, "percent": memory_percent}
72
+ except:
73
+ logger.info(f"{prefix}: RSS={rss_mb:.1f}MB")
74
+ return {"rss_mb": rss_mb, "vms_mb": None, "percent": None}
75
+ else:
76
+ # Normal VMS reporting for non-macOS or reasonable values
77
+ # Get percentage of system memory if available
78
+ try:
79
+ memory_percent = process.memory_percent()
80
+ logger.info(
81
+ f"{prefix}: RSS={rss_mb:.1f}MB, VMS={vms_mb:.1f}MB, "
82
+ f"System={memory_percent:.1f}%"
83
+ )
84
+ return {"rss_mb": rss_mb, "vms_mb": vms_mb, "percent": memory_percent}
85
+ except:
86
+ logger.info(f"{prefix}: RSS={rss_mb:.1f}MB, VMS={vms_mb:.1f}MB")
87
+ return {"rss_mb": rss_mb, "vms_mb": vms_mb, "percent": None}
71
88
 
72
89
  except Exception as e:
73
90
  logger.debug(f"Failed to get memory info: {e}")
@@ -512,11 +529,21 @@ def setup_startup_logging(project_root: Optional[Path] = None) -> Path:
512
529
  # Log initial memory usage
513
530
  if PSUTIL_AVAILABLE:
514
531
  try:
532
+ import platform
533
+
515
534
  process = psutil.Process()
516
535
  memory_info = process.memory_info()
517
536
  rss_mb = memory_info.rss / (1024 * 1024)
518
537
  vms_mb = memory_info.vms / (1024 * 1024)
519
- logger.info(f"Initial Memory: RSS={rss_mb:.1f}MB, VMS={vms_mb:.1f}MB")
538
+
539
+ # On macOS, VMS can report misleading values (400+ TB)
540
+ # Skip VMS reporting if it's unreasonably large
541
+ if (
542
+ platform.system() == "Darwin" and vms_mb > 100000
543
+ ): # > 100GB is suspicious
544
+ logger.info(f"Initial Memory: RSS={rss_mb:.1f}MB")
545
+ else:
546
+ logger.info(f"Initial Memory: RSS={rss_mb:.1f}MB, VMS={vms_mb:.1f}MB")
520
547
  except Exception as e:
521
548
  logger.debug(f"Failed to get initial memory info: {e}")
522
549
 
claude_mpm/constants.py CHANGED
@@ -36,6 +36,7 @@ class CLICommands(str, Enum):
36
36
  MEMORY = "memory"
37
37
  MONITOR = "monitor"
38
38
  CONFIG = "config"
39
+ CONFIGURE = "configure"
39
40
  AGGREGATE = "aggregate"
40
41
  CLEANUP = "cleanup-memory"
41
42
  MCP = "mcp"
@@ -21,6 +21,10 @@ from ..utils.imports import safe_import
21
21
  # Import with fallback support
22
22
  get_logger = safe_import("claude_mpm.core.logger", "core.logger", ["get_logger"])
23
23
 
24
+ # Global cache for Claude version to avoid duplicate detection/logging
25
+ _CACHED_CLAUDE_VERSION: Optional[str] = None
26
+ _VERSION_DETECTED: bool = False
27
+
24
28
 
25
29
  class OutputStyleManager:
26
30
  """Manages output style deployment and version-based handling."""
@@ -41,10 +45,17 @@ class OutputStyleManager:
41
45
  def _detect_claude_version(self) -> Optional[str]:
42
46
  """
43
47
  Detect Claude Code version by running 'claude --version'.
48
+ Uses global cache to avoid duplicate detection and logging.
44
49
 
45
50
  Returns:
46
51
  Version string (e.g., "1.0.82") or None if Claude not found
47
52
  """
53
+ global _CACHED_CLAUDE_VERSION, _VERSION_DETECTED
54
+
55
+ # Return cached version if already detected
56
+ if _VERSION_DETECTED:
57
+ return _CACHED_CLAUDE_VERSION
58
+
48
59
  try:
49
60
  # Run claude --version command
50
61
  result = subprocess.run(
@@ -57,6 +68,8 @@ class OutputStyleManager:
57
68
 
58
69
  if result.returncode != 0:
59
70
  self.logger.warning(f"Claude command failed: {result.stderr}")
71
+ _VERSION_DETECTED = True
72
+ _CACHED_CLAUDE_VERSION = None
60
73
  return None
61
74
 
62
75
  # Parse version from output
@@ -66,19 +79,30 @@ class OutputStyleManager:
66
79
 
67
80
  if version_match:
68
81
  version = version_match.group(1)
82
+ # Only log on first detection
69
83
  self.logger.info(f"Detected Claude version: {version}")
84
+ _CACHED_CLAUDE_VERSION = version
85
+ _VERSION_DETECTED = True
70
86
  return version
71
87
  self.logger.warning(f"Could not parse version from: {version_output}")
88
+ _VERSION_DETECTED = True
89
+ _CACHED_CLAUDE_VERSION = None
72
90
  return None
73
91
 
74
92
  except FileNotFoundError:
75
93
  self.logger.info("Claude Code not found in PATH")
94
+ _VERSION_DETECTED = True
95
+ _CACHED_CLAUDE_VERSION = None
76
96
  return None
77
97
  except subprocess.TimeoutExpired:
78
98
  self.logger.warning("Claude version check timed out")
99
+ _VERSION_DETECTED = True
100
+ _CACHED_CLAUDE_VERSION = None
79
101
  return None
80
102
  except Exception as e:
81
103
  self.logger.warning(f"Error detecting Claude version: {e}")
104
+ _VERSION_DETECTED = True
105
+ _CACHED_CLAUDE_VERSION = None
82
106
  return None
83
107
 
84
108
  def _compare_versions(self, version1: str, version2: str) -> int:
@@ -592,7 +592,7 @@ class SocketIOConnectionPool:
592
592
 
593
593
  # 2-second timeout for connection
594
594
  await asyncio.wait_for(
595
- client.connect(self.server_url, auth={"token": "dev-token"}, wait=True),
595
+ client.connect(self.server_url, wait=True),
596
596
  timeout=2.0,
597
597
  )
598
598
 
@@ -734,8 +734,8 @@ class SocketIOConnectionPool:
734
734
  def emit(self, event: str, data: Dict[str, Any]) -> bool:
735
735
  """Emit an event through the connection pool.
736
736
 
737
- This method provides compatibility for the legacy emit() interface
738
- by mapping to the modern emit_event() method with appropriate defaults.
737
+ This method provides compatibility for the legacy emit() interface.
738
+ For critical hook events, we use direct emission to avoid batching delays.
739
739
 
740
740
  Args:
741
741
  event: Event name (e.g., "claude_event")
@@ -747,10 +747,42 @@ class SocketIOConnectionPool:
747
747
  if not SOCKETIO_AVAILABLE or not self._running:
748
748
  return False
749
749
 
750
+ # For critical claude_event, use direct emission to avoid batching delays
751
+ if event == "claude_event":
752
+ return self._emit_direct(event, data)
753
+
750
754
  # Map to the modern emit_event method using default namespace
751
755
  self.emit_event("/", event, data)
752
756
  return True
753
757
 
758
+ def _emit_direct(self, event: str, data: Dict[str, Any]) -> bool:
759
+ """Emit an event directly without batching.
760
+
761
+ This is used for critical events that need immediate delivery.
762
+ """
763
+ try:
764
+ # Create a synchronous client for direct emission
765
+ import socketio
766
+
767
+ client = socketio.Client(logger=False, engineio_logger=False)
768
+
769
+ # Quick connect, emit, and disconnect
770
+ client.connect(self.server_url, wait=True, wait_timeout=1.0)
771
+ client.emit(event, data)
772
+ client.disconnect()
773
+
774
+ # Update stats
775
+ for stats in self.connection_stats.values():
776
+ stats.events_sent += 1
777
+ break
778
+
779
+ return True
780
+ except Exception as e:
781
+ self.logger.debug(f"Direct emit failed: {e}")
782
+ # Fall back to batched emission
783
+ self.emit_event("/", event, data)
784
+ return True
785
+
754
786
  def get_stats(self) -> Dict[str, Any]:
755
787
  """Get connection pool statistics."""
756
788
  with self.pool_lock:
@@ -136,7 +136,13 @@ class UnifiedAgentRegistry:
136
136
 
137
137
  # Discovery configuration
138
138
  self.file_extensions = {".md", ".json", ".yaml", ".yml"}
139
- self.ignore_patterns = {"__pycache__", ".git", "node_modules", ".pytest_cache"}
139
+ self.ignore_patterns = {
140
+ "__pycache__",
141
+ ".git",
142
+ "node_modules",
143
+ ".pytest_cache",
144
+ "backup",
145
+ }
140
146
 
141
147
  # Statistics
142
148
  self.discovery_stats = {
@@ -166,15 +172,15 @@ class UnifiedAgentRegistry:
166
172
  if user_path.exists():
167
173
  self.discovery_paths.append(user_path)
168
174
 
169
- # System-level agents
175
+ # System-level agents (includes templates as a subdirectory)
170
176
  system_path = self.path_manager.get_system_agents_dir()
171
177
  if system_path.exists():
172
178
  self.discovery_paths.append(system_path)
173
179
 
174
- # Templates directory
175
- templates_path = self.path_manager.get_templates_dir()
176
- if templates_path.exists():
177
- self.discovery_paths.append(templates_path)
180
+ # NOTE: Templates directory is NOT added separately because:
181
+ # - templates_path = system_path / "templates"
182
+ # - The rglob("*") in _discover_path will already find templates
183
+ # - Adding it separately causes duplicate discovery
178
184
 
179
185
  logger.debug(
180
186
  f"Discovery paths configured: {[str(p) for p in self.discovery_paths]}"
@@ -256,7 +262,10 @@ class UnifiedAgentRegistry:
256
262
  try:
257
263
  metadata = self._create_agent_metadata(file_path, agent_name, tier)
258
264
  if metadata:
259
- self.registry[agent_name] = metadata
265
+ # Store all discovered agents temporarily for tier precedence
266
+ # Use a unique key that includes tier to prevent overwrites
267
+ registry_key = f"{agent_name}_{tier.value}"
268
+ self.registry[registry_key] = metadata
260
269
  self.discovered_files.add(file_path)
261
270
  logger.debug(
262
271
  f"Discovered agent: {agent_name} ({tier.value}) at {file_path}"
@@ -269,9 +278,29 @@ class UnifiedAgentRegistry:
269
278
  # Remove extension and use filename as agent name
270
279
  name = file_path.stem
271
280
 
272
- # Skip certain files
273
- skip_files = {"README", "INSTRUCTIONS", "template", "example"}
274
- if name.upper() in skip_files:
281
+ # Skip certain files and non-agent templates
282
+ skip_files = {
283
+ "README",
284
+ "INSTRUCTIONS",
285
+ "template",
286
+ "example",
287
+ "base_agent",
288
+ "base_agent_template",
289
+ "agent_template",
290
+ "agent_schema",
291
+ "base_pm",
292
+ "workflow",
293
+ "output_style",
294
+ "memory",
295
+ "optimization_report",
296
+ "vercel_ops_instructions",
297
+ "agent-template",
298
+ "agent-schema", # Also handle hyphenated versions
299
+ }
300
+ # Case-insensitive comparison
301
+ if name.replace("-", "_").upper() in {
302
+ s.replace("-", "_").upper() for s in skip_files
303
+ }:
275
304
  return None
276
305
 
277
306
  # Normalize name
@@ -424,12 +453,14 @@ class UnifiedAgentRegistry:
424
453
 
425
454
  def _apply_tier_precedence(self) -> None:
426
455
  """Apply tier precedence rules to resolve conflicts."""
427
- # Group agents by name
456
+ # Group agents by their actual name (without tier suffix)
428
457
  agent_groups = {}
429
- for name, metadata in self.registry.items():
430
- if name not in agent_groups:
431
- agent_groups[name] = []
432
- agent_groups[name].append(metadata)
458
+ for registry_key, metadata in self.registry.items():
459
+ # Extract the actual agent name (registry_key is "name_tier")
460
+ agent_name = metadata.name # Use the actual name from metadata
461
+ if agent_name not in agent_groups:
462
+ agent_groups[agent_name] = []
463
+ agent_groups[agent_name].append(metadata)
433
464
 
434
465
  # Resolve conflicts using tier precedence
435
466
  resolved_registry = {}