claude-mpm 5.4.21__py3-none-any.whl → 5.4.59__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 (176) 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 +771 -1019
  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 +12 -0
  15. claude_mpm/cli/commands/mpm_init/core.py +72 -0
  16. claude_mpm/cli/commands/postmortem.py +1 -1
  17. claude_mpm/cli/commands/profile.py +276 -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 +147 -0
  23. claude_mpm/cli/parsers/skills_parser.py +0 -6
  24. claude_mpm/cli/startup.py +506 -180
  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 +61 -0
  40. claude_mpm/core/shared/config_loader.py +3 -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.DWzvg0-y.css +1 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +1 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BSNlmTZj.js +1 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
  77. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
  78. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
  79. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
  80. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  81. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
  82. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
  83. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
  84. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
  85. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
  86. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
  88. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
  89. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
  90. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
  91. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  92. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/NqQ1dWOy.js +1 -0
  93. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
  94. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Vzk33B_K.js +2 -0
  95. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
  96. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
  97. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
  98. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
  99. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
  100. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
  101. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
  102. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
  103. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
  104. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.m1gL8KXf.js +1 -0
  105. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
  106. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
  107. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  108. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  109. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  110. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  111. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  112. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  113. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  114. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  115. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  116. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  117. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  118. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  119. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  120. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  121. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  122. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  123. claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
  124. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  125. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  126. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  127. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  128. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  129. claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
  130. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  131. claude_mpm/init.py +276 -0
  132. claude_mpm/models/git_repository.py +3 -3
  133. claude_mpm/scripts/start_activity_logging.py +0 -0
  134. claude_mpm/services/agents/agent_builder.py +3 -3
  135. claude_mpm/services/agents/cache_git_manager.py +6 -6
  136. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  137. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -2
  138. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  139. claude_mpm/services/agents/deployment/agent_template_builder.py +31 -19
  140. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  141. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  142. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  143. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
  144. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
  145. claude_mpm/services/agents/git_source_manager.py +23 -4
  146. claude_mpm/services/agents/recommender.py +5 -3
  147. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  148. claude_mpm/services/agents/sources/git_source_sync_service.py +121 -10
  149. claude_mpm/services/agents/startup_sync.py +22 -2
  150. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  151. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  152. claude_mpm/services/git/git_operations_service.py +8 -8
  153. claude_mpm/services/monitor/management/lifecycle.py +7 -1
  154. claude_mpm/services/monitor/server.py +473 -3
  155. claude_mpm/services/pm_skills_deployer.py +711 -0
  156. claude_mpm/services/profile_manager.py +337 -0
  157. claude_mpm/services/skills/git_skill_source_manager.py +148 -11
  158. claude_mpm/services/skills/selective_skill_deployer.py +97 -48
  159. claude_mpm/services/skills_deployer.py +161 -65
  160. claude_mpm/services/socketio/dashboard_server.py +1 -0
  161. claude_mpm/services/socketio/event_normalizer.py +37 -6
  162. claude_mpm/services/socketio/server/core.py +262 -123
  163. claude_mpm/skills/bundled/security-scanning.md +112 -0
  164. claude_mpm/skills/skill_manager.py +98 -3
  165. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  166. claude_mpm/utils/agent_dependency_loader.py +14 -2
  167. claude_mpm/utils/agent_filters.py +1 -1
  168. claude_mpm/utils/migration.py +4 -4
  169. claude_mpm/utils/robust_installer.py +47 -3
  170. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +7 -4
  171. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +175 -81
  172. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
  173. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
  174. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
  175. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  176. {claude_mpm-5.4.21.dist-info → claude_mpm-5.4.59.dist-info}/top_level.txt +0 -0
@@ -51,6 +51,59 @@ logger = get_logger(__name__)
51
51
  # Deployment tracking index file
52
52
  DEPLOYED_INDEX_FILE = ".mpm-deployed-skills.json"
53
53
 
54
+ # Core skills that are universally useful across all projects
55
+ # These are deployed when skill mapping returns too many skills (>60)
56
+ # Target: ~25-30 core skills for balanced functionality
57
+ CORE_SKILLS = {
58
+ # Universal debugging and verification (4 skills)
59
+ "universal-debugging-systematic-debugging",
60
+ "universal-debugging-verification-before-completion",
61
+ "universal-verification-pre-merge",
62
+ "universal-verification-screenshot",
63
+
64
+ # Universal testing patterns (2 skills)
65
+ "universal-testing-test-driven-development",
66
+ "universal-testing-testing-anti-patterns",
67
+
68
+ # Universal architecture and design (1 skill)
69
+ "universal-architecture-software-patterns",
70
+
71
+ # Universal infrastructure (3 skills)
72
+ "universal-infrastructure-env-manager",
73
+ "universal-infrastructure-docker",
74
+ "universal-infrastructure-github-actions",
75
+
76
+ # Universal collaboration (1 skill)
77
+ "universal-collaboration-stacked-prs",
78
+
79
+ # Universal emergency/operations (1 skill)
80
+ "toolchains-universal-emergency-release",
81
+ "toolchains-universal-dependency-audit",
82
+
83
+ # Common language toolchains (6 skills)
84
+ "toolchains-typescript-core",
85
+ "toolchains-python-core",
86
+ "toolchains-javascript-tooling-biome",
87
+ "toolchains-python-tooling-mypy",
88
+ "toolchains-typescript-testing-vitest",
89
+ "toolchains-python-frameworks-flask",
90
+
91
+ # Common web frameworks (4 skills)
92
+ "toolchains-javascript-frameworks-nextjs",
93
+ "toolchains-nextjs-core",
94
+ "toolchains-typescript-frameworks-nodejs-backend",
95
+ "toolchains-javascript-frameworks-react-state-machine",
96
+
97
+ # Common testing tools (2 skills)
98
+ "toolchains-javascript-testing-playwright",
99
+ "toolchains-typescript-testing-jest",
100
+
101
+ # Common data/UI tools (3 skills)
102
+ "universal-data-xlsx",
103
+ "toolchains-ui-styling-tailwind",
104
+ "toolchains-ui-components-headlessui",
105
+ }
106
+
54
107
 
55
108
  def parse_agent_frontmatter(agent_file: Path) -> Dict[str, Any]:
56
109
  """Parse YAML frontmatter from agent markdown file.
@@ -140,50 +193,49 @@ def get_skills_from_agent(frontmatter: Dict[str, Any]) -> Set[str]:
140
193
  def get_skills_from_mapping(agent_ids: List[str]) -> Set[str]:
141
194
  """Get skills for agents using SkillToAgentMapper inference.
142
195
 
143
- Uses SkillToAgentMapper to find all skills associated with given agent IDs.
144
- This provides pattern-based skill discovery beyond explicit frontmatter declarations.
196
+ DEPRECATED: This function is deprecated as of Phase 3 refactor.
197
+ Skills are now declared exclusively in agent frontmatter.
198
+
199
+ The static skill_to_agent_mapping.yaml is no longer used for skill deployment.
200
+ Each agent must declare its skills in frontmatter or it gets zero skills.
201
+
202
+ This function remains for backward compatibility but is NO LONGER CALLED
203
+ by get_required_skills_from_agents().
145
204
 
146
205
  Args:
147
- agent_ids: List of agent identifiers (e.g., ["python-engineer", "typescript-engineer"])
206
+ agent_ids: List of DEPLOYED agent identifiers (e.g., ["python-engineer", "typescript-engineer"])
207
+ These should be extracted from ~/.claude/agents/*.md files only.
148
208
 
149
209
  Returns:
150
- Set of unique skill names inferred from mapping configuration
210
+ Set of unique skill names inferred from mapping configuration for DEPLOYED agents only
211
+ NOTE: This is now an empty set as the function is deprecated.
151
212
 
152
213
  Example:
153
- >>> agent_ids = ["python-engineer", "typescript-engineer"]
154
- >>> skills = get_skills_from_mapping(agent_ids)
155
- >>> print(f"Found {len(skills)} skills from mapping")
214
+ >>> # DEPRECATED - use frontmatter instead
215
+ >>> deployed_agent_ids = ["python-engineer", "typescript-engineer", "qa"]
216
+ >>> skills = get_skills_from_mapping(deployed_agent_ids) # Returns empty set
156
217
  """
157
- try:
158
- mapper = SkillToAgentMapper()
159
- all_skills = set()
160
-
161
- for agent_id in agent_ids:
162
- agent_skills = mapper.get_skills_for_agent(agent_id)
163
- if agent_skills:
164
- all_skills.update(agent_skills)
165
- logger.debug(f"Mapped {len(agent_skills)} skills to {agent_id}")
166
-
167
- logger.info(
168
- f"Mapped {len(all_skills)} unique skills for {len(agent_ids)} agents"
169
- )
170
- return all_skills
171
-
172
- except Exception as e:
173
- logger.warning(f"Failed to load SkillToAgentMapper: {e}")
174
- logger.info("Falling back to frontmatter-only skill discovery")
175
- return set()
218
+ # DEPRECATED: Return empty set
219
+ logger.warning(
220
+ "get_skills_from_mapping() is DEPRECATED and returns empty set. "
221
+ "Skills are now declared in agent frontmatter only. "
222
+ "Update your agents with 'skills:' field in frontmatter."
223
+ )
224
+ return set()
176
225
 
177
226
 
178
227
  def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
179
228
  """Extract all skills referenced by deployed agents.
180
229
 
181
- Combines skills from two sources:
182
- 1. Explicit frontmatter declarations (skills: field in agent .md files)
183
- 2. SkillToAgentMapper inference (pattern-based skill discovery)
230
+ MAJOR CHANGE (Phase 3): Now ONLY uses frontmatter-declared skills.
231
+ The static skill_to_agent_mapping.yaml is DEPRECATED. Each agent must
232
+ declare its skills in frontmatter or it gets zero skills deployed.
184
233
 
185
- This dual-source approach ensures agents get both explicitly declared skills
186
- and skills inferred from their domain/toolchain patterns.
234
+ This change:
235
+ - Eliminates dual-source complexity (frontmatter + mapping)
236
+ - Makes skill requirements explicit per agent
237
+ - Enables per-agent customization via frontmatter
238
+ - Removes dependency on static YAML mapping
187
239
 
188
240
  Args:
189
241
  agents_dir: Path to deployed agents directory (e.g., .claude/agents/)
@@ -204,13 +256,11 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
204
256
  agent_files = list(agents_dir.glob("*.md"))
205
257
  logger.debug(f"Scanning {len(agent_files)} agent files in {agents_dir}")
206
258
 
207
- # Source 1: Extract skills from frontmatter
259
+ # ONLY use frontmatter skills - no more mapping inference
208
260
  frontmatter_skills = set()
209
- agent_ids = []
210
261
 
211
262
  for agent_file in agent_files:
212
263
  agent_id = agent_file.stem
213
- agent_ids.append(agent_id)
214
264
 
215
265
  frontmatter = parse_agent_frontmatter(agent_file)
216
266
  agent_skills = get_skills_from_agent(frontmatter)
@@ -220,24 +270,23 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
220
270
  logger.debug(
221
271
  f"Agent {agent_id}: {len(agent_skills)} skills from frontmatter"
222
272
  )
273
+ else:
274
+ logger.debug(f"Agent {agent_id}: No skills declared in frontmatter")
223
275
 
224
- logger.info(f"Found {len(frontmatter_skills)} unique skills from frontmatter")
225
-
226
- # Source 2: Get skills from SkillToAgentMapper
227
- mapped_skills = get_skills_from_mapping(agent_ids)
228
-
229
- # Combine both sources
230
- required_skills = frontmatter_skills | mapped_skills
276
+ logger.info(
277
+ f"Found {len(frontmatter_skills)} unique skills from agent frontmatter "
278
+ f"(static mapping no longer used)"
279
+ )
231
280
 
232
281
  # Normalize skill paths: convert slashes to dashes for compatibility with deployment
233
- # SkillToAgentMapper returns paths like "toolchains/python/frameworks/django"
234
- # but deployment expects "toolchains-python-frameworks-django"
235
- normalized_skills = {skill.replace("/", "-") for skill in required_skills}
282
+ # Some skills may use slash format, normalize to dashes
283
+ normalized_skills = {skill.replace("/", "-") for skill in frontmatter_skills}
236
284
 
237
- logger.info(
238
- f"Combined {len(frontmatter_skills)} frontmatter + {len(mapped_skills)} mapped "
239
- f"= {len(required_skills)} total unique skills (normalized to {len(normalized_skills)})"
240
- )
285
+ if normalized_skills != frontmatter_skills:
286
+ logger.debug(
287
+ f"Normalized {len(frontmatter_skills)} skills to {len(normalized_skills)} "
288
+ "(converted slashes to dashes)"
289
+ )
241
290
 
242
291
  return normalized_skills
243
292
 
@@ -174,44 +174,93 @@ class SkillsDeployerService(LoggerMixin):
174
174
  if selective:
175
175
  # Auto-detect project root if not provided
176
176
  if project_root is None:
177
- # Try to find project root by looking for .claude directory
177
+ # Try to find project root by looking for .claude-mpm directory
178
178
  # Start from current directory and walk up
179
179
  current = Path.cwd()
180
180
  while current != current.parent:
181
- if (current / ".claude").exists():
181
+ if (current / ".claude-mpm").exists():
182
182
  project_root = current
183
183
  break
184
184
  current = current.parent
185
185
 
186
+ # Read skills from configuration.yaml instead of agent frontmatter
186
187
  if project_root:
187
- agents_dir = Path(project_root) / ".claude" / "agents"
188
+ config_path = Path(project_root) / ".claude-mpm" / "configuration.yaml"
188
189
  else:
189
- # Fallback to current directory's .claude/agents
190
- agents_dir = Path.cwd() / ".claude" / "agents"
190
+ # Fallback to current directory's configuration
191
+ config_path = Path.cwd() / ".claude-mpm" / "configuration.yaml"
191
192
 
192
193
  from claude_mpm.services.skills.selective_skill_deployer import (
193
194
  get_required_skills_from_agents,
195
+ get_skills_to_deploy,
196
+ save_agent_skills_to_config,
194
197
  )
195
198
 
196
- required_skill_names = get_required_skills_from_agents(agents_dir)
199
+ # Check if agent_referenced is empty and needs to be populated
200
+ required_skill_names, source = get_skills_to_deploy(config_path)
201
+
202
+ if not required_skill_names and project_root:
203
+ # agent_referenced is empty, scan deployed agents to populate it
204
+ agents_dir = Path(project_root) / ".claude" / "agents"
205
+ if agents_dir.exists():
206
+ self.logger.info(
207
+ "agent_referenced is empty in configuration.yaml, scanning deployed agents..."
208
+ )
209
+ agent_skills = get_required_skills_from_agents(agents_dir)
210
+ if agent_skills:
211
+ save_agent_skills_to_config(list(agent_skills), config_path)
212
+ self.logger.info(
213
+ f"Populated agent_referenced with {len(agent_skills)} skills from deployed agents"
214
+ )
215
+ # Re-read configuration after update
216
+ required_skill_names, source = get_skills_to_deploy(config_path)
217
+ else:
218
+ self.logger.warning(
219
+ "No skills found in deployed agents - configuration.yaml remains empty"
220
+ )
221
+ else:
222
+ self.logger.warning(
223
+ f"Agents directory not found at {agents_dir} - cannot scan for skills"
224
+ )
197
225
 
198
226
  if required_skill_names:
227
+ # Convert required_skill_names to a set for O(1) lookup
228
+ required_set = set(required_skill_names)
229
+
199
230
  # Filter to only required skills
200
- # Match on either 'name' or 'skill_id' field
231
+ # Match on: 'name', 'skill_id', or normalized 'source_path'
232
+ # source_path example: "universal/web/api-design-patterns/SKILL.md"
233
+ # normalized: "universal-web-api-design-patterns"
234
+ def skill_matches_requirement(skill):
235
+ # Check basic name and skill_id
236
+ if skill.get("name") in required_set:
237
+ return True
238
+ if skill.get("skill_id") in required_set:
239
+ return True
240
+
241
+ # Check normalized source_path
242
+ source_path = skill.get("source_path", "")
243
+ if source_path:
244
+ # Remove /SKILL.md suffix and replace / with -
245
+ normalized = source_path.replace("/SKILL.md", "").replace(
246
+ "/", "-"
247
+ )
248
+ if normalized in required_set:
249
+ return True
250
+
251
+ return False
252
+
201
253
  filtered_skills = [
202
- s
203
- for s in filtered_skills
204
- if s.get("name") in required_skill_names
205
- or s.get("skill_id") in required_skill_names
254
+ s for s in filtered_skills if skill_matches_requirement(s)
206
255
  ]
207
256
 
208
257
  self.logger.info(
209
258
  f"Selective deployment: {len(filtered_skills)}/{total_available} skills "
210
- f"(agent-referenced only)"
259
+ f"(source: {source})"
211
260
  )
212
261
  else:
213
262
  self.logger.warning(
214
- f"No skills found in agent frontmatter at {agents_dir}. "
263
+ f"No skills found in configuration at {config_path}. "
215
264
  f"Deploying all {total_available} skills."
216
265
  )
217
266
  else:
@@ -224,12 +273,19 @@ class SkillsDeployerService(LoggerMixin):
224
273
  skipped = []
225
274
  errors = []
226
275
 
227
- # Extract skill names for cleanup (needed regardless of deployment outcome)
228
- filtered_skills_names = [
229
- skill["name"]
230
- for skill in filtered_skills
231
- if isinstance(skill, dict) and "name" in skill
232
- ]
276
+ # Extract normalized skill names for cleanup (needed regardless of deployment outcome)
277
+ # Must match the names used during deployment (normalized from source_path)
278
+ filtered_skills_names = []
279
+ for skill in filtered_skills:
280
+ if isinstance(skill, dict) and "name" in skill:
281
+ source_path = skill.get("source_path", "")
282
+ if source_path:
283
+ # Normalize: "universal/web/api-design-patterns/SKILL.md" -> "universal-web-api-design-patterns"
284
+ normalized = source_path.replace("/SKILL.md", "").replace("/", "-")
285
+ filtered_skills_names.append(normalized)
286
+ else:
287
+ # Fallback to skill name
288
+ filtered_skills_names.append(skill["name"])
233
289
 
234
290
  for skill in filtered_skills:
235
291
  try:
@@ -243,9 +299,25 @@ class SkillsDeployerService(LoggerMixin):
243
299
  skill, skills_data["temp_dir"], collection_name, force=force
244
300
  )
245
301
  if result["deployed"]:
246
- deployed.append(skill["name"])
302
+ # Use normalized name for reporting
303
+ source_path = skill.get("source_path", "")
304
+ if source_path:
305
+ normalized = source_path.replace("/SKILL.md", "").replace(
306
+ "/", "-"
307
+ )
308
+ deployed.append(normalized)
309
+ else:
310
+ deployed.append(skill["name"])
247
311
  elif result["skipped"]:
248
- skipped.append(skill["name"])
312
+ # Use normalized name for reporting
313
+ source_path = skill.get("source_path", "")
314
+ if source_path:
315
+ normalized = source_path.replace("/SKILL.md", "").replace(
316
+ "/", "-"
317
+ )
318
+ skipped.append(normalized)
319
+ else:
320
+ skipped.append(skill["name"])
249
321
  if result["error"]:
250
322
  errors.append(result["error"])
251
323
  except Exception as e:
@@ -257,9 +329,9 @@ class SkillsDeployerService(LoggerMixin):
257
329
  self.logger.error(f"Failed to deploy {skill_name}: {e}")
258
330
  errors.append(f"{skill_name}: {e}")
259
331
 
260
- # Step 5: Cleanup orphaned skills (if selective mode enabled)
332
+ # Step 5: Cleanup orphaned skills (always run in selective mode)
261
333
  cleanup_result = {"removed_count": 0, "removed_skills": []}
262
- if selective and len(deployed) > 0:
334
+ if selective:
263
335
  # Get the set of skills that should remain deployed
264
336
  # This is the union of what we just deployed and what was already there
265
337
  try:
@@ -267,7 +339,8 @@ class SkillsDeployerService(LoggerMixin):
267
339
  cleanup_orphan_skills,
268
340
  )
269
341
 
270
- # Only cleanup if we're in selective mode
342
+ # Cleanup orphaned skills not referenced by agents
343
+ # This runs even if nothing new was deployed to remove stale skills
271
344
  cleanup_result = cleanup_orphan_skills(
272
345
  self.CLAUDE_SKILLS_DIR, set(filtered_skills_names)
273
346
  )
@@ -804,55 +877,76 @@ class SkillsDeployerService(LoggerMixin):
804
877
  Dict with deployed, skipped, error flags
805
878
  """
806
879
  skill_name = skill["name"]
807
- target_dir = self.CLAUDE_SKILLS_DIR / skill_name
880
+
881
+ # Use normalized source_path for both target directory and deployment tracking
882
+ # This ensures consistency with configuration.yaml skill names
883
+ source_path = skill.get("source_path", "")
884
+ if source_path:
885
+ # Normalize: "universal/web/api-design-patterns/SKILL.md" -> "universal-web-api-design-patterns"
886
+ normalized_name = source_path.replace("/SKILL.md", "").replace("/", "-")
887
+ target_dir = self.CLAUDE_SKILLS_DIR / normalized_name
888
+ else:
889
+ # Fallback to skill name if no source_path
890
+ target_dir = self.CLAUDE_SKILLS_DIR / skill_name
808
891
 
809
892
  # Check if already deployed
810
893
  if target_dir.exists() and not force:
811
894
  self.logger.debug(f"Skipped {skill_name} (already deployed)")
812
895
  return {"deployed": False, "skipped": True, "error": None}
813
896
 
814
- # Find skill source in collection directory
815
- # Updated structure: collection_dir / skills / category / skill-name
816
- # OR: collection_dir / universal / skill-name
817
- # OR: collection_dir / toolchains / toolchain-name / skill-name
897
+ # Find skill source using source_path from manifest
898
+ source_dir = None
818
899
 
819
- skills_base = collection_dir / "skills"
820
- category = skill.get("category", "")
900
+ if source_path:
901
+ # Direct lookup using source_path (most reliable)
902
+ # Example: "universal/web/api-design-patterns/SKILL.md" -> "universal/web/api-design-patterns"
903
+ skill_dir_path = source_path.replace("/SKILL.md", "")
904
+ potential_source = collection_dir / skill_dir_path
905
+ if potential_source.exists():
906
+ source_dir = potential_source
907
+ else:
908
+ self.logger.debug(
909
+ f"Source path {skill_dir_path} not found, trying fallback search"
910
+ )
821
911
 
822
- # Try multiple possible locations
823
- source_dir = None
824
- search_paths = []
825
-
826
- # Try category-based path
827
- if category and skills_base.exists():
828
- search_paths.append(skills_base / category / skill_name)
829
-
830
- # Try universal/toolchains structure
831
- if (collection_dir / "universal").exists():
832
- search_paths.append(collection_dir / "universal" / skill_name)
833
-
834
- if (collection_dir / "toolchains").exists():
835
- toolchain_dir = collection_dir / "toolchains"
836
- for tc in toolchain_dir.iterdir():
837
- if tc.is_dir():
838
- search_paths.append(tc / skill_name)
839
-
840
- # Search in all possible locations
841
- for path in search_paths:
842
- if path.exists():
843
- source_dir = path
844
- break
845
-
846
- # Fallback: search recursively for skill in skills directory
847
- if not source_dir and skills_base.exists():
848
- for cat_dir in skills_base.iterdir():
849
- if not cat_dir.is_dir():
850
- continue
851
- potential = cat_dir / skill_name
852
- if potential.exists():
853
- source_dir = potential
912
+ # Fallback: search using old logic (for backward compatibility)
913
+ if not source_dir:
914
+ skills_base = collection_dir / "skills"
915
+ category = skill.get("category", "")
916
+
917
+ # Try multiple possible locations
918
+ search_paths = []
919
+
920
+ # Try category-based path
921
+ if category and skills_base.exists():
922
+ search_paths.append(skills_base / category / skill_name)
923
+
924
+ # Try universal/toolchains structure
925
+ if (collection_dir / "universal").exists():
926
+ search_paths.append(collection_dir / "universal" / skill_name)
927
+
928
+ if (collection_dir / "toolchains").exists():
929
+ toolchain_dir = collection_dir / "toolchains"
930
+ for tc in toolchain_dir.iterdir():
931
+ if tc.is_dir():
932
+ search_paths.append(tc / skill_name)
933
+
934
+ # Search in all possible locations
935
+ for path in search_paths:
936
+ if path.exists():
937
+ source_dir = path
854
938
  break
855
939
 
940
+ # Final fallback: search recursively for skill in skills directory
941
+ if not source_dir and skills_base.exists():
942
+ for cat_dir in skills_base.iterdir():
943
+ if not cat_dir.is_dir():
944
+ continue
945
+ potential = cat_dir / skill_name
946
+ if potential.exists():
947
+ source_dir = potential
948
+ break
949
+
856
950
  if not source_dir or not source_dir.exists():
857
951
  return {
858
952
  "deployed": False,
@@ -887,12 +981,14 @@ class SkillsDeployerService(LoggerMixin):
887
981
  # NOTE: We use copy instead of symlink to maintain Claude Code compatibility
888
982
  shutil.copytree(source_dir, target_dir)
889
983
 
890
- # Track deployment in index
984
+ # Track deployment in index using normalized name
891
985
  from claude_mpm.services.skills.selective_skill_deployer import (
892
986
  track_deployed_skill,
893
987
  )
894
988
 
895
- track_deployed_skill(self.CLAUDE_SKILLS_DIR, skill_name, collection_name)
989
+ # Use normalized name for tracking (matches configuration.yaml format)
990
+ track_name = normalized_name if source_path else skill_name
991
+ track_deployed_skill(self.CLAUDE_SKILLS_DIR, track_name, collection_name)
896
992
 
897
993
  self.logger.debug(
898
994
  f"Deployed {skill_name} from {source_dir} to {target_dir}"
@@ -152,6 +152,7 @@ class DashboardServer(SocketIOServiceInterface):
152
152
 
153
153
  # Register handlers for all events we want to relay from monitor to dashboard
154
154
  relay_events = [
155
+ "claude_event", # Tool events from Claude Code hooks
155
156
  "session_started",
156
157
  "session_ended",
157
158
  "claude_status",
@@ -6,7 +6,7 @@ This normalizer ensures all events follow a consistent schema before broadcastin
6
6
  providing backward compatibility while establishing a standard format.
7
7
 
8
8
  DESIGN DECISION: Transform all events to a consistent schema:
9
- - event: Socket.IO event name (always "claude_event")
9
+ - event: Socket.IO event name (always "mpm_event")
10
10
  - type: Main category (hook, system, session, file, connection)
11
11
  - subtype: Specific event type (pre_tool, heartbeat, started, etc.)
12
12
  - timestamp: ISO format timestamp
@@ -72,7 +72,7 @@ class NormalizedEvent:
72
72
  structure explicit and self-documenting.
73
73
  """
74
74
 
75
- event: str = "claude_event" # Socket.IO event name
75
+ event: str = "mpm_event" # Socket.IO event name
76
76
  source: str = "" # WHERE the event comes from
77
77
  type: str = "" # WHAT category of event
78
78
  subtype: str = "" # Specific event type
@@ -81,6 +81,8 @@ class NormalizedEvent:
81
81
  correlation_id: Optional[str] = (
82
82
  None # For correlating related events (e.g., pre_tool/post_tool)
83
83
  )
84
+ session_id: Optional[str] = None # Session identifier for stream grouping
85
+ cwd: Optional[str] = None # Working directory for project identification
84
86
 
85
87
  def to_dict(self) -> Dict[str, Any]:
86
88
  """Convert to dictionary for emission."""
@@ -95,6 +97,12 @@ class NormalizedEvent:
95
97
  # Include correlation_id if present
96
98
  if self.correlation_id:
97
99
  result["correlation_id"] = self.correlation_id
100
+ # Include session_id if present
101
+ if self.session_id:
102
+ result["session_id"] = self.session_id
103
+ # Include cwd if present
104
+ if self.cwd:
105
+ result["cwd"] = self.cwd
98
106
  return result
99
107
 
100
108
 
@@ -113,6 +121,7 @@ class EventNormalizer:
113
121
  "pre_response": (EventType.HOOK, "pre_response"),
114
122
  "post_response": (EventType.HOOK, "post_response"),
115
123
  "hook_event": (EventType.HOOK, "generic"),
124
+ "hook_execution": (EventType.HOOK, "execution"), # Hook execution metadata
116
125
  "UserPrompt": (EventType.HOOK, "user_prompt"), # Legacy format
117
126
  # Test events (legacy format)
118
127
  "TestStart": (EventType.TEST, "start"),
@@ -225,20 +234,32 @@ class EventNormalizer:
225
234
  # Get or generate timestamp
226
235
  timestamp = self._extract_timestamp(event_data)
227
236
 
228
- # Extract correlation_id if present
237
+ # Extract correlation_id, session_id, and cwd if present
229
238
  correlation_id = None
239
+ session_id = None
240
+ cwd = None
230
241
  if isinstance(event_data, dict):
231
242
  correlation_id = event_data.get("correlation_id")
243
+ # Try both naming conventions for session_id
244
+ session_id = event_data.get("session_id") or event_data.get("sessionId")
245
+ # Try multiple field names for working directory
246
+ cwd = (
247
+ event_data.get("cwd")
248
+ or event_data.get("working_directory")
249
+ or event_data.get("workingDirectory")
250
+ )
232
251
 
233
252
  # Create normalized event
234
253
  normalized = NormalizedEvent(
235
- event="claude_event",
254
+ event="mpm_event",
236
255
  source=event_source,
237
256
  type=event_type,
238
257
  subtype=subtype,
239
258
  timestamp=timestamp,
240
259
  data=data,
241
260
  correlation_id=correlation_id,
261
+ session_id=session_id,
262
+ cwd=cwd,
242
263
  )
243
264
 
244
265
  self.stats["normalized"] += 1
@@ -252,7 +273,7 @@ class EventNormalizer:
252
273
 
253
274
  # Return a generic event on error
254
275
  return NormalizedEvent(
255
- event="claude_event",
276
+ event="mpm_event",
256
277
  source="system",
257
278
  type="unknown",
258
279
  subtype="error",
@@ -285,8 +306,16 @@ class EventNormalizer:
285
306
  # If source is not a valid EventSource value, keep it as-is
286
307
  pass
287
308
 
309
+ # Extract session_id and cwd, trying multiple naming conventions
310
+ session_id = event_data.get("session_id") or event_data.get("sessionId")
311
+ cwd = (
312
+ event_data.get("cwd")
313
+ or event_data.get("working_directory")
314
+ or event_data.get("workingDirectory")
315
+ )
316
+
288
317
  return NormalizedEvent(
289
- event="claude_event", # Always use standard event name
318
+ event="mpm_event", # Always use standard event name
290
319
  source=source,
291
320
  type=event_data.get("type", "unknown"),
292
321
  subtype=event_data.get("subtype", "generic"),
@@ -295,6 +324,8 @@ class EventNormalizer:
295
324
  ),
296
325
  data=event_data.get("data", {}),
297
326
  correlation_id=event_data.get("correlation_id"),
327
+ session_id=session_id,
328
+ cwd=cwd,
298
329
  )
299
330
 
300
331
  def _extract_event_info(self, event_data: Any) -> Tuple[str, str, Dict[str, Any]]: