claude-mpm 5.4.70__py3-none-any.whl → 5.4.73__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.
@@ -0,0 +1,481 @@
1
+ """Interactive Skill Selector for Claude MPM.
2
+
3
+ This module provides a two-tier interactive skill selection wizard:
4
+ 1. Select topic groups (toolchains) to explore
5
+ 2. Multi-select skills within each topic group
6
+
7
+ Features:
8
+ - Groups skills by toolchain (universal, python, typescript, etc.)
9
+ - Shows skills auto-included by agent dependencies
10
+ - Displays token counts for each skill
11
+ - Uses questionary with cyan style for consistency
12
+ - Matches agent selector UI pattern with table display
13
+ """
14
+
15
+ import json
16
+ import shutil
17
+ from dataclasses import dataclass
18
+ from typing import Dict, List, Optional, Set
19
+
20
+ import questionary
21
+
22
+ from claude_mpm.cli.interactive.questionary_styles import (
23
+ BANNER_WIDTH,
24
+ MPM_STYLE,
25
+ print_banner,
26
+ )
27
+ from claude_mpm.core.logging_utils import get_logger
28
+ from claude_mpm.core.unified_config import UnifiedConfig
29
+ from claude_mpm.core.unified_paths import get_path_manager
30
+
31
+ logger = get_logger(__name__)
32
+
33
+ # Topic/toolchain icons
34
+ TOPIC_ICONS = {
35
+ "universal": "🌐",
36
+ "python": "🐍",
37
+ "typescript": "📘",
38
+ "javascript": "📒",
39
+ "rust": "⚙️",
40
+ "go": "🔷",
41
+ "java": "☕",
42
+ "ruby": "💎",
43
+ "php": "🐘",
44
+ "csharp": "🔷",
45
+ "cpp": "⚙️",
46
+ "swift": "🍎",
47
+ None: "🌐", # Default for null toolchain (universal)
48
+ }
49
+
50
+
51
+ @dataclass
52
+ class SkillInfo:
53
+ """Information about a skill from manifest."""
54
+
55
+ name: str
56
+ toolchain: Optional[str]
57
+ framework: Optional[str]
58
+ tags: List[str]
59
+ full_tokens: int
60
+ description: str = ""
61
+
62
+ @property
63
+ def display_name(self) -> str:
64
+ """Get display name with token count."""
65
+ tokens_k = self.full_tokens // 1000
66
+ return f"{self.name} ({tokens_k}K tokens)"
67
+
68
+
69
+ class SkillSelector:
70
+ """Interactive skill selector with topic grouping."""
71
+
72
+ def __init__(
73
+ self,
74
+ skills_manifest: Dict,
75
+ agent_skill_deps: Optional[List[str]] = None,
76
+ deployed_skills: Optional[Set[str]] = None,
77
+ ):
78
+ """Initialize skill selector.
79
+
80
+ Args:
81
+ skills_manifest: Full manifest dict with all skills
82
+ agent_skill_deps: Skills required by deployed agents (auto-included)
83
+ deployed_skills: Skills currently deployed in .claude/skills/
84
+ """
85
+ self.manifest = skills_manifest
86
+ self.agent_skill_deps = set(agent_skill_deps or [])
87
+ self.deployed_skills = deployed_skills or set()
88
+ self.skills_by_toolchain: Dict[str, List[SkillInfo]] = {}
89
+ self._parse_manifest()
90
+
91
+ def _parse_manifest(self) -> None:
92
+ """Parse manifest and group skills by toolchain."""
93
+ # Handle both old and new manifest formats
94
+ skills_data = self.manifest.get("skills", {})
95
+
96
+ # Flatten skills from grouped format to flat list
97
+ all_skills = []
98
+ if isinstance(skills_data, dict):
99
+ for toolchain, skills_list in skills_data.items():
100
+ all_skills.extend(skills_list)
101
+ elif isinstance(skills_data, list):
102
+ all_skills = skills_data
103
+
104
+ # Group by toolchain
105
+ for skill_data in all_skills:
106
+ try:
107
+ skill = SkillInfo(
108
+ name=skill_data.get("name", ""),
109
+ toolchain=skill_data.get("toolchain"),
110
+ framework=skill_data.get("framework"),
111
+ tags=skill_data.get("tags", []),
112
+ full_tokens=skill_data.get("full_tokens", 0),
113
+ description=skill_data.get("description", ""),
114
+ )
115
+
116
+ # Group by toolchain (null -> universal)
117
+ toolchain_key = skill.toolchain or "universal"
118
+
119
+ if toolchain_key not in self.skills_by_toolchain:
120
+ self.skills_by_toolchain[toolchain_key] = []
121
+
122
+ self.skills_by_toolchain[toolchain_key].append(skill)
123
+ except Exception as e:
124
+ logger.warning(
125
+ f"Failed to parse skill: {skill_data.get('name', 'unknown')}: {e}"
126
+ )
127
+
128
+ # Sort skills within each toolchain by name
129
+ for toolchain in self.skills_by_toolchain:
130
+ self.skills_by_toolchain[toolchain].sort(key=lambda s: s.name)
131
+
132
+ @staticmethod
133
+ def _calculate_column_widths(
134
+ terminal_width: int, columns: Dict[str, int]
135
+ ) -> Dict[str, int]:
136
+ """Calculate dynamic column widths based on terminal size.
137
+
138
+ Args:
139
+ terminal_width: Current terminal width in characters
140
+ columns: Dict mapping column names to minimum widths
141
+
142
+ Returns:
143
+ Dict mapping column names to calculated widths
144
+
145
+ Design:
146
+ - Ensures minimum widths are respected
147
+ - Distributes extra space proportionally
148
+ - Handles narrow terminals gracefully (minimum 80 chars)
149
+ """
150
+ # Ensure minimum terminal width
151
+ min_terminal_width = 80
152
+ terminal_width = max(terminal_width, min_terminal_width)
153
+
154
+ # Calculate total minimum width needed
155
+ total_min_width = sum(columns.values())
156
+
157
+ # Account for spacing between columns
158
+ overhead = len(columns) + 1
159
+ available_width = terminal_width - overhead
160
+
161
+ # If we have extra space, distribute proportionally
162
+ if available_width > total_min_width:
163
+ extra_space = available_width - total_min_width
164
+ total_weight = sum(columns.values())
165
+
166
+ result = {}
167
+ for col_name, min_width in columns.items():
168
+ # Distribute extra space based on minimum width proportion
169
+ proportion = min_width / total_weight
170
+ extra = int(extra_space * proportion)
171
+ result[col_name] = min_width + extra
172
+ return result
173
+ # Terminal too narrow, use minimum widths
174
+ return columns.copy()
175
+
176
+ def _display_skills_table(self, skills: List[SkillInfo]) -> None:
177
+ """Display skills in a table with status (matches agent selector pattern).
178
+
179
+ Args:
180
+ skills: List of skills to display
181
+ """
182
+ if not skills:
183
+ print("\n📭 No skills found.")
184
+ return
185
+
186
+ # Calculate dynamic column widths based on terminal size
187
+ terminal_width = shutil.get_terminal_size().columns
188
+ min_widths = {
189
+ "#": 4,
190
+ "Skill ID": 30,
191
+ "Description": 35,
192
+ "Toolchain": 12,
193
+ "Status": 12,
194
+ }
195
+ widths = self._calculate_column_widths(terminal_width, min_widths)
196
+
197
+ # Print header with dynamic widths
198
+ print(
199
+ f"\n{'#':<{widths['#']}} "
200
+ f"{'Skill ID':<{widths['Skill ID']}} "
201
+ f"{'Description':<{widths['Description']}} "
202
+ f"{'Toolchain':<{widths['Toolchain']}} "
203
+ f"{'Status':<{widths['Status']}}"
204
+ )
205
+ separator_width = sum(widths.values()) + len(widths) - 1
206
+ print("-" * separator_width)
207
+
208
+ for i, skill in enumerate(skills, 1):
209
+ # Truncate to fit dynamic width
210
+ skill_id = skill.name
211
+ if len(skill_id) > widths["Skill ID"]:
212
+ skill_id = skill_id[: widths["Skill ID"] - 1] + "…"
213
+
214
+ description = skill.description or skill.name
215
+ if len(description) > widths["Description"]:
216
+ description = description[: widths["Description"] - 1] + "…"
217
+
218
+ toolchain = skill.toolchain or "universal"
219
+ if len(toolchain) > widths["Toolchain"]:
220
+ toolchain = toolchain[: widths["Toolchain"] - 1] + "…"
221
+
222
+ # Determine status
223
+ if skill.name in self.agent_skill_deps:
224
+ status = "✓ Required"
225
+ elif skill.name in self.deployed_skills:
226
+ status = "✓ Installed"
227
+ else:
228
+ status = "Available"
229
+
230
+ print(
231
+ f"{i:<{widths['#']}} "
232
+ f"{skill_id:<{widths['Skill ID']}} "
233
+ f"{description:<{widths['Description']}} "
234
+ f"{toolchain:<{widths['Toolchain']}} "
235
+ f"{status:<{widths['Status']}}"
236
+ )
237
+
238
+ def select_skills(self) -> List[str]:
239
+ """Run interactive selection and return selected skill IDs.
240
+
241
+ Returns:
242
+ List of selected skill IDs (names)
243
+ """
244
+ print_banner("SKILL CONFIGURATION", width=BANNER_WIDTH)
245
+
246
+ # Show agent-required skills (auto-included)
247
+ if self.agent_skill_deps:
248
+ self._show_agent_required_skills()
249
+
250
+ # Get all skills for table display
251
+ all_skills = []
252
+ for toolchain_skills in self.skills_by_toolchain.values():
253
+ all_skills.extend(toolchain_skills)
254
+ all_skills.sort(key=lambda s: s.name)
255
+
256
+ # Display skills table
257
+ print(f"\n📋 Found {len(all_skills)} skill(s) available:")
258
+ self._display_skills_table(all_skills)
259
+
260
+ # Select topic groups to explore
261
+ selected_groups = self._select_topic_groups()
262
+
263
+ if not selected_groups:
264
+ print("\n⚠️ No topic groups selected. Using only agent-required skills.")
265
+ return list(self.agent_skill_deps)
266
+
267
+ # Multi-select skills from each group
268
+ selected_skills = set(self.agent_skill_deps) # Start with auto-included
269
+
270
+ for group in selected_groups:
271
+ group_skills = self._select_skills_from_group(group)
272
+ selected_skills.update(group_skills)
273
+
274
+ # Confirm selection
275
+ print(f"\n✅ Total skills selected: {len(selected_skills)}")
276
+ print(f" - Auto-included (from agents): {len(self.agent_skill_deps)}")
277
+ print(f" - Manually selected: {len(selected_skills - self.agent_skill_deps)}")
278
+
279
+ return list(selected_skills)
280
+
281
+ def _show_agent_required_skills(self) -> None:
282
+ """Display skills that are auto-included from agent dependencies."""
283
+ print("\n📦 Agent-Required Skills (auto-included):")
284
+ for skill_name in sorted(self.agent_skill_deps):
285
+ print(f" ✓ {skill_name}")
286
+ print()
287
+
288
+ def _select_topic_groups(self) -> List[str]:
289
+ """First tier: Select which toolchain groups to browse.
290
+
291
+ Returns:
292
+ List of selected toolchain keys
293
+ """
294
+ # Build choices with counts
295
+ choices = []
296
+ for toolchain in sorted(self.skills_by_toolchain.keys()):
297
+ skills = self.skills_by_toolchain[toolchain]
298
+ icon = TOPIC_ICONS.get(toolchain, "📦")
299
+ display_name = toolchain.capitalize() if toolchain else "Universal"
300
+ choice_text = f"{icon} {display_name} ({len(skills)} skills)"
301
+ choices.append(questionary.Choice(title=choice_text, value=toolchain))
302
+
303
+ if not choices:
304
+ print("\n⚠️ No skills available in manifest.")
305
+ return []
306
+
307
+ # Multi-select groups
308
+ selected = questionary.checkbox(
309
+ "📂 Select Topic Groups to Add Skills From:",
310
+ choices=choices,
311
+ style=MPM_STYLE,
312
+ ).ask()
313
+
314
+ if selected is None: # User cancelled
315
+ return []
316
+
317
+ return selected
318
+
319
+ def _select_skills_from_group(self, toolchain: str) -> List[str]:
320
+ """Second tier: Multi-select skills within a toolchain group.
321
+
322
+ Args:
323
+ toolchain: Toolchain key to select from
324
+
325
+ Returns:
326
+ List of selected skill names
327
+ """
328
+ skills = self.skills_by_toolchain.get(toolchain, [])
329
+ if not skills:
330
+ return []
331
+
332
+ icon = TOPIC_ICONS.get(toolchain, "📦")
333
+ display_name = toolchain.capitalize() if toolchain else "Universal"
334
+
335
+ print(f"\n{icon} {display_name} Skills:")
336
+
337
+ # Build choices with numbered format like agent selector
338
+ choices = []
339
+ for i, skill in enumerate(skills, 1):
340
+ # Mark if already selected (from agent deps)
341
+ already_selected = skill.name in self.agent_skill_deps
342
+
343
+ # Format: "1. skill-name - toolchain (XK tokens)"
344
+ tokens_k = skill.full_tokens // 1000
345
+ desc = skill.description[:50] if skill.description else skill.name
346
+ choice_text = f"{i}. {skill.name} - {desc}... ({tokens_k}K tokens)"
347
+
348
+ choice = questionary.Choice(
349
+ title=choice_text,
350
+ value=skill.name,
351
+ checked=already_selected,
352
+ )
353
+ choices.append(choice)
354
+
355
+ # Multi-select skills
356
+ selected = questionary.checkbox(
357
+ f"Select {display_name} skills to include:",
358
+ choices=choices,
359
+ style=MPM_STYLE,
360
+ ).ask()
361
+
362
+ if selected is None: # User cancelled
363
+ return []
364
+
365
+ return selected
366
+
367
+
368
+ def load_skills_manifest() -> Optional[Dict]:
369
+ """Load skills manifest from cache.
370
+
371
+ Returns:
372
+ Manifest dict or None if not found
373
+ """
374
+ try:
375
+ path_manager = get_path_manager()
376
+ manifest_path = (
377
+ path_manager.get_cache_dir() / "skills" / "system" / "manifest.json"
378
+ )
379
+
380
+ if not manifest_path.exists():
381
+ logger.error(f"Skills manifest not found at {manifest_path}")
382
+ print("\n❌ Skills manifest not found. Run 'claude-mpm skills sync' first.")
383
+ return None
384
+
385
+ with open(manifest_path, encoding="utf-8") as f:
386
+ return json.load(f)
387
+
388
+ except Exception as e:
389
+ logger.error(f"Failed to load skills manifest: {e}")
390
+ print(f"\n❌ Failed to load skills manifest: {e}")
391
+ return None
392
+
393
+
394
+ def get_agent_skill_dependencies(config: UnifiedConfig) -> List[str]:
395
+ """Get skill dependencies from deployed agents.
396
+
397
+ Args:
398
+ config: UnifiedConfig instance
399
+
400
+ Returns:
401
+ List of skill IDs required by enabled agents
402
+ """
403
+ try:
404
+ from claude_mpm.services.agents.deployment.deployment_reconciler import (
405
+ DeploymentReconciler,
406
+ )
407
+
408
+ reconciler = DeploymentReconciler(config)
409
+ enabled_agents = config.agents.enabled
410
+
411
+ if not enabled_agents:
412
+ logger.debug("No enabled agents, no skill dependencies")
413
+ return []
414
+
415
+ # Get skill dependencies
416
+ skill_deps = reconciler._get_agent_skill_dependencies(enabled_agents)
417
+ return list(skill_deps)
418
+
419
+ except Exception as e:
420
+ logger.warning(f"Failed to get agent skill dependencies: {e}")
421
+ return []
422
+
423
+
424
+ def get_deployed_skills() -> Set[str]:
425
+ """Get skills currently deployed in .claude/skills/ directory.
426
+
427
+ Returns:
428
+ Set of deployed skill IDs
429
+ """
430
+ try:
431
+ from claude_mpm.services.agents.deployment.deployment_reconciler import (
432
+ DeploymentReconciler,
433
+ )
434
+
435
+ config = UnifiedConfig()
436
+ reconciler = DeploymentReconciler(config)
437
+
438
+ # Get path to deployed skills directory
439
+ path_manager = get_path_manager()
440
+ deploy_dir = path_manager.get_deploy_dir() / "skills"
441
+
442
+ # Use reconciler's method to list deployed skills
443
+ return reconciler._list_deployed_skills(deploy_dir)
444
+
445
+ except Exception as e:
446
+ logger.warning(f"Failed to get deployed skills: {e}")
447
+ return set()
448
+
449
+
450
+ def run_skill_selector() -> Optional[List[str]]:
451
+ """Main entry point for skill selector.
452
+
453
+ Returns:
454
+ List of selected skill IDs, or None if cancelled
455
+ """
456
+ try:
457
+ # Load config
458
+ config = UnifiedConfig()
459
+
460
+ # Load manifest
461
+ manifest = load_skills_manifest()
462
+ if not manifest:
463
+ return None
464
+
465
+ # Get agent skill dependencies
466
+ agent_deps = get_agent_skill_dependencies(config)
467
+
468
+ # Get deployed skills
469
+ deployed = get_deployed_skills()
470
+
471
+ # Run selector
472
+ selector = SkillSelector(manifest, agent_deps, deployed)
473
+ return selector.select_skills()
474
+
475
+ except KeyboardInterrupt:
476
+ print("\n\n❌ Skill selection cancelled")
477
+ return None
478
+ except Exception as e:
479
+ logger.error(f"Skill selection error: {e}", exc_info=True)
480
+ print(f"\n❌ Skill selection error: {e}")
481
+ return None
@@ -292,6 +292,11 @@ def add_top_level_run_arguments(parser: argparse.ArgumentParser) -> None:
292
292
  action="store_true",
293
293
  help="Force rebuild of all system agents by deleting local claude-mpm agents",
294
294
  )
295
+ run_group.add_argument(
296
+ "--force-sync",
297
+ action="store_true",
298
+ help="Force refresh agents and skills from remote repos, bypassing ETag cache",
299
+ )
295
300
 
296
301
  # Dependency checking options (for backward compatibility at top level)
297
302
  dep_group_top = parser.add_argument_group(
claude_mpm/cli/startup.py CHANGED
@@ -458,7 +458,7 @@ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) ->
458
458
  return removed_count
459
459
 
460
460
 
461
- def sync_remote_agents_on_startup():
461
+ def sync_remote_agents_on_startup(force_sync: bool = False):
462
462
  """
463
463
  Synchronize agent templates from remote sources on startup.
464
464
 
@@ -476,6 +476,9 @@ def sync_remote_agents_on_startup():
476
476
  3. Cleanup orphaned agents (ours but no longer deployed) - Phase 3
477
477
  4. Cleanup legacy agent cache directories (after sync/deployment) - Phase 4
478
478
  5. Log deployment results
479
+
480
+ Args:
481
+ force_sync: Force download even if cache is fresh (bypasses ETag).
479
482
  """
480
483
  # DEPRECATED: Legacy warning - no-op function, kept for compatibility
481
484
  check_legacy_cache()
@@ -511,7 +514,7 @@ def sync_remote_agents_on_startup():
511
514
  )
512
515
 
513
516
  # Phase 1: Sync files from Git sources
514
- result = sync_agents_on_startup()
517
+ result = sync_agents_on_startup(force_refresh=force_sync)
515
518
 
516
519
  # Only proceed with deployment if sync was enabled and ran
517
520
  if result.get("enabled") and result.get("sources_synced", 0) > 0:
@@ -861,7 +864,7 @@ def sync_remote_agents_on_startup():
861
864
  pass # Ignore cleanup errors
862
865
 
863
866
 
864
- def sync_remote_skills_on_startup():
867
+ def sync_remote_skills_on_startup(force_sync: bool = False):
865
868
  """
866
869
  Synchronize skill templates from remote sources on startup.
867
870
 
@@ -879,6 +882,9 @@ def sync_remote_skills_on_startup():
879
882
  4. Apply profile filtering if active
880
883
  5. Deploy resolved skills to ~/.claude/skills/ - Phase 2 progress bar
881
884
  6. Log deployment results with source indication
885
+
886
+ Args:
887
+ force_sync: Force download even if cache is fresh (bypasses ETag).
882
888
  """
883
889
  try:
884
890
  from pathlib import Path
@@ -984,7 +990,7 @@ def sync_remote_skills_on_startup():
984
990
 
985
991
  # Sync all sources with progress callback
986
992
  results = manager.sync_all_sources(
987
- force=False, progress_callback=sync_progress.update
993
+ force=force_sync, progress_callback=sync_progress.update
988
994
  )
989
995
 
990
996
  # Finish sync progress bar with clear breakdown
@@ -1093,7 +1099,7 @@ def sync_remote_skills_on_startup():
1093
1099
  # Deploy to project-local directory with cleanup
1094
1100
  deployment_result = manager.deploy_skills(
1095
1101
  target_dir=Path.cwd() / ".claude" / "skills",
1096
- force=False,
1102
+ force=force_sync,
1097
1103
  # CRITICAL FIX: Empty list should mean "deploy no skills", not "deploy all"
1098
1104
  # When skills_to_deploy is [], we want skill_filter=set() NOT skill_filter=None
1099
1105
  # None means "no filtering" (deploy all), empty set means "filter to nothing"
@@ -1437,7 +1443,7 @@ def auto_install_chrome_devtools_on_startup():
1437
1443
  # Continue execution - chrome-devtools installation failure shouldn't block startup
1438
1444
 
1439
1445
 
1440
- def run_background_services():
1446
+ def run_background_services(force_sync: bool = False):
1441
1447
  """
1442
1448
  Initialize all background services on startup.
1443
1449
 
@@ -1448,6 +1454,9 @@ def run_background_services():
1448
1454
  explicitly requests them via agent-manager commands. This prevents unwanted
1449
1455
  file creation in project .claude/ directories.
1450
1456
  See: SystemInstructionsDeployer and agent_deployment.py line 504-509
1457
+
1458
+ Args:
1459
+ force_sync: Force download even if cache is fresh (bypasses ETag).
1451
1460
  """
1452
1461
  # Sync hooks early to ensure up-to-date configuration
1453
1462
  # RATIONALE: Hooks should be synced before other services to fix stale configs
@@ -1458,7 +1467,7 @@ def run_background_services():
1458
1467
  check_mcp_auto_configuration()
1459
1468
  verify_mcp_gateway_startup()
1460
1469
  check_for_updates_async()
1461
- sync_remote_agents_on_startup() # Sync agents from remote sources
1470
+ sync_remote_agents_on_startup(force_sync=force_sync) # Sync agents from remote sources
1462
1471
  show_agent_summary() # Display agent counts after deployment
1463
1472
 
1464
1473
  # Skills deployment order (precedence: remote > bundled)
@@ -1467,7 +1476,7 @@ def run_background_services():
1467
1476
  # 3. Discover and link runtime skills (user-added skills)
1468
1477
  # This ensures remote skills take precedence over bundled skills when names conflict
1469
1478
  deploy_bundled_skills() # Base layer: package-bundled skills
1470
- sync_remote_skills_on_startup() # Override layer: Git-based skills (takes precedence)
1479
+ sync_remote_skills_on_startup(force_sync=force_sync) # Override layer: Git-based skills (takes precedence)
1471
1480
  discover_and_link_runtime_skills() # Discovery: user-added skills
1472
1481
  show_skill_summary() # Display skill counts after deployment
1473
1482
  verify_and_show_pm_skills() # PM skills verification and status
claude_mpm/constants.py CHANGED
@@ -169,6 +169,7 @@ class SkillsCommands(str, Enum):
169
169
  INFO = "info"
170
170
  CONFIG = "config"
171
171
  CONFIGURE = "configure" # Interactive skills selection (like agents configure)
172
+ SELECT = "select" # Interactive topic-grouped skill selector
172
173
  # GitHub deployment commands
173
174
  DEPLOY_FROM_GITHUB = "deploy-github"
174
175
  LIST_AVAILABLE = "list-available"
@@ -16,7 +16,7 @@ Design Principles:
16
16
  from pathlib import Path
17
17
  from typing import Any, Dict, List, Optional, Union
18
18
 
19
- from pydantic import BaseModel, Field, validator
19
+ from pydantic import BaseModel, Field, field_validator
20
20
  from pydantic_settings import BaseSettings
21
21
 
22
22
  from .exceptions import ConfigurationError
@@ -54,8 +54,9 @@ class LoggingConfig(BaseModel):
54
54
  default=True, description="Enable console logging"
55
55
  )
56
56
 
57
- @validator("level")
58
- def validate_log_level(self, v):
57
+ @field_validator("level")
58
+ @classmethod
59
+ def validate_log_level(cls, v):
59
60
  valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
60
61
  if v.upper() not in valid_levels:
61
62
  raise ValueError(f"Invalid log level. Must be one of: {valid_levels}")
@@ -65,8 +66,15 @@ class LoggingConfig(BaseModel):
65
66
  class AgentConfig(BaseModel):
66
67
  """Agent system configuration."""
67
68
 
69
+ # Explicit deployment lists (simplified model)
70
+ enabled: List[str] = Field(
71
+ default_factory=list,
72
+ description="Explicit list of agent IDs to deploy (empty = use auto_discover)",
73
+ )
74
+
68
75
  auto_discover: bool = Field(
69
- default=True, description="Enable automatic agent discovery"
76
+ default=False,
77
+ description="Enable automatic agent discovery (deprecated, use enabled list)",
70
78
  )
71
79
  precedence: List[str] = Field(
72
80
  default=["project", "user", "system"], description="Agent precedence order"
@@ -239,6 +247,21 @@ class DocumentationConfig(BaseModel):
239
247
  )
240
248
 
241
249
 
250
+ class SkillConfig(BaseModel):
251
+ """Skill system configuration."""
252
+
253
+ # Explicit deployment lists (simplified model)
254
+ enabled: List[str] = Field(
255
+ default_factory=list,
256
+ description="Explicit list of skill IDs to deploy (includes agent dependencies)",
257
+ )
258
+
259
+ auto_detect_dependencies: bool = Field(
260
+ default=True,
261
+ description="Automatically include skills required by enabled agents",
262
+ )
263
+
264
+
242
265
  class UnifiedConfig(BaseSettings):
243
266
  """
244
267
  Unified configuration model for Claude MPM.
@@ -258,6 +281,7 @@ class UnifiedConfig(BaseSettings):
258
281
  network: NetworkConfig = Field(default_factory=NetworkConfig)
259
282
  logging: LoggingConfig = Field(default_factory=LoggingConfig)
260
283
  agents: AgentConfig = Field(default_factory=AgentConfig)
284
+ skills: SkillConfig = Field(default_factory=SkillConfig)
261
285
  memory: MemoryConfig = Field(default_factory=MemoryConfig)
262
286
  security: SecurityConfig = Field(default_factory=SecurityConfig)
263
287
  performance: PerformanceConfig = Field(default_factory=PerformanceConfig)
@@ -287,8 +311,9 @@ class UnifiedConfig(BaseSettings):
287
311
  validate_assignment = True
288
312
  extra = "allow" # Allow extra fields for backward compatibility
289
313
 
290
- @validator("environment")
291
- def validate_environment(self, v):
314
+ @field_validator("environment")
315
+ @classmethod
316
+ def validate_environment(cls, v):
292
317
  valid_envs = ["development", "testing", "production"]
293
318
  if v not in valid_envs:
294
319
  raise ValueError(f"Invalid environment. Must be one of: {valid_envs}")
@@ -554,12 +579,12 @@ class ConfigurationService:
554
579
  import yaml
555
580
 
556
581
  with file_path.open("w") as f:
557
- yaml.dump(self._config.dict(), f, default_flow_style=False)
582
+ yaml.dump(self._config.model_dump(), f, default_flow_style=False)
558
583
  elif format.lower() == "json":
559
584
  import json
560
585
 
561
586
  with file_path.open("w") as f:
562
- json.dump(self._config.dict(), f, indent=2)
587
+ json.dump(self._config.model_dump(), f, indent=2)
563
588
  else:
564
589
  raise ConfigurationError(f"Unsupported export format: {format}")
565
590