claude-mpm 5.4.22__py3-none-any.whl → 5.4.48__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (119) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT.md +164 -0
  3. claude_mpm/agents/BASE_ENGINEER.md +658 -0
  4. claude_mpm/agents/MEMORY.md +1 -1
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +739 -1052
  6. claude_mpm/agents/WORKFLOW.md +5 -254
  7. claude_mpm/agents/agent_loader.py +1 -1
  8. claude_mpm/agents/base_agent.json +31 -0
  9. claude_mpm/agents/frontmatter_validator.py +2 -2
  10. claude_mpm/cli/commands/agent_state_manager.py +10 -10
  11. claude_mpm/cli/commands/agents.py +9 -9
  12. claude_mpm/cli/commands/auto_configure.py +4 -4
  13. claude_mpm/cli/commands/configure.py +1 -1
  14. claude_mpm/cli/commands/configure_agent_display.py +10 -0
  15. claude_mpm/cli/commands/mpm_init/core.py +65 -0
  16. claude_mpm/cli/commands/postmortem.py +1 -1
  17. claude_mpm/cli/commands/profile.py +277 -0
  18. claude_mpm/cli/commands/skills.py +14 -18
  19. claude_mpm/cli/executor.py +10 -0
  20. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  21. claude_mpm/cli/parsers/base_parser.py +7 -0
  22. claude_mpm/cli/parsers/profile_parser.py +148 -0
  23. claude_mpm/cli/parsers/skills_parser.py +0 -6
  24. claude_mpm/cli/startup.py +346 -75
  25. claude_mpm/commands/mpm-config.md +13 -250
  26. claude_mpm/commands/mpm-doctor.md +9 -22
  27. claude_mpm/commands/mpm-help.md +5 -206
  28. claude_mpm/commands/mpm-init.md +81 -507
  29. claude_mpm/commands/mpm-monitor.md +15 -402
  30. claude_mpm/commands/mpm-organize.md +61 -441
  31. claude_mpm/commands/mpm-postmortem.md +6 -108
  32. claude_mpm/commands/mpm-session-resume.md +12 -363
  33. claude_mpm/commands/mpm-status.md +5 -69
  34. claude_mpm/commands/mpm-ticket-view.md +52 -495
  35. claude_mpm/commands/mpm-version.md +5 -107
  36. claude_mpm/core/config.py +2 -4
  37. claude_mpm/core/framework/loaders/agent_loader.py +1 -1
  38. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  39. claude_mpm/core/optimized_startup.py +59 -0
  40. claude_mpm/core/shared/config_loader.py +1 -1
  41. claude_mpm/core/unified_agent_registry.py +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  58. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  59. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
  76. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  77. claude_mpm/init.py +63 -0
  78. claude_mpm/models/git_repository.py +3 -3
  79. claude_mpm/scripts/start_activity_logging.py +0 -0
  80. claude_mpm/services/agents/agent_builder.py +3 -3
  81. claude_mpm/services/agents/cache_git_manager.py +6 -6
  82. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  83. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -2
  84. claude_mpm/services/agents/deployment/agent_format_converter.py +23 -13
  85. claude_mpm/services/agents/deployment/agent_template_builder.py +29 -19
  86. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  87. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  88. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  89. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
  90. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
  91. claude_mpm/services/agents/git_source_manager.py +19 -4
  92. claude_mpm/services/agents/recommender.py +5 -3
  93. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  94. claude_mpm/services/agents/sources/git_source_sync_service.py +112 -6
  95. claude_mpm/services/agents/startup_sync.py +22 -2
  96. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  97. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  98. claude_mpm/services/git/git_operations_service.py +8 -8
  99. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  100. claude_mpm/services/monitor/server.py +473 -3
  101. claude_mpm/services/pm_skills_deployer.py +711 -0
  102. claude_mpm/services/profile_manager.py +331 -0
  103. claude_mpm/services/skills/git_skill_source_manager.py +101 -3
  104. claude_mpm/services/skills_deployer.py +4 -3
  105. claude_mpm/services/socketio/dashboard_server.py +1 -0
  106. claude_mpm/services/socketio/event_normalizer.py +37 -6
  107. claude_mpm/services/socketio/server/core.py +262 -123
  108. claude_mpm/skills/skill_manager.py +92 -3
  109. claude_mpm/utils/agent_dependency_loader.py +14 -2
  110. claude_mpm/utils/agent_filters.py +1 -1
  111. claude_mpm/utils/migration.py +4 -4
  112. claude_mpm/utils/robust_installer.py +47 -3
  113. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/METADATA +7 -4
  114. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/RECORD +118 -79
  115. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/WHEEL +0 -0
  116. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/entry_points.txt +0 -0
  117. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/licenses/LICENSE +0 -0
  118. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  119. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+
8
+ <link rel="modulepreload" href="/_app/immutable/entry/start.DzuEhzqh.js">
9
+ <link rel="modulepreload" href="/_app/immutable/chunks/CIXEwuWe.js">
10
+ <link rel="modulepreload" href="/_app/immutable/chunks/DjhvlsAc.js">
11
+ <link rel="modulepreload" href="/_app/immutable/entry/app.DTL5mJO-.js">
12
+ <link rel="modulepreload" href="/_app/immutable/chunks/DMkZpdF2.js">
13
+ <link rel="modulepreload" href="/_app/immutable/chunks/CWc5urbQ.js">
14
+ <link rel="modulepreload" href="/_app/immutable/chunks/BgChzWQ1.js">
15
+ </head>
16
+ <body data-sveltekit-preload-data="hover">
17
+ <div style="display: contents">
18
+ <script>
19
+ {
20
+ __sveltekit_16ujgvz = {
21
+ base: ""
22
+ };
23
+
24
+ const element = document.currentScript.parentElement;
25
+
26
+ Promise.all([
27
+ import("/_app/immutable/entry/start.DzuEhzqh.js"),
28
+ import("/_app/immutable/entry/app.DTL5mJO-.js")
29
+ ]).then(([kit, app]) => {
30
+ kit.start(app, element);
31
+ });
32
+ }
33
+ </script>
34
+ </div>
35
+ </body>
36
+ </html>
@@ -394,6 +394,8 @@ class ClaudeHookHandler:
394
394
  Returns:
395
395
  Modified input for PreToolUse events (v2.0.30+), None otherwise
396
396
  """
397
+ import time
398
+
397
399
  # Try multiple field names for compatibility
398
400
  hook_type = (
399
401
  event.get("hook_event_name")
@@ -425,15 +427,40 @@ class ClaudeHookHandler:
425
427
  # Call appropriate handler if exists
426
428
  handler = event_handlers.get(hook_type)
427
429
  if handler:
430
+ # Track execution timing for hook emission
431
+ start_time = time.time()
432
+ success = False
433
+ error_message = None
434
+ result = None
435
+
428
436
  try:
429
437
  # Handlers can optionally return modified input
430
438
  result = handler(event)
439
+ success = True
431
440
  # Only PreToolUse handlers should return modified input
432
441
  if hook_type == "PreToolUse" and result is not None:
433
- return result
442
+ return_value = result
443
+ else:
444
+ return_value = None
434
445
  except Exception as e:
446
+ error_message = str(e)
447
+ return_value = None
435
448
  if DEBUG:
436
449
  print(f"Error handling {hook_type}: {e}", file=sys.stderr)
450
+ finally:
451
+ # Calculate duration
452
+ duration_ms = int((time.time() - start_time) * 1000)
453
+
454
+ # Emit hook execution event
455
+ self._emit_hook_execution_event(
456
+ hook_type=hook_type,
457
+ event=event,
458
+ success=success,
459
+ duration_ms=duration_ms,
460
+ error_message=error_message,
461
+ )
462
+
463
+ return return_value
437
464
 
438
465
  return None
439
466
 
@@ -478,6 +505,127 @@ class ClaudeHookHandler:
478
505
  """Generate event key through duplicate detector (backward compatibility)."""
479
506
  return self.duplicate_detector.generate_event_key(event)
480
507
 
508
+ def _emit_hook_execution_event(
509
+ self,
510
+ hook_type: str,
511
+ event: dict,
512
+ success: bool,
513
+ duration_ms: int,
514
+ error_message: Optional[str] = None,
515
+ ):
516
+ """Emit a structured JSON event for hook execution.
517
+
518
+ This emits a normalized event following the claude_event schema to provide
519
+ visibility into hook processing, timing, and success/failure status.
520
+
521
+ Args:
522
+ hook_type: The type of hook that executed (e.g., "UserPromptSubmit", "PreToolUse")
523
+ event: The original hook event data
524
+ success: Whether the hook executed successfully
525
+ duration_ms: How long the hook took to execute in milliseconds
526
+ error_message: Optional error message if the hook failed
527
+ """
528
+ # Generate a human-readable summary based on hook type
529
+ summary = self._generate_hook_summary(hook_type, event, success)
530
+
531
+ # Extract common fields
532
+ session_id = event.get("session_id", "")
533
+ working_dir = event.get("cwd", "")
534
+
535
+ # Build hook execution data
536
+ hook_data = {
537
+ "hook_name": hook_type,
538
+ "hook_type": hook_type,
539
+ "session_id": session_id,
540
+ "working_directory": working_dir,
541
+ "success": success,
542
+ "duration_ms": duration_ms,
543
+ "result_summary": summary,
544
+ "timestamp": datetime.now(timezone.utc).isoformat(),
545
+ }
546
+
547
+ # Add error information if present
548
+ if error_message:
549
+ hook_data["error_message"] = error_message
550
+
551
+ # Add hook-specific context
552
+ if hook_type == "PreToolUse":
553
+ hook_data["tool_name"] = event.get("tool_name", "")
554
+ elif hook_type == "PostToolUse":
555
+ hook_data["tool_name"] = event.get("tool_name", "")
556
+ hook_data["exit_code"] = event.get("exit_code", 0)
557
+ elif hook_type == "UserPromptSubmit":
558
+ prompt = event.get("prompt", "")
559
+ hook_data["prompt_preview"] = prompt[:100] if len(prompt) > 100 else prompt
560
+ hook_data["prompt_length"] = len(prompt)
561
+ elif hook_type == "SubagentStop":
562
+ hook_data["agent_type"] = event.get("agent_type", "unknown")
563
+ hook_data["reason"] = event.get("reason", "unknown")
564
+
565
+ # Emit through connection manager with proper structure
566
+ # This uses the existing event infrastructure
567
+ self._emit_socketio_event("", "hook_execution", hook_data)
568
+
569
+ if DEBUG:
570
+ print(
571
+ f"📊 Hook execution event: {hook_type} - {duration_ms}ms - {'✅' if success else '❌'}",
572
+ file=sys.stderr,
573
+ )
574
+
575
+ def _generate_hook_summary(self, hook_type: str, event: dict, success: bool) -> str:
576
+ """Generate a human-readable summary of what the hook did.
577
+
578
+ Args:
579
+ hook_type: The type of hook
580
+ event: The hook event data
581
+ success: Whether the hook executed successfully
582
+
583
+ Returns:
584
+ A brief description of what happened
585
+ """
586
+ if not success:
587
+ return f"Hook {hook_type} failed during processing"
588
+
589
+ # Generate hook-specific summaries
590
+ if hook_type == "UserPromptSubmit":
591
+ prompt = event.get("prompt", "")
592
+ if prompt.startswith("/"):
593
+ return f"Processed command: {prompt.split()[0]}"
594
+ return f"Processed user prompt ({len(prompt)} chars)"
595
+
596
+ if hook_type == "PreToolUse":
597
+ tool_name = event.get("tool_name", "unknown")
598
+ return f"Pre-processing tool call: {tool_name}"
599
+
600
+ if hook_type == "PostToolUse":
601
+ tool_name = event.get("tool_name", "unknown")
602
+ exit_code = event.get("exit_code", 0)
603
+ status = "success" if exit_code == 0 else "failed"
604
+ return f"Completed tool call: {tool_name} ({status})"
605
+
606
+ if hook_type == "SubagentStop":
607
+ agent_type = event.get("agent_type", "unknown")
608
+ reason = event.get("reason", "unknown")
609
+ return f"Subagent {agent_type} stopped: {reason}"
610
+
611
+ if hook_type == "SessionStart":
612
+ return "New session started"
613
+
614
+ if hook_type == "Stop":
615
+ reason = event.get("reason", "unknown")
616
+ return f"Session stopped: {reason}"
617
+
618
+ if hook_type == "Notification":
619
+ notification_type = event.get("notification_type", "unknown")
620
+ return f"Notification received: {notification_type}"
621
+
622
+ if hook_type == "AssistantResponse":
623
+ response_len = len(event.get("response", ""))
624
+ return f"Assistant response generated ({response_len} chars)"
625
+
626
+ # Default summary
627
+ return f"Hook {hook_type} processed successfully"
628
+
481
629
  def __del__(self):
482
630
  """Cleanup on handler destruction."""
483
631
  # Clean up connection manager if it exists
@@ -58,7 +58,7 @@ except ImportError:
58
58
  (),
59
59
  {
60
60
  "to_dict": lambda: {
61
- "event": "claude_event",
61
+ "event": "mpm_event",
62
62
  "type": event_data.get("type", "unknown"),
63
63
  "subtype": event_data.get("subtype", "generic"),
64
64
  "timestamp": event_data.get(
@@ -119,13 +119,33 @@ class ConnectionManagerService:
119
119
  tool_call_id = data.get("tool_call_id")
120
120
 
121
121
  # Create event data for normalization
122
+ # Extract session_id (try both camelCase and snake_case)
123
+ session_id = data.get("session_id") or data.get("sessionId")
124
+
125
+ # Extract working directory for project identification
126
+ # Try multiple field names for maximum compatibility
127
+ cwd = (
128
+ data.get("cwd")
129
+ or data.get("working_directory")
130
+ or data.get("workingDirectory")
131
+ )
132
+
133
+ # For hook_execution events, extract the actual hook type from data
134
+ # Otherwise use "hook" as the type
135
+ if event == "hook_execution":
136
+ hook_type = data.get("hook_type", "unknown")
137
+ event_type = hook_type
138
+ else:
139
+ event_type = "hook"
140
+
122
141
  raw_event = {
123
- "type": "hook",
124
- "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop"
142
+ "type": event_type, # Use actual hook type for hook_execution, "hook" otherwise
143
+ "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop", "execution"
125
144
  "timestamp": datetime.now(timezone.utc).isoformat(),
126
145
  "data": data,
127
- "source": "claude_hooks", # Identify the source
128
- "session_id": data.get("sessionId"), # Include session if available
146
+ "source": "mpm_hook", # Identify the source as mpm_hook
147
+ "session_id": session_id, # Include session if available (supports both naming conventions)
148
+ "cwd": cwd, # Add working directory at top level for easy frontend access
129
149
  "correlation_id": tool_call_id, # Set from tool_call_id for event correlation
130
150
  }
131
151
 
@@ -154,7 +174,7 @@ class ConnectionManagerService:
154
174
  if self.connection_pool:
155
175
  try:
156
176
  # Emit to Socket.IO server directly
157
- self.connection_pool.emit("claude_event", claude_event_data)
177
+ self.connection_pool.emit("mpm_event", claude_event_data)
158
178
  if DEBUG:
159
179
  print(f"✅ Emitted via connection pool: {event}", file=sys.stderr)
160
180
  return # Success - no need for fallback
@@ -13,9 +13,9 @@ for structured memory storage with semantic search capabilities.
13
13
  DESIGN DECISIONS:
14
14
  - Priority 10 for early execution to enrich prompts before other hooks
15
15
  - Uses subprocess to call kuzu-memory directly for maximum compatibility
16
- - Graceful degradation if kuzu-memory is not in PATH (though it's now required)
16
+ - Graceful degradation if kuzu-memory is not installed
17
17
  - Automatic extraction and storage of important information
18
- - kuzu-memory>=1.1.5 is now a REQUIRED dependency (moved from optional in v4.8.6)
18
+ - kuzu-memory is an OPTIONAL dependency (install with: pip install claude-mpm[memory])
19
19
  """
20
20
 
21
21
  import json
@@ -51,9 +51,9 @@ class KuzuMemoryHook(SubmitHook):
51
51
  self.enabled = self.kuzu_memory_cmd is not None
52
52
 
53
53
  if not self.enabled:
54
- logger.warning(
55
- "Kuzu-memory not found in PATH. As of v4.8.6, it's a required dependency. "
56
- "Install with: pip install kuzu-memory>=1.1.5 or pipx install kuzu-memory"
54
+ logger.debug(
55
+ "Kuzu-memory not found. Graph-based memory disabled. "
56
+ "To enable: pip install claude-mpm[memory] (requires cmake)"
57
57
  )
58
58
  else:
59
59
  logger.info(f"Kuzu-memory integration enabled: {self.kuzu_memory_cmd}")
claude_mpm/init.py CHANGED
@@ -163,6 +163,9 @@ class ProjectInitializer:
163
163
  f"✓ Found {agent_count} project agent(s) in .claude-mpm/agents/"
164
164
  )
165
165
 
166
+ # Verify and deploy PM skills (non-blocking)
167
+ self._verify_and_deploy_pm_skills(project_root, is_mcp_mode)
168
+
166
169
  return True
167
170
 
168
171
  except Exception as e:
@@ -170,6 +173,66 @@ class ProjectInitializer:
170
173
  print(f"✗ Failed to create .claude-mpm/ directory: {e}")
171
174
  return False
172
175
 
176
+ def _verify_and_deploy_pm_skills(
177
+ self, project_root: Path, is_mcp_mode: bool = False
178
+ ) -> None:
179
+ """Verify PM skills are deployed and auto-deploy if missing.
180
+
181
+ Non-blocking operation that gracefully handles errors.
182
+
183
+ Args:
184
+ project_root: Project root directory
185
+ is_mcp_mode: Whether running in MCP mode (suppress console output)
186
+ """
187
+ try:
188
+ from claude_mpm.services.pm_skills_deployer import PMSkillsDeployerService
189
+
190
+ deployer = PMSkillsDeployerService()
191
+ result = deployer.verify_pm_skills(project_root)
192
+
193
+ if not result.verified:
194
+ # Log warnings
195
+ for warning in result.warnings:
196
+ self.logger.warning(warning)
197
+
198
+ # Auto-deploy PM skills
199
+ self.logger.info("Auto-deploying PM skills...")
200
+ deploy_result = deployer.deploy_pm_skills(project_root)
201
+
202
+ if deploy_result.success:
203
+ self.logger.info(
204
+ f"PM skills deployed: {len(deploy_result.deployed)} deployed, "
205
+ f"{len(deploy_result.skipped)} skipped"
206
+ )
207
+
208
+ # Print to console if not in MCP mode
209
+ if not is_mcp_mode:
210
+ if deploy_result.deployed:
211
+ print(
212
+ f"✓ Deployed {len(deploy_result.deployed)} PM skill(s) "
213
+ f"to .claude-mpm/skills/pm/"
214
+ )
215
+ else:
216
+ self.logger.warning(
217
+ f"PM skills deployment had errors: {len(deploy_result.errors)}"
218
+ )
219
+ if not is_mcp_mode and deploy_result.errors:
220
+ print(f"⚠ PM skills deployment had {len(deploy_result.errors)} error(s)")
221
+ else:
222
+ # Skills verified successfully
223
+ registry = deployer._load_registry(project_root)
224
+ skill_count = len(registry.get("skills", []))
225
+ self.logger.debug(f"PM skills verified: {skill_count} skills")
226
+
227
+ if not is_mcp_mode and skill_count > 0:
228
+ print(f"✓ Verified {skill_count} PM skill(s)")
229
+
230
+ except ImportError:
231
+ self.logger.debug("PM skills deployer not available")
232
+ except Exception as e:
233
+ self.logger.warning(f"PM skills verification failed: {e}")
234
+ # Don't print to console - this is a non-critical failure
235
+
173
236
  def _migrate_project_agents(self):
174
237
  """Migrate agents from old subdirectory structure to direct agents directory.
175
238
 
@@ -34,7 +34,7 @@ class GitRepository:
34
34
  def cache_path(self) -> Path:
35
35
  """Return cache directory path for this repository.
36
36
 
37
- Cache structure: ~/.claude-mpm/cache/remote-agents/{owner}/{repo}/{subdirectory}/
37
+ Cache structure: ~/.claude-mpm/cache/agents/{owner}/{repo}/{subdirectory}/
38
38
 
39
39
  Returns:
40
40
  Absolute path to cache directory for this repository
@@ -45,10 +45,10 @@ class GitRepository:
45
45
  ... subdirectory="agents"
46
46
  ... )
47
47
  >>> repo.cache_path
48
- Path('/Users/user/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/agents')
48
+ Path('/Users/user/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents')
49
49
  """
50
50
  home = Path.home()
51
- base_cache = home / ".claude-mpm" / "cache" / "remote-agents"
51
+ base_cache = home / ".claude-mpm" / "cache" / "agents"
52
52
 
53
53
  # Extract owner and repo from URL
54
54
  owner, repo = self._parse_github_url(self.url)
File without changes
@@ -206,8 +206,8 @@ class AgentBuilderService:
206
206
  """
207
207
  errors = []
208
208
 
209
- # Required fields
210
- required_fields = ["id", "name", "prompt", "model"]
209
+ # Required fields (model is optional - defaults to sonnet if not specified)
210
+ required_fields = ["id", "name", "prompt"]
211
211
  for field in required_fields:
212
212
  if field not in config:
213
213
  errors.append(f"Missing required field: {field}")
@@ -219,7 +219,7 @@ class AgentBuilderService:
219
219
  except AgentDeploymentError as e:
220
220
  errors.append(str(e))
221
221
 
222
- # Validate model
222
+ # Validate model (only if present)
223
223
  if "model" in config:
224
224
  try:
225
225
  self._validate_model(config["model"])
@@ -29,7 +29,7 @@ Error Handling:
29
29
 
30
30
  Example:
31
31
  >>> from pathlib import Path
32
- >>> manager = CacheGitManager(Path.home() / ".claude-mpm/cache/remote-agents")
32
+ >>> manager = CacheGitManager(Path.home() / ".claude-mpm/cache/agents")
33
33
  >>> if manager.is_git_repo():
34
34
  ... status = manager.get_status()
35
35
  ... print(f"Branch: {status['branch']}, Uncommitted: {len(status['uncommitted'])}")
@@ -76,7 +76,7 @@ class CacheGitManager:
76
76
  timeout: Git command timeout in seconds (default: 30)
77
77
 
78
78
  Example:
79
- >>> cache_dir = Path.home() / ".claude-mpm/cache/remote-agents"
79
+ >>> cache_dir = Path.home() / ".claude-mpm/cache/agents"
80
80
  >>> manager = CacheGitManager(cache_dir)
81
81
  """
82
82
  self.cache_path = Path(cache_path)
@@ -105,12 +105,12 @@ class CacheGitManager:
105
105
 
106
106
  Example:
107
107
  >>> # Case 1: cache_path inside repo (searches upward)
108
- >>> # cache_path: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/agents
109
- >>> # Found at: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents
108
+ >>> # cache_path: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents
109
+ >>> # Found at: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents
110
110
 
111
111
  >>> # Case 2: repo nested in cache_path (searches downward)
112
- >>> # cache_path: ~/.claude-mpm/cache/remote-agents
113
- >>> # Found at: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents
112
+ >>> # cache_path: ~/.claude-mpm/cache/agents
113
+ >>> # Found at: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents
114
114
  """
115
115
  # Strategy 1: Search upward (cache_path is inside repo)
116
116
  current = self.cache_path
@@ -876,13 +876,13 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
876
876
  user_agents_dir = potential_user_dir
877
877
  self.logger.info(f"Found user agents at: {user_agents_dir}")
878
878
 
879
- # Check for remote agents (cached from GitHub)
880
- remote_agents_dir = None
879
+ # Check for agents cache (from Git sources)
880
+ agents_cache_dir = None
881
881
  cache_dir = user_home / ".claude-mpm" / "cache"
882
- potential_remote_dir = cache_dir / "remote-agents"
883
- if potential_remote_dir.exists():
884
- remote_agents_dir = potential_remote_dir
885
- self.logger.info(f"Found remote agents cache at: {remote_agents_dir}")
882
+ potential_cache_dir = cache_dir / "agents"
883
+ if potential_cache_dir.exists():
884
+ agents_cache_dir = potential_cache_dir
885
+ self.logger.info(f"Found agents cache at: {agents_cache_dir}")
886
886
 
887
887
  # Get agents with version comparison and cleanup (4-tier discovery)
888
888
  agents_to_deploy, agent_sources, cleanup_results = (
@@ -890,7 +890,7 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
890
890
  system_templates_dir=system_templates_dir,
891
891
  project_agents_dir=project_agents_dir,
892
892
  user_agents_dir=user_agents_dir,
893
- remote_agents_dir=remote_agents_dir, # NEW: 4th tier
893
+ agents_cache_dir=agents_cache_dir, # NEW: 4th tier
894
894
  working_directory=self.working_directory,
895
895
  excluded_agents=excluded_agents,
896
896
  config=config,
@@ -898,6 +898,9 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
898
898
  )
899
899
  )
900
900
 
901
+ # Keep track of all enabled agents before filtering (for cleanup)
902
+ all_enabled_agents = agents_to_deploy.copy()
903
+
901
904
  # Compare with deployed versions if agents directory exists
902
905
  if agents_dir.exists():
903
906
  comparison_results = self.multi_source_service.compare_deployed_versions(
@@ -954,6 +957,25 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
954
957
  f"All {len(comparison_results.get('up_to_date', []))} agents are up to date"
955
958
  )
956
959
 
960
+ # Cleanup excluded agents (remove agents not in deployment list)
961
+ # CRITICAL: Use all_enabled_agents (before filtering for updates) to preserve up-to-date agents
962
+ # Bug fix (1M-XXX): Previously used filtered agents_to_deploy which could be empty,
963
+ # causing all agents to be removed when everything was up-to-date
964
+ exclusion_cleanup_results = self.multi_source_service.cleanup_excluded_agents(
965
+ deployed_agents_dir=agents_dir,
966
+ agents_to_deploy=all_enabled_agents,
967
+ )
968
+
969
+ # Add exclusion cleanup results to main cleanup results
970
+ if exclusion_cleanup_results.get("removed"):
971
+ cleanup_results.setdefault("excluded_removed", []).extend(
972
+ exclusion_cleanup_results["removed"]
973
+ )
974
+ self.logger.info(
975
+ f"Removed {len(exclusion_cleanup_results['removed'])} excluded agents: "
976
+ f"{', '.join(exclusion_cleanup_results['removed'])}"
977
+ )
978
+
957
979
  # Convert to list of Path objects
958
980
  template_files = list(agents_to_deploy.values())
959
981
 
@@ -248,7 +248,7 @@ class AgentDiscoveryService:
248
248
  return agent_info
249
249
 
250
250
  except yaml.YAMLError as e:
251
- self.logger.error(f"Invalid YAML frontmatter in {template_file.name}: {e}")
251
+ self.logger.warning(f"Invalid YAML frontmatter in {template_file.name}: {e}")
252
252
  return None
253
253
  except Exception as e:
254
254
  self.logger.error(
@@ -431,7 +431,7 @@ class AgentDiscoveryService:
431
431
  return True
432
432
 
433
433
  except yaml.YAMLError:
434
- self.logger.error(
434
+ self.logger.warning(
435
435
  f"Invalid YAML frontmatter in template: {template_file.name}"
436
436
  )
437
437
  return False
@@ -137,8 +137,8 @@ class AgentFormatConverter:
137
137
  else:
138
138
  pass
139
139
 
140
- # Extract additional fields
141
- model = self.extract_yaml_field(yaml_content, "model") or "sonnet"
140
+ # Extract additional fields - model is optional (Claude Code uses conversation model if not set)
141
+ model = self.extract_yaml_field(yaml_content, "model") # None if not specified
142
142
  author = (
143
143
  self.extract_yaml_field(yaml_content, "author")
144
144
  or "claude-mpm@anthropic.com"
@@ -147,7 +147,7 @@ class AgentFormatConverter:
147
147
  # Extract instructions from YAML content
148
148
  instructions = self._extract_instructions_from_yaml(yaml_content, agent_name)
149
149
 
150
- # Map model names to Claude Code format
150
+ # Map model names to Claude Code format (only if model is specified)
151
151
  model_map = {
152
152
  "claude-3-5-sonnet-20241022": "sonnet",
153
153
  "claude-3-5-sonnet": "sonnet",
@@ -159,7 +159,8 @@ class AgentFormatConverter:
159
159
  "opus": "opus",
160
160
  }
161
161
 
162
- mapped_model = model_map.get(model, "sonnet")
162
+ # Only map model if it's not None (preserve None for agents without model field)
163
+ mapped_model = model_map.get(model, model) if model is not None else None
163
164
 
164
165
  # Create multiline description with example (Claude Code format)
165
166
  multiline_description = f"""{description}
@@ -172,16 +173,25 @@ assistant: "I'll use the {name} agent to provide specialized assistance."
172
173
 
173
174
  # Build new YAML frontmatter - Claude Code compatible format
174
175
  # NOTE: Removed tags field and other non-essential fields for Claude Code compatibility
175
- new_frontmatter = f"""---
176
- name: {name}
177
- description: |
178
- {self._indent_text(multiline_description, 2)}
179
- model: {mapped_model}
180
- version: "{version}"
181
- author: "{author}"
182
- ---
176
+ frontmatter_lines = [
177
+ "---",
178
+ f"name: {name}",
179
+ "description: |",
180
+ f" {self._indent_text(multiline_description, 2)}",
181
+ ]
183
182
 
184
- """
183
+ # Only include model field if explicitly set in source
184
+ if mapped_model is not None:
185
+ frontmatter_lines.append(f"model: {mapped_model}")
186
+
187
+ frontmatter_lines.extend([
188
+ f'version: "{version}"',
189
+ f'author: "{author}"',
190
+ "---",
191
+ "",
192
+ ])
193
+
194
+ new_frontmatter = "\n".join(frontmatter_lines)
185
195
 
186
196
  return new_frontmatter + instructions
187
197