claude-mpm 5.4.91__py3-none-any.whl → 5.4.95__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 (100) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/monitor.py +2 -2
  3. claude_mpm/cli/startup_logging.py +2 -2
  4. claude_mpm/commands/mpm-session-resume.md +1 -1
  5. claude_mpm/core/unified_config.py +1 -1
  6. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  7. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  8. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BS0ej2w8.js → 1WZnGYqX.js} +1 -1
  9. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{7ZAeO_Uj.js → 67pF3qNn.js} +1 -1
  10. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BCWDw8BF.js → 6RxdMKe4.js} +1 -1
  11. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BVFqgd56.js → 8cZrfX0h.js} +1 -1
  12. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDNOxKrg.js → 9a6T2nm-.js} +1 -1
  13. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{MJf6AOIJ.js → B443AUzu.js} +1 -1
  14. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B8AwtY2H.js +1 -0
  15. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DNI1jw9S.js → BF15LAsF.js} +1 -1
  16. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DOeJfApz.js → BRcwIQNr.js} +1 -1
  17. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BfXd4Xj4.js → BV6nKitt.js} +1 -1
  18. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DWDi9IaK.js → BViJ8lZt.js} +1 -1
  19. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BnFPFynJ.js → BcQ-Q0FE.js} +1 -1
  20. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B_fnSNFx.js → Bpyvgze_.js} +1 -1
  21. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  22. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  23. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{EKp_wsKE.js → C3rbW_a-.js} +1 -1
  24. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_l0vq62.js → C8WYN38h.js} +1 -1
  25. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDi5wzaD.js → C9I8FlXH.js} +1 -1
  26. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BwpSELyW.js → CIQcWgO2.js} +1 -1
  27. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CQ94FMOU.js → CIctN7YN.js} +1 -1
  28. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzkNB1Vu.js → CKrS_JZW.js} +1 -1
  29. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D0Fj1OdD.js → CR6P9C4A.js} +1 -1
  30. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{NsEh4Ivo.js → CRRR9MD_.js} +1 -1
  31. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  32. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DdIDcQsD.js → CSXtMOf0.js} +1 -1
  33. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C5Dg_JxJ.js → CT-sbxSk.js} +1 -1
  34. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyrTH56Q.js → CWm6DJsp.js} +1 -1
  35. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C86quetY.js → CpqQ1Kzn.js} +1 -1
  36. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dqtg3hb8.js → D9iCMida.js} +1 -1
  37. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DJN4AVXS.js → D9ykgMoY.js} +1 -1
  38. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{14Ru8gxt.js → DL2Ldur1.js} +1 -1
  39. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLeM8wSV.js → DPfltzjH.js} +1 -1
  40. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
  41. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CvWciI1W.js → DUliQN2b.js} +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DOViuQX_.js → DXlhR01x.js} +1 -1
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{s04HIjWg.js → D_lyTybS.js} +1 -1
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{WiqB4NUY.js → DngoTTgh.js} +1 -1
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CT5eAo1x.js → DqkmHtDC.js} +1 -1
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DJuK4-OP.js → DsDh8EYs.js} +1 -1
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C7i47te_.js → DypDmXgd.js} +1 -1
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BfMC7wDI.js → IPYC-LnN.js} +1 -1
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{URSAF6IJ.js → JTLiF7dt.js} +1 -1
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B06ALsCS.js → JpevfAFt.js} +1 -1
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D1ARDjz0.js → Zxy7qc-l.js} +1 -1
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{vJiSSdpk.js → qtd3IeO4.js} +1 -1
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BXs4CVzO.js → ulBFON_C.js} +1 -1
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DTgfNBV9.js → wQVh1CoA.js} +1 -1
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.CnXU_fEX.js → app.Dr7t0z2J.js} +2 -2
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.BuxSUm_s.js → 0.RgBboRvH.js} +1 -1
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.znyTz9u3.js → 1.DG-KkbDf.js} +1 -1
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  63. claude_mpm/dashboard/static/svelte-build/index.html +8 -8
  64. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/event_handlers.py +46 -10
  69. claude_mpm/hooks/claude_hooks/hook_handler.py +3 -1
  70. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -16
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
  73. claude_mpm/services/agents/cache_git_manager.py +1 -1
  74. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
  75. claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
  76. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  77. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  78. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  79. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  80. claude_mpm/services/diagnostics/models.py +14 -1
  81. claude_mpm/services/monitor/daemon_manager.py +15 -4
  82. claude_mpm/services/monitor/management/lifecycle.py +8 -2
  83. claude_mpm/services/monitor/server.py +106 -16
  84. claude_mpm/services/skills/selective_skill_deployer.py +114 -16
  85. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/METADATA +1 -1
  86. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/RECORD +91 -89
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B7S5qgOx.css +0 -1
  88. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.D3t4z6uz.css +0 -1
  89. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
  90. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/AeivYILh.js +0 -1
  91. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cn4nXAfg.js +0 -1
  92. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D_vpdI7l.js +0 -325
  93. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DnL7ky1O.js +0 -1
  94. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.CUaAfoQJ.js +0 -1
  95. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.CLVHDDxl.js +0 -1
  96. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/WHEEL +0 -0
  97. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/entry_points.txt +0 -0
  98. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/licenses/LICENSE +0 -0
  99. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  100. {claude_mpm-5.4.91.dist-info → claude_mpm-5.4.95.dist-info}/top_level.txt +0 -0
@@ -368,6 +368,83 @@ class UnifiedMonitorServer:
368
368
  finally:
369
369
  await self._cleanup_async()
370
370
 
371
+ def _categorize_event(self, event_name: str) -> str:
372
+ """Categorize event by name to determine Socket.IO event type.
373
+
374
+ Maps specific event names to their category for frontend filtering.
375
+
376
+ Args:
377
+ event_name: The raw event name (e.g., "subagent_start", "todo_updated")
378
+
379
+ Returns:
380
+ Category name (e.g., "hook_event", "system_event")
381
+ """
382
+ # Hook events - agent lifecycle and todo updates
383
+ if event_name in ("subagent_start", "subagent_stop", "todo_updated"):
384
+ return "hook_event"
385
+
386
+ # Tool events - both hook-style and direct tool events
387
+ if event_name in (
388
+ "pre_tool",
389
+ "post_tool",
390
+ "tool.start",
391
+ "tool.end",
392
+ "tool_use",
393
+ "tool_result",
394
+ ):
395
+ return "tool_event"
396
+
397
+ # Session events - session lifecycle
398
+ if event_name in (
399
+ "session.started",
400
+ "session.ended",
401
+ "session_start",
402
+ "session_end",
403
+ ):
404
+ return "session_event"
405
+
406
+ # Response events - API response lifecycle
407
+ if event_name in (
408
+ "response.start",
409
+ "response.end",
410
+ "response_started",
411
+ "response_ended",
412
+ ):
413
+ return "response_event"
414
+
415
+ # Agent events - agent delegation and returns
416
+ if event_name in (
417
+ "agent.delegated",
418
+ "agent.returned",
419
+ "agent_start",
420
+ "agent_end",
421
+ ):
422
+ return "agent_event"
423
+
424
+ # File events - file operations
425
+ if event_name in (
426
+ "file.read",
427
+ "file.write",
428
+ "file.edit",
429
+ "file_read",
430
+ "file_write",
431
+ ):
432
+ return "file_event"
433
+
434
+ # Claude API events
435
+ if event_name in ("user_prompt", "assistant_message"):
436
+ return "claude_event"
437
+
438
+ # System events
439
+ if event_name in ("system_ready", "system_shutdown"):
440
+ return "system_event"
441
+
442
+ # Log uncategorized events for debugging
443
+ self.logger.debug(f"Uncategorized event: {event_name}")
444
+
445
+ # Default to claude_event for unknown events
446
+ return "claude_event"
447
+
371
448
  def _setup_event_handlers(self):
372
449
  """Setup Socket.IO event handlers."""
373
450
  try:
@@ -474,7 +551,7 @@ class UnifiedMonitorServer:
474
551
  )
475
552
  if version_file.exists():
476
553
  version = version_file.read_text().strip()
477
- except Exception:
554
+ except Exception: # nosec B110
478
555
  pass
479
556
 
480
557
  return web.json_response(
@@ -499,10 +576,23 @@ class UnifiedMonitorServer:
499
576
  event = data.get("event", "claude_event")
500
577
  event_data = data.get("data", {})
501
578
 
502
- # Emit to Socket.IO clients via the appropriate event
579
+ # Categorize event and wrap in expected format
580
+ event_type = self._categorize_event(event)
581
+ wrapped_event = {
582
+ "type": event_type,
583
+ "subtype": event,
584
+ "data": event_data,
585
+ "timestamp": event_data.get("timestamp")
586
+ or datetime.now(timezone.utc).isoformat() + "Z",
587
+ "session_id": event_data.get("session_id"),
588
+ }
589
+
590
+ # Emit to Socket.IO clients via the categorized event type
503
591
  if self.sio:
504
- await self.sio.emit(event, event_data)
505
- self.logger.debug(f"HTTP event forwarded to Socket.IO: {event}")
592
+ await self.sio.emit(event_type, wrapped_event)
593
+ self.logger.debug(
594
+ f"HTTP event forwarded to Socket.IO: {event} -> {event_type}"
595
+ )
506
596
 
507
597
  return web.Response(status=204) # No content response
508
598
 
@@ -859,7 +949,7 @@ class UnifiedMonitorServer:
859
949
  # Configuration endpoint for dashboard initialization
860
950
  async def config_handler(request):
861
951
  """Return configuration for dashboard initialization."""
862
- import subprocess
952
+ import subprocess # nosec B404
863
953
 
864
954
  config = {
865
955
  "workingDirectory": Path.cwd(),
@@ -870,7 +960,7 @@ class UnifiedMonitorServer:
870
960
 
871
961
  # Try to get current git branch
872
962
  try:
873
- result = subprocess.run(
963
+ result = subprocess.run( # nosec B603 B607
874
964
  ["git", "branch", "--show-current"],
875
965
  capture_output=True,
876
966
  text=True,
@@ -880,7 +970,7 @@ class UnifiedMonitorServer:
880
970
  )
881
971
  if result.returncode == 0 and result.stdout.strip():
882
972
  config["gitBranch"] = result.stdout.strip()
883
- except Exception:
973
+ except Exception: # nosec B110
884
974
  pass # Keep default "Unknown" value
885
975
 
886
976
  return web.json_response(config)
@@ -910,7 +1000,7 @@ class UnifiedMonitorServer:
910
1000
  # Git history handler
911
1001
  async def git_history_handler(request: web.Request) -> web.Response:
912
1002
  """Get git history for a file."""
913
- import subprocess
1003
+ import subprocess # nosec B404
914
1004
 
915
1005
  try:
916
1006
  data = await request.json()
@@ -939,7 +1029,7 @@ class UnifiedMonitorServer:
939
1029
  )
940
1030
 
941
1031
  # Get git log for file
942
- result = subprocess.run(
1032
+ result = subprocess.run( # nosec B603 B607
943
1033
  [
944
1034
  "git",
945
1035
  "log",
@@ -978,7 +1068,7 @@ class UnifiedMonitorServer:
978
1068
  # Git diff handler
979
1069
  async def git_diff_handler(request: web.Request) -> web.Response:
980
1070
  """Get git diff for a file with optional commit selection."""
981
- import subprocess
1071
+ import subprocess # nosec B404
982
1072
 
983
1073
  try:
984
1074
  file_path = request.query.get("path", "")
@@ -1010,7 +1100,7 @@ class UnifiedMonitorServer:
1010
1100
  )
1011
1101
 
1012
1102
  # Find git repository root
1013
- git_root_result = subprocess.run(
1103
+ git_root_result = subprocess.run( # nosec B603 B607
1014
1104
  ["git", "rev-parse", "--show-toplevel"],
1015
1105
  check=False,
1016
1106
  capture_output=True,
@@ -1034,7 +1124,7 @@ class UnifiedMonitorServer:
1034
1124
  git_root = Path(git_root_result.stdout.strip())
1035
1125
 
1036
1126
  # Check if file is tracked by git
1037
- ls_files_result = subprocess.run(
1127
+ ls_files_result = subprocess.run( # nosec B603 B607
1038
1128
  ["git", "ls-files", "--error-unmatch", str(path)],
1039
1129
  check=False,
1040
1130
  capture_output=True,
@@ -1056,7 +1146,7 @@ class UnifiedMonitorServer:
1056
1146
  )
1057
1147
 
1058
1148
  # Get commit history for this file (last 5 commits)
1059
- history_result = subprocess.run(
1149
+ history_result = subprocess.run( # nosec B603 B607
1060
1150
  [
1061
1151
  "git",
1062
1152
  "log",
@@ -1087,7 +1177,7 @@ class UnifiedMonitorServer:
1087
1177
  )
1088
1178
 
1089
1179
  # Check for uncommitted changes
1090
- uncommitted_result = subprocess.run(
1180
+ uncommitted_result = subprocess.run( # nosec B603 B607
1091
1181
  ["git", "diff", "HEAD", str(path)],
1092
1182
  check=False,
1093
1183
  capture_output=True,
@@ -1100,7 +1190,7 @@ class UnifiedMonitorServer:
1100
1190
  # Get diff based on commit parameter
1101
1191
  if commit_hash:
1102
1192
  # Get diff for specific commit
1103
- result = subprocess.run(
1193
+ result = subprocess.run( # nosec B603 B607
1104
1194
  ["git", "show", commit_hash, "--", str(path)],
1105
1195
  check=False,
1106
1196
  capture_output=True,
@@ -1469,7 +1559,7 @@ class UnifiedMonitorServer:
1469
1559
  gather = asyncio.gather(*tasks_to_cancel, return_exceptions=True)
1470
1560
  try:
1471
1561
  loop.run_until_complete(gather)
1472
- except Exception:
1562
+ except Exception: # nosec B110
1473
1563
  # Some tasks might fail to cancel, that's ok
1474
1564
  pass
1475
1565
 
@@ -214,18 +214,90 @@ def get_skills_from_mapping(agent_ids: List[str]) -> Set[str]:
214
214
  return set()
215
215
 
216
216
 
217
+ def extract_skills_from_content(agent_file: Path) -> Set[str]:
218
+ """Extract skill names from [SKILL: skill-name] markers in agent file content.
219
+
220
+ This function complements frontmatter skill extraction by finding inline
221
+ skill references in the agent's markdown content body.
222
+
223
+ Supports multiple formats:
224
+ - Bold marker: **[SKILL: skill-name]**
225
+ - Plain marker: [SKILL: skill-name]
226
+ - Backtick list: - `skill-name` - Description
227
+ - With spaces: [SKILL: skill-name ]
228
+
229
+ Args:
230
+ agent_file: Path to agent markdown file
231
+
232
+ Returns:
233
+ Set of skill names found in content body
234
+
235
+ Example:
236
+ >>> skills = extract_skills_from_content(Path("pm.md"))
237
+ >>> # Finds skills from markers like **[SKILL: mpm-delegation-patterns]**
238
+ >>> # Also finds from lists like - `mpm-teaching-mode` - Description
239
+ >>> print(f"Found {len(skills)} skills in content")
240
+ """
241
+ try:
242
+ content = agent_file.read_text(encoding="utf-8")
243
+ except Exception as e:
244
+ logger.warning(f"Failed to read {agent_file}: {e}")
245
+ return set()
246
+
247
+ skills = set()
248
+
249
+ # Pattern 1: [SKILL: skill-name] markers (with optional markdown bold)
250
+ # Handles: **[SKILL: skill-name]** or [SKILL: skill-name]
251
+ # Pattern breakdown:
252
+ # - \*{0,2}: Optional bold markdown (0-2 asterisks)
253
+ # - \[SKILL:\s*: Opening bracket with optional whitespace
254
+ # - ([a-zA-Z0-9_-]+): Skill name (capture group)
255
+ # - \s*\]: Closing bracket with optional whitespace
256
+ # - \*{0,2}: Optional closing bold markdown
257
+ pattern1 = r"\*{0,2}\[SKILL:\s*([a-zA-Z0-9_-]+)\s*\]\*{0,2}"
258
+ matches1 = re.findall(pattern1, content, re.IGNORECASE)
259
+ skills.update(matches1)
260
+
261
+ # Pattern 2: Backtick list items with mpm-* or toolchains-* skills
262
+ # Handles: - `mpm-skill-name` - Description
263
+ # Pattern breakdown:
264
+ # - ^-\s+: Start with dash and whitespace (list item)
265
+ # - `: Opening backtick
266
+ # - ((?:mpm-|toolchains-|universal-)[a-zA-Z0-9_-]+): Skill name starting with prefix
267
+ # - `: Closing backtick
268
+ # - \s+-: Followed by whitespace and dash (description separator)
269
+ pattern2 = r"^-\s+`((?:mpm-|toolchains-|universal-)[a-zA-Z0-9_-]+)`\s+-"
270
+ matches2 = re.findall(pattern2, content, re.MULTILINE | re.IGNORECASE)
271
+ skills.update(matches2)
272
+
273
+ if skills:
274
+ logger.debug(
275
+ f"Found {len(skills)} skills from content markers in {agent_file.name}"
276
+ )
277
+
278
+ return skills
279
+
280
+
217
281
  def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
218
282
  """Extract all skills referenced by deployed agents.
219
283
 
220
- MAJOR CHANGE (Phase 3): Now ONLY uses frontmatter-declared skills.
284
+ MAJOR CHANGE (Phase 3): Now uses TWO sources for skill discovery:
285
+ 1. Frontmatter-declared skills (skills: field)
286
+ 2. Content body markers ([SKILL: skill-name])
287
+
221
288
  The static skill_to_agent_mapping.yaml is DEPRECATED. Each agent must
222
- declare its skills in frontmatter or it gets zero skills deployed.
289
+ declare its skills via frontmatter OR inline markers.
223
290
 
224
291
  This change:
225
292
  - Eliminates dual-source complexity (frontmatter + mapping)
226
293
  - Makes skill requirements explicit per agent
227
- - Enables per-agent customization via frontmatter
294
+ - Enables per-agent customization via frontmatter or inline markers
228
295
  - Removes dependency on static YAML mapping
296
+ - Fixes PM skills being removed as orphaned (they use inline markers)
297
+
298
+ Special handling for PM_INSTRUCTIONS.md:
299
+ - Also scans .claude-mpm/PM_INSTRUCTIONS.md for skill markers
300
+ - PM instructions are not in agents_dir but contain [SKILL: ...] references
229
301
 
230
302
  Args:
231
303
  agents_dir: Path to deployed agents directory (e.g., .claude/agents/)
@@ -244,37 +316,63 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
244
316
 
245
317
  # Scan all agent markdown files
246
318
  agent_files = list(agents_dir.glob("*.md"))
247
- logger.debug(f"Scanning {len(agent_files)} agent files in {agents_dir}")
248
319
 
249
- # ONLY use frontmatter skills - no more mapping inference
320
+ # Special case: Add PM_INSTRUCTIONS.md if it exists
321
+ # PM instructions live in .claude-mpm/ not .claude/agents/
322
+ pm_instructions = agents_dir.parent.parent / ".claude-mpm" / "PM_INSTRUCTIONS.md"
323
+ if pm_instructions.exists():
324
+ agent_files.append(pm_instructions)
325
+ logger.debug("Added PM_INSTRUCTIONS.md for skill scanning")
326
+
327
+ logger.debug(f"Scanning {len(agent_files)} agent files (including PM instructions)")
328
+
329
+ # Use TWO sources: frontmatter AND content markers
250
330
  frontmatter_skills = set()
331
+ content_skills = set()
251
332
 
252
333
  for agent_file in agent_files:
253
334
  agent_id = agent_file.stem
254
335
 
336
+ # Source 1: Extract from frontmatter
255
337
  frontmatter = parse_agent_frontmatter(agent_file)
256
- agent_skills = get_skills_from_agent(frontmatter)
338
+ agent_fm_skills = get_skills_from_agent(frontmatter)
257
339
 
258
- if agent_skills:
259
- frontmatter_skills.update(agent_skills)
340
+ if agent_fm_skills:
341
+ frontmatter_skills.update(agent_fm_skills)
260
342
  logger.debug(
261
- f"Agent {agent_id}: {len(agent_skills)} skills from frontmatter"
343
+ f"Agent {agent_id}: {len(agent_fm_skills)} skills from frontmatter"
262
344
  )
263
- else:
264
- logger.debug(f"Agent {agent_id}: No skills declared in frontmatter")
345
+
346
+ # Source 2: Extract from content body [SKILL: ...] markers
347
+ agent_content_skills = extract_skills_from_content(agent_file)
348
+
349
+ if agent_content_skills:
350
+ content_skills.update(agent_content_skills)
351
+ logger.debug(
352
+ f"Agent {agent_id}: {len(agent_content_skills)} skills from content markers"
353
+ )
354
+
355
+ if not agent_fm_skills and not agent_content_skills:
356
+ logger.debug(
357
+ f"Agent {agent_id}: No skills declared (checked frontmatter + content)"
358
+ )
359
+
360
+ # Combine both sources
361
+ all_skills = frontmatter_skills | content_skills
265
362
 
266
363
  logger.info(
267
- f"Found {len(frontmatter_skills)} unique skills from agent frontmatter "
268
- f"(static mapping no longer used)"
364
+ f"Found {len(all_skills)} unique skills "
365
+ f"({len(frontmatter_skills)} from frontmatter, "
366
+ f"{len(content_skills)} from content markers)"
269
367
  )
270
368
 
271
369
  # Normalize skill paths: convert slashes to dashes for compatibility with deployment
272
370
  # Some skills may use slash format, normalize to dashes
273
- normalized_skills = {skill.replace("/", "-") for skill in frontmatter_skills}
371
+ normalized_skills = {skill.replace("/", "-") for skill in all_skills}
274
372
 
275
- if normalized_skills != frontmatter_skills:
373
+ if normalized_skills != all_skills:
276
374
  logger.debug(
277
- f"Normalized {len(frontmatter_skills)} skills to {len(normalized_skills)} "
375
+ f"Normalized {len(all_skills)} skills to {len(normalized_skills)} "
278
376
  "(converted slashes to dashes)"
279
377
  )
280
378
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 5.4.91
3
+ Version: 5.4.95
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team