claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__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 (53) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/engineer.json +33 -11
  3. claude_mpm/cli/commands/agents.py +556 -1009
  4. claude_mpm/cli/commands/memory.py +248 -927
  5. claude_mpm/cli/commands/run.py +139 -484
  6. claude_mpm/cli/startup_logging.py +76 -0
  7. claude_mpm/core/agent_registry.py +6 -10
  8. claude_mpm/core/framework_loader.py +114 -595
  9. claude_mpm/core/logging_config.py +2 -4
  10. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  11. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  12. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  13. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  14. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  15. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  16. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  17. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  18. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  19. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  20. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  21. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  22. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  23. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  24. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  25. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  26. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  28. claude_mpm/services/agents/registry/__init__.py +1 -1
  29. claude_mpm/services/cli/__init__.py +18 -0
  30. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  31. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  32. claude_mpm/services/cli/agent_listing_service.py +463 -0
  33. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  34. claude_mpm/services/cli/agent_validation_service.py +589 -0
  35. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  36. claude_mpm/services/cli/memory_crud_service.py +617 -0
  37. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  38. claude_mpm/services/cli/session_manager.py +513 -0
  39. claude_mpm/services/cli/socketio_manager.py +498 -0
  40. claude_mpm/services/cli/startup_checker.py +370 -0
  41. claude_mpm/services/core/cache_manager.py +311 -0
  42. claude_mpm/services/core/memory_manager.py +637 -0
  43. claude_mpm/services/core/path_resolver.py +498 -0
  44. claude_mpm/services/core/service_container.py +520 -0
  45. claude_mpm/services/core/service_interfaces.py +436 -0
  46. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  47. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
  48. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
  49. claude_mpm/cli/commands/run_config_checker.py +0 -159
  50. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
  51. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
  52. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
  53. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,589 @@
1
+ """
2
+ Agent Validation Service
3
+ ========================
4
+
5
+ WHY: This service encapsulates all agent validation logic, including frontmatter
6
+ validation, file integrity checks, and deployment state validation. It provides
7
+ a clean interface for the CLI to validate and fix agent issues.
8
+
9
+ DESIGN DECISIONS:
10
+ - Uses FrontmatterValidator for low-level validation operations
11
+ - Provides high-level validation and fix operations for the CLI
12
+ - Generates detailed validation reports for user feedback
13
+ - Maintains separation between validation logic and CLI presentation
14
+ """
15
+
16
+ from abc import ABC, abstractmethod
17
+ from pathlib import Path
18
+ from typing import Any, Dict, Optional, Tuple
19
+
20
+ from ...agents.frontmatter_validator import FrontmatterValidator
21
+ from ...core.agent_registry import AgentRegistryAdapter
22
+ from ...core.logger import get_logger
23
+
24
+
25
+ class IAgentValidationService(ABC):
26
+ """Interface for agent validation operations."""
27
+
28
+ @abstractmethod
29
+ def validate_agent(self, agent_name: str) -> Dict[str, Any]:
30
+ """Validate a single agent."""
31
+
32
+ @abstractmethod
33
+ def validate_all_agents(self) -> Dict[str, Any]:
34
+ """Validate all deployed agents."""
35
+
36
+ @abstractmethod
37
+ def fix_agent_frontmatter(
38
+ self, agent_name: str, dry_run: bool = True
39
+ ) -> Dict[str, Any]:
40
+ """Fix frontmatter issues for a single agent."""
41
+
42
+ @abstractmethod
43
+ def fix_all_agents(self, dry_run: bool = True) -> Dict[str, Any]:
44
+ """Fix frontmatter issues for all agents."""
45
+
46
+ @abstractmethod
47
+ def check_agent_integrity(self, agent_name: str) -> Dict[str, Any]:
48
+ """Verify agent file structure and content integrity."""
49
+
50
+ @abstractmethod
51
+ def validate_deployment_state(self) -> Dict[str, Any]:
52
+ """Check deployment consistency across all agents."""
53
+
54
+
55
+ class AgentValidationService(IAgentValidationService):
56
+ """Service for validating and fixing agent deployment issues."""
57
+
58
+ def __init__(self):
59
+ """Initialize the validation service."""
60
+ self.logger = get_logger(__name__)
61
+ self.validator = FrontmatterValidator()
62
+ self._registry = None
63
+
64
+ @property
65
+ def registry(self):
66
+ """Get agent registry instance (lazy loaded)."""
67
+ if self._registry is None:
68
+ try:
69
+ adapter = AgentRegistryAdapter()
70
+ self._registry = adapter.registry
71
+ except Exception as e:
72
+ self.logger.error(f"Failed to initialize agent registry: {e}")
73
+ raise RuntimeError(f"Could not initialize agent registry: {e}")
74
+ return self._registry
75
+
76
+ def validate_agent(self, agent_name: str) -> Dict[str, Any]:
77
+ """
78
+ Validate a single agent.
79
+
80
+ Args:
81
+ agent_name: Name of the agent to validate
82
+
83
+ Returns:
84
+ Dictionary containing validation results
85
+ """
86
+ try:
87
+ # Get agent from registry
88
+ agent = self.registry.get_agent(agent_name)
89
+ if not agent:
90
+ return {
91
+ "success": False,
92
+ "agent": agent_name,
93
+ "error": f"Agent '{agent_name}' not found",
94
+ "available_agents": list(self.registry.list_agents().keys()),
95
+ }
96
+
97
+ # Validate agent file
98
+ agent_path = Path(agent.path)
99
+ if not agent_path.exists():
100
+ return {
101
+ "success": False,
102
+ "agent": agent_name,
103
+ "path": str(agent_path),
104
+ "error": "Agent file not found",
105
+ "is_valid": False,
106
+ }
107
+
108
+ # Perform validation
109
+ result = self.validator.validate_file(agent_path)
110
+
111
+ return {
112
+ "success": True,
113
+ "agent": agent_name,
114
+ "path": str(agent_path),
115
+ "is_valid": result.is_valid,
116
+ "errors": result.errors,
117
+ "warnings": result.warnings,
118
+ "corrections_available": len(result.corrections) > 0,
119
+ "corrections": result.corrections,
120
+ }
121
+
122
+ except Exception as e:
123
+ self.logger.error(
124
+ f"Error validating agent {agent_name}: {e}", exc_info=True
125
+ )
126
+ return {"success": False, "agent": agent_name, "error": str(e)}
127
+
128
+ def validate_all_agents(self) -> Dict[str, Any]:
129
+ """
130
+ Validate all deployed agents.
131
+
132
+ Returns:
133
+ Dictionary containing validation results for all agents
134
+ """
135
+ try:
136
+ all_agents = self.registry.list_agents()
137
+ if not all_agents:
138
+ return {
139
+ "success": True,
140
+ "message": "No agents found to validate",
141
+ "total_agents": 0,
142
+ "results": [],
143
+ }
144
+
145
+ results = []
146
+ total_issues = 0
147
+ total_errors = 0
148
+ total_warnings = 0
149
+ agents_with_issues = []
150
+
151
+ for agent_id, metadata in all_agents.items():
152
+ agent_path = Path(metadata["path"])
153
+
154
+ # Check if file exists
155
+ if not agent_path.exists():
156
+ results.append(
157
+ {
158
+ "agent": agent_id,
159
+ "path": str(agent_path),
160
+ "is_valid": False,
161
+ "error": "File not found",
162
+ }
163
+ )
164
+ total_errors += 1
165
+ agents_with_issues.append(agent_id)
166
+ continue
167
+
168
+ # Validate the agent
169
+ validation_result = self.validator.validate_file(agent_path)
170
+
171
+ agent_result = {
172
+ "agent": agent_id,
173
+ "path": str(agent_path),
174
+ "is_valid": validation_result.is_valid,
175
+ "errors": validation_result.errors,
176
+ "warnings": validation_result.warnings,
177
+ "corrections_available": len(validation_result.corrections) > 0,
178
+ }
179
+
180
+ results.append(agent_result)
181
+
182
+ if validation_result.errors:
183
+ total_errors += len(validation_result.errors)
184
+ total_issues += len(validation_result.errors)
185
+ agents_with_issues.append(agent_id)
186
+
187
+ if validation_result.warnings:
188
+ total_warnings += len(validation_result.warnings)
189
+ total_issues += len(validation_result.warnings)
190
+ if agent_id not in agents_with_issues:
191
+ agents_with_issues.append(agent_id)
192
+
193
+ return {
194
+ "success": True,
195
+ "total_agents": len(all_agents),
196
+ "agents_checked": len(results),
197
+ "total_issues": total_issues,
198
+ "total_errors": total_errors,
199
+ "total_warnings": total_warnings,
200
+ "agents_with_issues": agents_with_issues,
201
+ "results": results,
202
+ }
203
+
204
+ except Exception as e:
205
+ self.logger.error(f"Error validating all agents: {e}", exc_info=True)
206
+ return {"success": False, "error": str(e)}
207
+
208
+ def fix_agent_frontmatter(
209
+ self, agent_name: str, dry_run: bool = True
210
+ ) -> Dict[str, Any]:
211
+ """
212
+ Fix frontmatter issues for a single agent.
213
+
214
+ Args:
215
+ agent_name: Name of the agent to fix
216
+ dry_run: If True, don't actually write changes
217
+
218
+ Returns:
219
+ Dictionary containing fix results
220
+ """
221
+ try:
222
+ # Get agent from registry
223
+ agent = self.registry.get_agent(agent_name)
224
+ if not agent:
225
+ return {
226
+ "success": False,
227
+ "agent": agent_name,
228
+ "error": f"Agent '{agent_name}' not found",
229
+ }
230
+
231
+ agent_path = Path(agent.path)
232
+ if not agent_path.exists():
233
+ return {
234
+ "success": False,
235
+ "agent": agent_name,
236
+ "path": str(agent_path),
237
+ "error": "Agent file not found",
238
+ }
239
+
240
+ # Fix the agent
241
+ result = self.validator.correct_file(agent_path, dry_run=dry_run)
242
+
243
+ return {
244
+ "success": True,
245
+ "agent": agent_name,
246
+ "path": str(agent_path),
247
+ "dry_run": dry_run,
248
+ "was_valid": result.is_valid and not result.corrections,
249
+ "errors_found": result.errors,
250
+ "warnings_found": result.warnings,
251
+ "corrections_made": result.corrections if not dry_run else [],
252
+ "corrections_available": result.corrections if dry_run else [],
253
+ "is_fixed": not dry_run and result.is_valid,
254
+ }
255
+
256
+ except Exception as e:
257
+ self.logger.error(f"Error fixing agent {agent_name}: {e}", exc_info=True)
258
+ return {"success": False, "agent": agent_name, "error": str(e)}
259
+
260
+ def fix_all_agents(self, dry_run: bool = True) -> Dict[str, Any]:
261
+ """
262
+ Fix frontmatter issues for all agents.
263
+
264
+ Args:
265
+ dry_run: If True, don't actually write changes
266
+
267
+ Returns:
268
+ Dictionary containing fix results for all agents
269
+ """
270
+ try:
271
+ all_agents = self.registry.list_agents()
272
+ if not all_agents:
273
+ return {
274
+ "success": True,
275
+ "message": "No agents found to fix",
276
+ "total_agents": 0,
277
+ "results": [],
278
+ }
279
+
280
+ results = []
281
+ total_issues = 0
282
+ total_fixed = 0
283
+ agents_fixed = []
284
+ agents_with_errors = []
285
+
286
+ for agent_id, metadata in all_agents.items():
287
+ agent_path = Path(metadata["path"])
288
+
289
+ # Check if file exists
290
+ if not agent_path.exists():
291
+ results.append(
292
+ {
293
+ "agent": agent_id,
294
+ "path": str(agent_path),
295
+ "skipped": True,
296
+ "reason": "File not found",
297
+ }
298
+ )
299
+ agents_with_errors.append(agent_id)
300
+ continue
301
+
302
+ # Fix the agent
303
+ fix_result = self.validator.correct_file(agent_path, dry_run=dry_run)
304
+
305
+ agent_result = {
306
+ "agent": agent_id,
307
+ "path": str(agent_path),
308
+ "was_valid": fix_result.is_valid and not fix_result.corrections,
309
+ "errors_found": len(fix_result.errors),
310
+ "warnings_found": len(fix_result.warnings),
311
+ "corrections_made": (
312
+ len(fix_result.corrections) if not dry_run else 0
313
+ ),
314
+ "corrections_available": (
315
+ len(fix_result.corrections) if dry_run else 0
316
+ ),
317
+ }
318
+
319
+ results.append(agent_result)
320
+
321
+ if fix_result.errors:
322
+ total_issues += len(fix_result.errors)
323
+
324
+ if fix_result.warnings:
325
+ total_issues += len(fix_result.warnings)
326
+
327
+ if fix_result.corrections:
328
+ if not dry_run:
329
+ total_fixed += len(fix_result.corrections)
330
+ agents_fixed.append(agent_id)
331
+
332
+ return {
333
+ "success": True,
334
+ "dry_run": dry_run,
335
+ "total_agents": len(all_agents),
336
+ "agents_checked": len(results),
337
+ "total_issues_found": total_issues,
338
+ "total_corrections_made": total_fixed if not dry_run else 0,
339
+ "total_corrections_available": total_fixed if dry_run else 0,
340
+ "agents_fixed": agents_fixed,
341
+ "agents_with_errors": agents_with_errors,
342
+ "results": results,
343
+ }
344
+
345
+ except Exception as e:
346
+ self.logger.error(f"Error fixing all agents: {e}", exc_info=True)
347
+ return {"success": False, "error": str(e)}
348
+
349
+ def check_agent_integrity(self, agent_name: str) -> Dict[str, Any]:
350
+ """
351
+ Verify agent file structure and content integrity.
352
+
353
+ Args:
354
+ agent_name: Name of the agent to check
355
+
356
+ Returns:
357
+ Dictionary containing integrity check results
358
+ """
359
+ try:
360
+ # Get agent from registry
361
+ agent = self.registry.get_agent(agent_name)
362
+ if not agent:
363
+ return {
364
+ "success": False,
365
+ "agent": agent_name,
366
+ "error": f"Agent '{agent_name}' not found",
367
+ }
368
+
369
+ agent_path = Path(agent.path)
370
+ checks = {
371
+ "file_exists": False,
372
+ "has_frontmatter": False,
373
+ "has_content": False,
374
+ "valid_frontmatter": False,
375
+ "required_fields": [],
376
+ "missing_fields": [],
377
+ "file_size": 0,
378
+ "line_count": 0,
379
+ }
380
+
381
+ # Check file existence
382
+ if not agent_path.exists():
383
+ return {
384
+ "success": True,
385
+ "agent": agent_name,
386
+ "path": str(agent_path),
387
+ "integrity": checks,
388
+ "is_valid": False,
389
+ "issues": ["File does not exist"],
390
+ }
391
+
392
+ checks["file_exists"] = True
393
+
394
+ # Read file content
395
+ try:
396
+ content = agent_path.read_text()
397
+ checks["file_size"] = len(content)
398
+ checks["line_count"] = len(content.splitlines())
399
+ checks["has_content"] = len(content.strip()) > 0
400
+ except Exception as e:
401
+ return {
402
+ "success": True,
403
+ "agent": agent_name,
404
+ "path": str(agent_path),
405
+ "integrity": checks,
406
+ "is_valid": False,
407
+ "issues": [f"Cannot read file: {e}"],
408
+ }
409
+
410
+ # Check for frontmatter
411
+ if content.startswith("---"):
412
+ checks["has_frontmatter"] = True
413
+
414
+ # Validate frontmatter
415
+ validation_result = self.validator.validate_file(agent_path)
416
+ checks["valid_frontmatter"] = validation_result.is_valid
417
+
418
+ # Extract frontmatter to check fields
419
+ try:
420
+ frontmatter_match = self.validator._extract_frontmatter(content)
421
+ if frontmatter_match:
422
+ import yaml
423
+
424
+ frontmatter = yaml.safe_load(frontmatter_match[0])
425
+
426
+ # Check required fields
427
+ required = ["name", "type", "description"]
428
+ for field in required:
429
+ if field in frontmatter:
430
+ checks["required_fields"].append(field)
431
+ else:
432
+ checks["missing_fields"].append(field)
433
+ except Exception:
434
+ pass
435
+
436
+ # Determine overall validity
437
+ issues = []
438
+ if not checks["file_exists"]:
439
+ issues.append("File does not exist")
440
+ if not checks["has_content"]:
441
+ issues.append("File is empty")
442
+ if not checks["has_frontmatter"]:
443
+ issues.append("No frontmatter found")
444
+ if checks["has_frontmatter"] and not checks["valid_frontmatter"]:
445
+ issues.append("Invalid frontmatter format")
446
+ if checks["missing_fields"]:
447
+ issues.append(
448
+ f"Missing required fields: {', '.join(checks['missing_fields'])}"
449
+ )
450
+
451
+ is_valid = len(issues) == 0
452
+
453
+ return {
454
+ "success": True,
455
+ "agent": agent_name,
456
+ "path": str(agent_path),
457
+ "integrity": checks,
458
+ "is_valid": is_valid,
459
+ "issues": issues,
460
+ }
461
+
462
+ except Exception as e:
463
+ self.logger.error(
464
+ f"Error checking integrity for agent {agent_name}: {e}", exc_info=True
465
+ )
466
+ return {"success": False, "agent": agent_name, "error": str(e)}
467
+
468
+ def validate_deployment_state(self) -> Dict[str, Any]:
469
+ """
470
+ Check deployment consistency across all agents.
471
+
472
+ Returns:
473
+ Dictionary containing deployment state validation results
474
+ """
475
+ try:
476
+ deployment_state = {
477
+ "total_registered": 0,
478
+ "files_found": 0,
479
+ "files_missing": [],
480
+ "duplicate_names": {},
481
+ "conflicting_paths": {},
482
+ "deployment_issues": [],
483
+ }
484
+
485
+ all_agents = self.registry.list_agents()
486
+ deployment_state["total_registered"] = len(all_agents)
487
+
488
+ # Track agent names and paths for duplicate detection
489
+ name_to_agents = {}
490
+ path_to_agents = {}
491
+
492
+ for agent_id, metadata in all_agents.items():
493
+ agent_path = Path(metadata["path"])
494
+ agent_name = metadata.get("name", agent_id)
495
+
496
+ # Check file existence
497
+ if agent_path.exists():
498
+ deployment_state["files_found"] += 1
499
+ else:
500
+ deployment_state["files_missing"].append(
501
+ {"agent": agent_id, "path": str(agent_path)}
502
+ )
503
+
504
+ # Track names for duplicates
505
+ if agent_name in name_to_agents:
506
+ if agent_name not in deployment_state["duplicate_names"]:
507
+ deployment_state["duplicate_names"][agent_name] = []
508
+ deployment_state["duplicate_names"][agent_name].append(agent_id)
509
+ if (
510
+ name_to_agents[agent_name]
511
+ not in deployment_state["duplicate_names"][agent_name]
512
+ ):
513
+ deployment_state["duplicate_names"][agent_name].append(
514
+ name_to_agents[agent_name]
515
+ )
516
+ else:
517
+ name_to_agents[agent_name] = agent_id
518
+
519
+ # Track paths for conflicts
520
+ path_str = str(agent_path)
521
+ if path_str in path_to_agents:
522
+ if path_str not in deployment_state["conflicting_paths"]:
523
+ deployment_state["conflicting_paths"][path_str] = []
524
+ deployment_state["conflicting_paths"][path_str].append(agent_id)
525
+ if (
526
+ path_to_agents[path_str]
527
+ not in deployment_state["conflicting_paths"][path_str]
528
+ ):
529
+ deployment_state["conflicting_paths"][path_str].append(
530
+ path_to_agents[path_str]
531
+ )
532
+ else:
533
+ path_to_agents[path_str] = agent_id
534
+
535
+ # Identify deployment issues
536
+ if deployment_state["files_missing"]:
537
+ deployment_state["deployment_issues"].append(
538
+ f"{len(deployment_state['files_missing'])} agents have missing files"
539
+ )
540
+
541
+ if deployment_state["duplicate_names"]:
542
+ deployment_state["deployment_issues"].append(
543
+ f"{len(deployment_state['duplicate_names'])} duplicate agent names found"
544
+ )
545
+
546
+ if deployment_state["conflicting_paths"]:
547
+ deployment_state["deployment_issues"].append(
548
+ f"{len(deployment_state['conflicting_paths'])} path conflicts found"
549
+ )
550
+
551
+ # Determine if deployment is healthy
552
+ is_healthy = (
553
+ len(deployment_state["files_missing"]) == 0
554
+ and len(deployment_state["duplicate_names"]) == 0
555
+ and len(deployment_state["conflicting_paths"]) == 0
556
+ )
557
+
558
+ return {
559
+ "success": True,
560
+ "is_healthy": is_healthy,
561
+ "deployment_state": deployment_state,
562
+ "summary": {
563
+ "total_agents": deployment_state["total_registered"],
564
+ "healthy_agents": deployment_state["files_found"]
565
+ - len(deployment_state["duplicate_names"])
566
+ - len(deployment_state["conflicting_paths"]),
567
+ "issues_count": len(deployment_state["deployment_issues"]),
568
+ },
569
+ }
570
+
571
+ except Exception as e:
572
+ self.logger.error(f"Error validating deployment state: {e}", exc_info=True)
573
+ return {"success": False, "error": str(e)}
574
+
575
+ def _extract_frontmatter(self, content: str) -> Optional[Tuple[str, str]]:
576
+ """
577
+ Extract frontmatter and content from an agent file.
578
+
579
+ Args:
580
+ content: File content
581
+
582
+ Returns:
583
+ Tuple of (frontmatter, content) or None if no frontmatter
584
+ """
585
+ pattern = r"^---\n(.*?)\n---\n(.*)$"
586
+ match = re.match(pattern, content, re.DOTALL)
587
+ if match:
588
+ return match.groups()
589
+ return None