claude-mpm 5.4.36__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.
Files changed (137) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +489 -177
  3. claude_mpm/agents/base_agent.json +1 -1
  4. claude_mpm/agents/frontmatter_validator.py +2 -2
  5. claude_mpm/cli/commands/configure_agent_display.py +12 -0
  6. claude_mpm/cli/commands/mpm_init/core.py +72 -0
  7. claude_mpm/cli/commands/profile.py +276 -0
  8. claude_mpm/cli/commands/skills.py +14 -18
  9. claude_mpm/cli/executor.py +10 -0
  10. claude_mpm/cli/parsers/base_parser.py +7 -0
  11. claude_mpm/cli/parsers/profile_parser.py +147 -0
  12. claude_mpm/cli/parsers/skills_parser.py +0 -6
  13. claude_mpm/cli/startup.py +433 -147
  14. claude_mpm/commands/mpm-config.md +13 -250
  15. claude_mpm/commands/mpm-doctor.md +9 -22
  16. claude_mpm/commands/mpm-help.md +5 -206
  17. claude_mpm/commands/mpm-init.md +81 -507
  18. claude_mpm/commands/mpm-monitor.md +15 -402
  19. claude_mpm/commands/mpm-organize.md +61 -441
  20. claude_mpm/commands/mpm-postmortem.md +6 -108
  21. claude_mpm/commands/mpm-session-resume.md +12 -363
  22. claude_mpm/commands/mpm-status.md +5 -69
  23. claude_mpm/commands/mpm-ticket-view.md +52 -495
  24. claude_mpm/commands/mpm-version.md +5 -107
  25. claude_mpm/core/optimized_startup.py +61 -0
  26. claude_mpm/core/shared/config_loader.py +3 -1
  27. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
  28. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
  29. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CWc5urbQ.js → 4TdZjIqw.js} +1 -1
  30. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
  31. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
  32. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
  33. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
  34. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
  35. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
  36. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
  37. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  38. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uj46x2Wr.js → BSNlmTZj.js} +1 -1
  39. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
  40. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
  41. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
  42. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DjhvlsAc.js → NqQ1dWOy.js} +1 -1
  77. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
  78. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{N4qtv3Hx.js → Vzk33B_K.js} +1 -1
  79. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
  80. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
  81. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
  82. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
  83. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
  84. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
  85. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
  86. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
  88. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.CAGBuiOw.js → 0.m1gL8KXf.js} +1 -1
  89. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
  90. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
  91. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  92. claude_mpm/dashboard/static/svelte-build/index.html +10 -10
  93. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  94. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  95. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  96. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  97. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  98. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  99. claude_mpm/init.py +276 -0
  100. claude_mpm/scripts/start_activity_logging.py +0 -0
  101. claude_mpm/services/agents/agent_builder.py +3 -3
  102. claude_mpm/services/agents/deployment/agent_deployment.py +22 -0
  103. claude_mpm/services/agents/deployment/agent_discovery_service.py +3 -1
  104. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  105. claude_mpm/services/agents/deployment/agent_template_builder.py +29 -17
  106. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  107. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  108. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +149 -4
  109. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +47 -26
  110. claude_mpm/services/agents/git_source_manager.py +21 -2
  111. claude_mpm/services/agents/sources/git_source_sync_service.py +116 -5
  112. claude_mpm/services/monitor/management/lifecycle.py +7 -1
  113. claude_mpm/services/pm_skills_deployer.py +711 -0
  114. claude_mpm/services/profile_manager.py +337 -0
  115. claude_mpm/services/skills/git_skill_source_manager.py +148 -11
  116. claude_mpm/services/skills/selective_skill_deployer.py +97 -48
  117. claude_mpm/services/skills_deployer.py +161 -65
  118. claude_mpm/skills/bundled/security-scanning.md +112 -0
  119. claude_mpm/skills/skill_manager.py +98 -3
  120. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  121. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +3 -2
  122. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +126 -67
  123. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +0 -1
  124. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +0 -1
  125. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +0 -1
  126. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +0 -1
  127. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +0 -2
  128. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +0 -2
  129. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +0 -1
  130. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +0 -1
  131. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +0 -10
  132. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  133. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
  134. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
  135. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
  136. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  137. {claude_mpm-5.4.36.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}"
@@ -81,6 +81,36 @@ API_KEY = "sk-1234567890abcdef" # In code! # pragma: allowlist secret
81
81
 
82
82
  # ✅ Safe: Use environment variables
83
83
  API_KEY = os.getenv("API_KEY")
84
+
85
+ # ❌ CRITICAL: MCP config files with API keys
86
+ # NEVER commit these files:
87
+ # - .mcp-vector-search/config.json (OpenRouter API keys)
88
+ # - .mcp/config.json (MCP server credentials)
89
+ # - openrouter.json, anthropic-config.json
90
+ # - credentials.json, secrets.json, api-keys.json
91
+
92
+ # ✅ Safe: Verify .gitignore before committing
93
+ # Check file is ignored: git check-ignore <file_path>
94
+ # Check file not tracked: git ls-files <file_path>
95
+ ```
96
+
97
+ **MCP Secret File Patterns (High Risk):**
98
+ ```bash
99
+ # Files that commonly contain API keys:
100
+ .mcp-vector-search/config.json # OpenRouter, OpenAI keys
101
+ .mcp/config.json # MCP server credentials
102
+ **/mcp-config.json
103
+ openrouter.json
104
+ anthropic-config.json
105
+ openai-config.json
106
+ credentials.json
107
+ secrets.json
108
+ api-keys.json
109
+
110
+ # ALWAYS add to .gitignore:
111
+ echo ".mcp-vector-search/" >> .gitignore
112
+ echo "credentials.json" >> .gitignore
113
+ echo "secrets.json" >> .gitignore
84
114
  ```
85
115
 
86
116
  ### 4. XML External Entities (XXE)
@@ -178,6 +208,88 @@ if failed_login_count > 5:
178
208
  alert_security_team()
179
209
  ```
180
210
 
211
+ ## Secret Detection and Prevention
212
+
213
+ ### Pre-commit Hooks with detect-secrets
214
+ ```bash
215
+ # Install detect-secrets
216
+ pip install detect-secrets
217
+
218
+ # Create baseline of existing secrets
219
+ detect-secrets scan > .secrets.baseline
220
+
221
+ # Install pre-commit hooks
222
+ pip install pre-commit
223
+ pre-commit install
224
+
225
+ # Add to .pre-commit-config.yaml:
226
+ # - repo: https://github.com/Yelp/detect-secrets
227
+ # rev: v1.5.0
228
+ # hooks:
229
+ # - id: detect-secrets
230
+ # args: ['--baseline', '.secrets.baseline']
231
+
232
+ # Scan for new secrets
233
+ detect-secrets scan --baseline .secrets.baseline
234
+
235
+ # Audit baseline (mark false positives)
236
+ detect-secrets audit .secrets.baseline
237
+ ```
238
+
239
+ ### Manual Secret Scanning
240
+ ```bash
241
+ # Check if file is ignored by git
242
+ git check-ignore .mcp-vector-search/config.json
243
+ # Exit code 0 = ignored (safe)
244
+ # Exit code 1 = NOT ignored (DANGER!)
245
+
246
+ # Check if file is tracked by git
247
+ git ls-files .mcp-vector-search/config.json
248
+ # Output present = tracked (CRITICAL - remove immediately!)
249
+ # No output = not tracked (safe if also in .gitignore)
250
+
251
+ # Search git history for committed secrets
252
+ git log --all --full-history -- .mcp-vector-search/config.json
253
+
254
+ # Remove file from git history (if accidentally committed)
255
+ git filter-branch --force --index-filter \
256
+ 'git rm --cached --ignore-unmatch .mcp-vector-search/config.json' \
257
+ --prune-empty --tag-name-filter cat -- --all
258
+ ```
259
+
260
+ ### Incident Response: Exposed API Key
261
+ If you've committed an API key to git:
262
+
263
+ 1. **IMMEDIATELY rotate the exposed credential**
264
+ - OpenRouter: https://openrouter.ai/settings/keys
265
+ - Anthropic: https://console.anthropic.com/settings/keys
266
+ - OpenAI: https://platform.openai.com/api-keys
267
+
268
+ 2. **Remove from git history**
269
+ ```bash
270
+ # Using git-filter-repo (recommended)
271
+ git filter-repo --path .mcp-vector-search/config.json --invert-paths
272
+
273
+ # Force push to remote (WARNING: destructive)
274
+ git push origin --force --all
275
+ git push origin --force --tags
276
+ ```
277
+
278
+ 3. **Add to .gitignore** (if not already there)
279
+ ```bash
280
+ echo ".mcp-vector-search/" >> .gitignore
281
+ git add .gitignore
282
+ git commit -m "chore: add MCP config to gitignore"
283
+ ```
284
+
285
+ 4. **Verify cleanup**
286
+ ```bash
287
+ git log --all --full-history -- .mcp-vector-search/config.json
288
+ # Should show no results
289
+ ```
290
+
291
+ 5. **Notify stakeholders** if the key had production access
292
+
181
293
  ## Security Scanning Tools
182
294
 
183
295
  ### Python