claude-mpm 4.1.2__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 (87) 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 +57 -40
  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/commands/agents.py +556 -1009
  35. claude_mpm/cli/commands/memory.py +248 -927
  36. claude_mpm/cli/commands/run.py +139 -484
  37. claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
  38. claude_mpm/cli/startup_logging.py +76 -0
  39. claude_mpm/core/agent_registry.py +6 -10
  40. claude_mpm/core/framework_loader.py +205 -595
  41. claude_mpm/core/log_manager.py +49 -1
  42. claude_mpm/core/logging_config.py +2 -4
  43. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  44. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  45. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  47. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  48. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  49. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  50. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  51. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  52. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  53. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  54. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  55. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  56. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  57. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  58. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  59. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  60. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  61. claude_mpm/services/agents/registry/__init__.py +1 -1
  62. claude_mpm/services/cli/__init__.py +18 -0
  63. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  64. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  65. claude_mpm/services/cli/agent_listing_service.py +463 -0
  66. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  67. claude_mpm/services/cli/agent_validation_service.py +589 -0
  68. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  69. claude_mpm/services/cli/memory_crud_service.py +617 -0
  70. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  71. claude_mpm/services/cli/session_manager.py +513 -0
  72. claude_mpm/services/cli/socketio_manager.py +498 -0
  73. claude_mpm/services/cli/startup_checker.py +370 -0
  74. claude_mpm/services/core/cache_manager.py +311 -0
  75. claude_mpm/services/core/memory_manager.py +637 -0
  76. claude_mpm/services/core/path_resolver.py +498 -0
  77. claude_mpm/services/core/service_container.py +520 -0
  78. claude_mpm/services/core/service_interfaces.py +436 -0
  79. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  80. claude_mpm/services/memory/router.py +116 -10
  81. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
  82. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +86 -55
  83. claude_mpm/cli/commands/run_config_checker.py +0 -159
  84. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
  85. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
  86. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
  87. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,637 @@
1
+ """
2
+ Memory Management Service
3
+ =========================
4
+
5
+ WHY: Centralizes all memory-related operations that were previously scattered
6
+ across the framework_loader.py god class. This service handles loading,
7
+ aggregation, deduplication, and migration of agent memories.
8
+
9
+ DESIGN DECISION:
10
+ - Extracted from framework_loader.py to follow Single Responsibility Principle
11
+ - Uses dependency injection for ICacheManager and IPathResolver
12
+ - Maintains backward compatibility with legacy memory file formats
13
+ - Implements proper memory precedence: project > user > system
14
+
15
+ ARCHITECTURE:
16
+ - IMemoryManager interface implementation
17
+ - Loads PM memories and agent-specific memories
18
+ - Handles memory aggregation and deduplication
19
+ - Migrates legacy memory formats automatically
20
+ - Uses caching for performance optimization
21
+ """
22
+
23
+ import logging
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional, Set
27
+
28
+ from ...core.logger import get_logger
29
+ from .service_interfaces import ICacheManager, IMemoryManager, IPathResolver
30
+
31
+
32
+ class MemoryManager(IMemoryManager):
33
+ """
34
+ Memory management service for agent memories.
35
+
36
+ This service handles:
37
+ 1. Loading PM memories from PM_memories.md files
38
+ 2. Loading agent-specific memories (only for deployed agents)
39
+ 3. Memory aggregation and deduplication
40
+ 4. Legacy format migration (e.g., PM.md -> PM_memories.md)
41
+ 5. Memory caching for performance
42
+
43
+ Memory Loading Order:
44
+ - User-level memories: ~/.claude-mpm/memories/ (global defaults)
45
+ - Project-level memories: ./.claude-mpm/memories/ (overrides user)
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ cache_manager: ICacheManager,
51
+ path_resolver: IPathResolver,
52
+ logger: Optional[logging.Logger] = None,
53
+ ):
54
+ """
55
+ Initialize memory manager.
56
+
57
+ Args:
58
+ cache_manager: Cache manager for memory caching
59
+ path_resolver: Path resolver for finding memory directories
60
+ logger: Optional logger instance
61
+ """
62
+ self._cache_manager = cache_manager
63
+ self._path_resolver = path_resolver
64
+ self.logger = logger or get_logger("memory_manager")
65
+
66
+ # Memory statistics
67
+ self._stats = {
68
+ "loaded_count": 0,
69
+ "skipped_count": 0,
70
+ "cache_hits": 0,
71
+ "cache_misses": 0,
72
+ }
73
+
74
+ def load_memories(self, agent_name: Optional[str] = None) -> Dict[str, Any]:
75
+ """
76
+ Load memories for an agent or all agents.
77
+
78
+ Args:
79
+ agent_name: Specific agent name or None for all
80
+
81
+ Returns:
82
+ Dictionary containing:
83
+ - actual_memories: PM memories content
84
+ - agent_memories: Dict of agent-specific memories
85
+ """
86
+ # Try to get from cache first
87
+ cached_memories = self._cache_manager.get_memories()
88
+ if cached_memories is not None:
89
+ self._stats["cache_hits"] += 1
90
+ self.logger.debug("Memory cache hit")
91
+
92
+ # Filter for specific agent if requested
93
+ if agent_name and "agent_memories" in cached_memories:
94
+ if agent_name in cached_memories["agent_memories"]:
95
+ return {
96
+ "actual_memories": cached_memories.get("actual_memories", ""),
97
+ "agent_memories": {
98
+ agent_name: cached_memories["agent_memories"][agent_name]
99
+ },
100
+ }
101
+ return {
102
+ "actual_memories": cached_memories.get("actual_memories", ""),
103
+ "agent_memories": {},
104
+ }
105
+
106
+ return cached_memories
107
+
108
+ # Cache miss - perform actual loading
109
+ self._stats["cache_misses"] += 1
110
+ self.logger.debug("Loading memories from disk (cache miss)")
111
+
112
+ # Reset statistics for this load
113
+ self._stats["loaded_count"] = 0
114
+ self._stats["skipped_count"] = 0
115
+
116
+ # Get deployed agents set (needed to filter agent memories)
117
+ deployed_agents = self._get_deployed_agents()
118
+
119
+ # Load memories from both user and project directories
120
+ result = self._load_actual_memories(deployed_agents)
121
+
122
+ # Cache the loaded memories
123
+ self._cache_manager.set_memories(result)
124
+
125
+ # Filter for specific agent if requested
126
+ if agent_name and "agent_memories" in result:
127
+ if agent_name in result["agent_memories"]:
128
+ return {
129
+ "actual_memories": result.get("actual_memories", ""),
130
+ "agent_memories": {
131
+ agent_name: result["agent_memories"][agent_name]
132
+ },
133
+ }
134
+ return {
135
+ "actual_memories": result.get("actual_memories", ""),
136
+ "agent_memories": {},
137
+ }
138
+
139
+ return result
140
+
141
+ def save_memory(
142
+ self, key: str, value: Any, agent_name: Optional[str] = None
143
+ ) -> None:
144
+ """
145
+ Save a memory entry.
146
+
147
+ Args:
148
+ key: Memory key
149
+ value: Memory value
150
+ agent_name: Agent name or None for global
151
+ """
152
+ # Determine target file
153
+ project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
154
+ self._path_resolver.ensure_directory(project_memories_dir)
155
+
156
+ if agent_name:
157
+ memory_file = project_memories_dir / f"{agent_name}_memories.md"
158
+ else:
159
+ memory_file = project_memories_dir / "PM_memories.md"
160
+
161
+ # Load existing content or create new
162
+ if memory_file.exists():
163
+ content = memory_file.read_text(encoding="utf-8")
164
+ lines = content.split("\n")
165
+ else:
166
+ lines = [
167
+ f"# {'Agent Memory: ' + agent_name if agent_name else 'PM Memory'}",
168
+ "",
169
+ ]
170
+
171
+ # Add new memory as a bullet point
172
+ timestamp = datetime.now().isoformat()
173
+ lines.append(f"- [{timestamp}] {key}: {value}")
174
+
175
+ # Write back
176
+ memory_file.write_text("\n".join(lines), encoding="utf-8")
177
+
178
+ # Clear cache to force reload on next access
179
+ self._cache_manager.clear_memory_caches()
180
+
181
+ self.logger.info(f"Saved memory to {memory_file.name}")
182
+
183
+ def search_memories(
184
+ self, query: str, agent_name: Optional[str] = None
185
+ ) -> List[Dict[str, Any]]:
186
+ """
187
+ Search memories by query.
188
+
189
+ Args:
190
+ query: Search query
191
+ agent_name: Specific agent or None for all
192
+
193
+ Returns:
194
+ List of matching memory entries
195
+ """
196
+ memories = self.load_memories(agent_name)
197
+ results = []
198
+
199
+ query_lower = query.lower()
200
+
201
+ # Search in PM memories
202
+ if memories.get("actual_memories"):
203
+ for line in memories["actual_memories"].split("\n"):
204
+ if line.strip().startswith("-") and query_lower in line.lower():
205
+ results.append(
206
+ {"type": "PM", "content": line.strip(), "agent": None}
207
+ )
208
+
209
+ # Search in agent memories
210
+ if "agent_memories" in memories:
211
+ for agent, content in memories["agent_memories"].items():
212
+ if isinstance(content, str):
213
+ for line in content.split("\n"):
214
+ if line.strip().startswith("-") and query_lower in line.lower():
215
+ results.append(
216
+ {
217
+ "type": "Agent",
218
+ "content": line.strip(),
219
+ "agent": agent,
220
+ }
221
+ )
222
+
223
+ return results
224
+
225
+ def clear_memories(self, agent_name: Optional[str] = None) -> None:
226
+ """
227
+ Clear memories for an agent or all agents.
228
+
229
+ Args:
230
+ agent_name: Specific agent or None for all
231
+ """
232
+ # Clear cache
233
+ self._cache_manager.clear_memory_caches()
234
+
235
+ # Clear files if requested
236
+ project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
237
+ if not project_memories_dir.exists():
238
+ return
239
+
240
+ if agent_name:
241
+ # Clear specific agent memory
242
+ memory_file = project_memories_dir / f"{agent_name}_memories.md"
243
+ if memory_file.exists():
244
+ memory_file.unlink()
245
+ self.logger.info(f"Cleared memories for agent: {agent_name}")
246
+ else:
247
+ # Clear all memories
248
+ for memory_file in project_memories_dir.glob("*_memories.md"):
249
+ memory_file.unlink()
250
+ self.logger.info(f"Cleared memory file: {memory_file.name}")
251
+
252
+ def get_memory_stats(self) -> Dict[str, Any]:
253
+ """
254
+ Get memory system statistics.
255
+
256
+ Returns:
257
+ Dictionary with memory statistics
258
+ """
259
+ memories = self.load_memories()
260
+
261
+ stats = dict(self._stats) # Copy internal stats
262
+
263
+ # Add memory content stats
264
+ stats["pm_memory_size"] = len(
265
+ memories.get("actual_memories", "").encode("utf-8")
266
+ )
267
+ stats["agent_count"] = len(memories.get("agent_memories", {}))
268
+
269
+ if "agent_memories" in memories:
270
+ total_agent_size = 0
271
+ for content in memories["agent_memories"].values():
272
+ if isinstance(content, str):
273
+ total_agent_size += len(content.encode("utf-8"))
274
+ stats["total_agent_memory_size"] = total_agent_size
275
+
276
+ return stats
277
+
278
+ # Internal methods (extracted from framework_loader.py)
279
+
280
+ def _load_actual_memories(self, deployed_agents: Set[str]) -> Dict[str, Any]:
281
+ """
282
+ Load actual memories from both user and project directories.
283
+
284
+ Args:
285
+ deployed_agents: Set of deployed agent names
286
+
287
+ Returns:
288
+ Dictionary with actual_memories and agent_memories
289
+ """
290
+ # Define memory directories in priority order
291
+ user_memories_dir = Path.home() / ".claude-mpm" / "memories"
292
+ project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
293
+
294
+ # Dictionary to store aggregated memories
295
+ pm_memories = []
296
+ agent_memories_dict = {}
297
+
298
+ # Load memories from user directory first
299
+ if user_memories_dir.exists():
300
+ self.logger.info(
301
+ f"Loading user-level memory files from: {user_memories_dir}"
302
+ )
303
+ self._load_memories_from_directory(
304
+ user_memories_dir,
305
+ deployed_agents,
306
+ pm_memories,
307
+ agent_memories_dict,
308
+ "user",
309
+ )
310
+ else:
311
+ self.logger.debug(
312
+ f"No user memories directory found at: {user_memories_dir}"
313
+ )
314
+
315
+ # Load memories from project directory (overrides user memories)
316
+ if project_memories_dir.exists():
317
+ self.logger.info(
318
+ f"Loading project-level memory files from: {project_memories_dir}"
319
+ )
320
+ self._load_memories_from_directory(
321
+ project_memories_dir,
322
+ deployed_agents,
323
+ pm_memories,
324
+ agent_memories_dict,
325
+ "project",
326
+ )
327
+ else:
328
+ self.logger.debug(
329
+ f"No project memories directory found at: {project_memories_dir}"
330
+ )
331
+
332
+ result = {}
333
+
334
+ # Aggregate PM memories
335
+ if pm_memories:
336
+ aggregated_pm = self._aggregate_memories(pm_memories)
337
+ result["actual_memories"] = aggregated_pm
338
+ memory_size = len(aggregated_pm.encode("utf-8"))
339
+ self.logger.info(
340
+ f"Aggregated PM memory ({memory_size:,} bytes) from {len(pm_memories)} source(s)"
341
+ )
342
+
343
+ # Store agent memories (already aggregated per agent)
344
+ if agent_memories_dict:
345
+ result["agent_memories"] = agent_memories_dict
346
+ for agent_name, memory_content in agent_memories_dict.items():
347
+ memory_size = len(memory_content.encode("utf-8"))
348
+ self.logger.debug(
349
+ f"Aggregated {agent_name} memory: {memory_size:,} bytes"
350
+ )
351
+
352
+ # Log summary
353
+ if self._stats["loaded_count"] > 0 or self._stats["skipped_count"] > 0:
354
+ agent_count = len(agent_memories_dict) if agent_memories_dict else 0
355
+ pm_loaded = bool(result.get("actual_memories"))
356
+
357
+ summary_parts = []
358
+ if pm_loaded:
359
+ summary_parts.append("PM memory loaded")
360
+ if agent_count > 0:
361
+ summary_parts.append(f"{agent_count} agent memories loaded")
362
+ if self._stats["skipped_count"] > 0:
363
+ summary_parts.append(
364
+ f"{self._stats['skipped_count']} non-deployed agent memories skipped"
365
+ )
366
+
367
+ self.logger.info(f"Memory loading complete: {' | '.join(summary_parts)}")
368
+
369
+ if len(deployed_agents) > 0:
370
+ self.logger.debug(
371
+ f"Deployed agents available for memory loading: {', '.join(sorted(deployed_agents))}"
372
+ )
373
+
374
+ return result
375
+
376
+ def _load_memories_from_directory(
377
+ self,
378
+ memories_dir: Path,
379
+ deployed_agents: Set[str],
380
+ pm_memories: List[Dict[str, Any]],
381
+ agent_memories_dict: Dict[str, Any],
382
+ source: str,
383
+ ) -> None:
384
+ """
385
+ Load memories from a specific directory.
386
+
387
+ Args:
388
+ memories_dir: Directory to load memories from
389
+ deployed_agents: Set of deployed agent names
390
+ pm_memories: List to append PM memories to
391
+ agent_memories_dict: Dict to store agent memories
392
+ source: Source label ("user" or "project")
393
+ """
394
+ # Load PM memories (always loaded)
395
+ pm_memory_path = memories_dir / "PM_memories.md"
396
+ old_pm_path = memories_dir / "PM.md"
397
+
398
+ # Migrate from old PM.md if needed
399
+ if not pm_memory_path.exists() and old_pm_path.exists():
400
+ self._migrate_legacy_file(old_pm_path, pm_memory_path)
401
+
402
+ if pm_memory_path.exists():
403
+ try:
404
+ loaded_content = pm_memory_path.read_text(encoding="utf-8")
405
+ if loaded_content:
406
+ pm_memories.append(
407
+ {
408
+ "source": source,
409
+ "content": loaded_content,
410
+ "path": pm_memory_path,
411
+ }
412
+ )
413
+ memory_size = len(loaded_content.encode("utf-8"))
414
+ self.logger.info(
415
+ f"Loaded {source} PM memory: {pm_memory_path} ({memory_size:,} bytes)"
416
+ )
417
+ self._stats["loaded_count"] += 1
418
+ except Exception as e:
419
+ self.logger.error(
420
+ f"Failed to load PM memory from {pm_memory_path}: {e}"
421
+ )
422
+
423
+ # Migrate old format memory files
424
+ for old_file in memories_dir.glob("*.md"):
425
+ # Skip files already in correct format and special files
426
+ if old_file.name.endswith("_memories.md") or old_file.name in [
427
+ "PM.md",
428
+ "README.md",
429
+ ]:
430
+ continue
431
+
432
+ # Determine new name based on old format
433
+ if old_file.stem.endswith("_agent"):
434
+ # Old format: {agent_name}_agent.md -> {agent_name}_memories.md
435
+ agent_name = old_file.stem[:-6] # Remove "_agent" suffix
436
+ new_path = memories_dir / f"{agent_name}_memories.md"
437
+ if not new_path.exists():
438
+ self._migrate_legacy_file(old_file, new_path)
439
+ else:
440
+ # Intermediate format: {agent_name}.md -> {agent_name}_memories.md
441
+ agent_name = old_file.stem
442
+ new_path = memories_dir / f"{agent_name}_memories.md"
443
+ if not new_path.exists():
444
+ self._migrate_legacy_file(old_file, new_path)
445
+
446
+ # Load agent memories (only for deployed agents)
447
+ for memory_file in memories_dir.glob("*_memories.md"):
448
+ # Skip PM_memories.md as we already handled it
449
+ if memory_file.name == "PM_memories.md":
450
+ continue
451
+
452
+ # Extract agent name from file (remove "_memories" suffix)
453
+ agent_name = memory_file.stem[:-9] # Remove "_memories" suffix
454
+
455
+ # Check if agent is deployed
456
+ if agent_name in deployed_agents:
457
+ try:
458
+ loaded_content = memory_file.read_text(encoding="utf-8")
459
+ if loaded_content:
460
+ # Store or merge agent memories
461
+ if agent_name not in agent_memories_dict:
462
+ agent_memories_dict[agent_name] = []
463
+
464
+ # If it's a list, append the new memory entry
465
+ if isinstance(agent_memories_dict[agent_name], list):
466
+ agent_memories_dict[agent_name].append(
467
+ {
468
+ "source": source,
469
+ "content": loaded_content,
470
+ "path": memory_file,
471
+ }
472
+ )
473
+
474
+ memory_size = len(loaded_content.encode("utf-8"))
475
+ self.logger.info(
476
+ f"Loaded {source} memory for {agent_name}: {memory_file.name} ({memory_size:,} bytes)"
477
+ )
478
+ self._stats["loaded_count"] += 1
479
+ except Exception as e:
480
+ self.logger.error(
481
+ f"Failed to load agent memory from {memory_file}: {e}"
482
+ )
483
+ else:
484
+ # Log skipped memories
485
+ self.logger.info(
486
+ f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed)"
487
+ )
488
+
489
+ # Detect naming mismatches
490
+ alt_name = (
491
+ agent_name.replace("_", "-")
492
+ if "_" in agent_name
493
+ else agent_name.replace("-", "_")
494
+ )
495
+ if alt_name in deployed_agents:
496
+ self.logger.warning(
497
+ f"Naming mismatch detected: Memory file uses '{agent_name}' but deployed agent is '{alt_name}'. "
498
+ f"Consider renaming {memory_file.name} to {alt_name}_memories.md"
499
+ )
500
+
501
+ self._stats["skipped_count"] += 1
502
+
503
+ # Aggregate agent memories for this directory
504
+ for agent_name in list(agent_memories_dict.keys()):
505
+ if (
506
+ isinstance(agent_memories_dict[agent_name], list)
507
+ and agent_memories_dict[agent_name]
508
+ ):
509
+ # Aggregate memories for this agent
510
+ aggregated = self._aggregate_memories(agent_memories_dict[agent_name])
511
+ agent_memories_dict[agent_name] = aggregated
512
+
513
+ def _aggregate_memories(self, memory_entries: List[Dict[str, Any]]) -> str:
514
+ """
515
+ Aggregate multiple memory entries into a single memory string.
516
+
517
+ Strategy:
518
+ - Preserve all unique bullet-point items (lines starting with -)
519
+ - Remove exact duplicates
520
+ - Project-level memories take precedence over user-level
521
+
522
+ Args:
523
+ memory_entries: List of memory entries with source, content, and path
524
+
525
+ Returns:
526
+ Aggregated memory content as a string
527
+ """
528
+ if not memory_entries:
529
+ return ""
530
+
531
+ # If only one entry, return it as-is
532
+ if len(memory_entries) == 1:
533
+ return memory_entries[0]["content"]
534
+
535
+ # Parse all memories into a simple list
536
+ all_items = {} # Dict to track items and their source
537
+ metadata_lines = []
538
+ agent_id = None
539
+
540
+ for entry in memory_entries:
541
+ content = entry["content"]
542
+ source = entry["source"]
543
+
544
+ for line in content.split("\n"):
545
+ # Check for header to extract agent_id
546
+ if line.startswith("# Agent Memory:"):
547
+ agent_id = line.replace("# Agent Memory:", "").strip()
548
+ # Check for metadata lines
549
+ elif line.startswith("<!-- ") and line.endswith(" -->"):
550
+ # Only keep metadata from project source or if not already present
551
+ if source == "project" or line not in metadata_lines:
552
+ metadata_lines.append(line)
553
+ # Check for list items
554
+ elif line.strip().startswith("-"):
555
+ # Normalize the item for comparison
556
+ item_text = line.strip()
557
+ normalized = item_text.lstrip("- ").strip().lower()
558
+
559
+ # Add item if new or if project source overrides user source
560
+ if normalized not in all_items or source == "project":
561
+ all_items[normalized] = (item_text, source)
562
+
563
+ # Build aggregated content
564
+ lines = []
565
+
566
+ # Add header
567
+ if agent_id:
568
+ lines.append(f"# Agent Memory: {agent_id}")
569
+ else:
570
+ lines.append("# Agent Memory")
571
+
572
+ # Add latest timestamp
573
+ lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
574
+ lines.append("")
575
+
576
+ # Add all unique items (sorted for consistency)
577
+ for normalized_key in sorted(all_items.keys()):
578
+ item_text, _ = all_items[normalized_key]
579
+ lines.append(item_text)
580
+
581
+ return "\n".join(lines)
582
+
583
+ def _migrate_legacy_file(self, old_path: Path, new_path: Path) -> None:
584
+ """
585
+ Migrate memory file from old naming convention to new.
586
+
587
+ WHY: Supports backward compatibility by automatically migrating from
588
+ the old {agent_id}_agent.md and {agent_id}.md formats to the new
589
+ {agent_id}_memories.md format.
590
+
591
+ Args:
592
+ old_path: Path to the old file
593
+ new_path: Path to the new file
594
+ """
595
+ if old_path.exists() and not new_path.exists():
596
+ try:
597
+ # Read content from old file
598
+ content = old_path.read_text(encoding="utf-8")
599
+ # Write to new file
600
+ new_path.write_text(content, encoding="utf-8")
601
+ # Remove old file
602
+ old_path.unlink()
603
+ self.logger.info(
604
+ f"Migrated memory file from {old_path.name} to {new_path.name}"
605
+ )
606
+ except Exception as e:
607
+ self.logger.error(f"Failed to migrate memory file {old_path.name}: {e}")
608
+
609
+ def _get_deployed_agents(self) -> Set[str]:
610
+ """
611
+ Get a set of deployed agent names from .claude/agents/ directories.
612
+
613
+ Returns:
614
+ Set of agent names (file stems) that are deployed
615
+ """
616
+ # Try to get from cache first
617
+ cached = self._cache_manager.get_deployed_agents()
618
+ if cached is not None:
619
+ return cached
620
+
621
+ # Cache miss - perform actual scan
622
+ self.logger.debug("Scanning for deployed agents (cache miss)")
623
+ deployed = set()
624
+
625
+ # Check project-level .claude/agents/
626
+ project_agents_dir = Path.cwd() / ".claude" / "agents"
627
+ if project_agents_dir.exists():
628
+ for agent_file in project_agents_dir.glob("*.md"):
629
+ agent_name = agent_file.stem
630
+ if agent_name.upper() != "README":
631
+ deployed.add(agent_name)
632
+ self.logger.debug(f"Found deployed agent: {agent_name}")
633
+
634
+ # Cache the result
635
+ self._cache_manager.set_deployed_agents(deployed)
636
+
637
+ return deployed