claude-mpm 5.4.71__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,539 @@
1
+ """
2
+ Deployment Reconciliation Service - Simplified Deployment Model
3
+
4
+ This service implements the simplified deployment model where:
5
+ 1. Configuration has explicit lists: agents.enabled and skills.enabled
6
+ 2. On startup/sync, reconcile deployed state with configured state
7
+ 3. Deploy agents from cache to project .claude/ directories
8
+ 4. Remove agents/skills NOT in enabled lists
9
+
10
+ Key Principles:
11
+ - Explicit configuration over auto-discovery
12
+ - Clear reconciliation view (configured vs deployed)
13
+ - Simple cleanup of unneeded agents/skills
14
+ - Backward compatibility with empty enabled lists
15
+ """
16
+
17
+ import shutil
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Dict, List, Optional, Set
21
+
22
+ from claude_mpm.core.logging_utils import get_logger
23
+ from claude_mpm.core.unified_config import UnifiedConfig
24
+ from claude_mpm.core.unified_paths import get_path_manager
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class DeploymentResult:
31
+ """Result of deployment reconciliation."""
32
+
33
+ deployed: List[str] # Newly deployed
34
+ removed: List[str] # Removed (not in config)
35
+ unchanged: List[str] # Already deployed and still needed
36
+ errors: List[str] # Errors during reconciliation
37
+
38
+ @property
39
+ def success(self) -> bool:
40
+ """Whether reconciliation succeeded (no errors)."""
41
+ return len(self.errors) == 0
42
+
43
+
44
+ @dataclass
45
+ class ReconciliationState:
46
+ """Current state of agent/skill deployment."""
47
+
48
+ configured: Set[str] # IDs in config enabled list
49
+ deployed: Set[str] # IDs currently deployed
50
+ cached: Set[str] # IDs available in cache
51
+
52
+ @property
53
+ def to_deploy(self) -> Set[str]:
54
+ """Agents/skills that need deployment (in config but not deployed)."""
55
+ return self.configured - self.deployed
56
+
57
+ @property
58
+ def to_remove(self) -> Set[str]:
59
+ """Agents/skills that should be removed (deployed but not in config)."""
60
+ return self.deployed - self.configured
61
+
62
+ @property
63
+ def unchanged(self) -> Set[str]:
64
+ """Agents/skills already deployed and still needed."""
65
+ return self.configured & self.deployed
66
+
67
+
68
+ class DeploymentReconciler:
69
+ """
70
+ Reconciles configured agents/skills with deployed state.
71
+
72
+ This service implements the simplified deployment model:
73
+ 1. Read agents.enabled and skills.enabled from config
74
+ 2. Discover what's currently deployed in .claude/agents and .claude/skills
75
+ 3. Deploy missing agents/skills from cache
76
+ 4. Remove agents/skills not in enabled lists
77
+ """
78
+
79
+ def __init__(self, config: Optional[UnifiedConfig] = None):
80
+ """
81
+ Initialize reconciler.
82
+
83
+ Args:
84
+ config: UnifiedConfig instance (auto-loads if None)
85
+ """
86
+ self.config = config or self._load_config()
87
+ self.path_manager = get_path_manager()
88
+
89
+ def _load_config(self) -> UnifiedConfig:
90
+ """Load configuration from standard location."""
91
+ # For now, return default config
92
+ # TODO: Load from .claude-mpm/configuration.yaml
93
+ return UnifiedConfig()
94
+
95
+ def reconcile_agents(self, project_path: Optional[Path] = None) -> DeploymentResult:
96
+ """
97
+ Reconcile agent deployment with configuration.
98
+
99
+ Args:
100
+ project_path: Project directory (default: current directory)
101
+
102
+ Returns:
103
+ DeploymentResult with reconciliation summary
104
+ """
105
+ project_path = project_path or Path.cwd()
106
+ cache_dir = self.path_manager.get_cache_dir() / "agents"
107
+ deploy_dir = project_path / ".claude" / "agents"
108
+
109
+ # Get current state
110
+ state = self._get_agent_state(cache_dir, deploy_dir)
111
+
112
+ # Check backward compatibility
113
+ if not self.config.agents.enabled and self.config.agents.auto_discover:
114
+ logger.warning(
115
+ "agents.enabled is empty and auto_discover is True. "
116
+ "Consider migrating to explicit agent list. "
117
+ "Falling back to auto-discovery mode."
118
+ )
119
+ # In auto-discovery mode, don't remove anything
120
+ return DeploymentResult(
121
+ deployed=[], removed=[], unchanged=list(state.deployed), errors=[]
122
+ )
123
+
124
+ result = DeploymentResult(deployed=[], removed=[], unchanged=[], errors=[])
125
+
126
+ # Deploy missing agents
127
+ for agent_id in state.to_deploy:
128
+ if agent_id not in state.cached:
129
+ error_msg = f"Agent '{agent_id}' not found in cache. Run 'claude-mpm agents sync' first."
130
+ logger.warning(error_msg)
131
+ result.errors.append(error_msg)
132
+ continue
133
+
134
+ try:
135
+ self._deploy_agent(agent_id, cache_dir, deploy_dir)
136
+ result.deployed.append(agent_id)
137
+ logger.info(f"Deployed agent: {agent_id}")
138
+ except Exception as e:
139
+ error_msg = f"Failed to deploy agent '{agent_id}': {e}"
140
+ logger.error(error_msg)
141
+ result.errors.append(error_msg)
142
+
143
+ # Remove unneeded agents (only MPM agents, not user-created)
144
+ for agent_id in state.to_remove:
145
+ try:
146
+ if self._is_mpm_agent(deploy_dir, agent_id):
147
+ self._remove_agent(agent_id, deploy_dir)
148
+ result.removed.append(agent_id)
149
+ logger.info(f"Removed agent: {agent_id}")
150
+ else:
151
+ logger.debug(f"Skipping removal of user agent: {agent_id}")
152
+ result.unchanged.append(agent_id)
153
+ except Exception as e:
154
+ error_msg = f"Failed to remove agent '{agent_id}': {e}"
155
+ logger.error(error_msg)
156
+ result.errors.append(error_msg)
157
+
158
+ # Track unchanged agents
159
+ result.unchanged.extend(list(state.unchanged))
160
+
161
+ return result
162
+
163
+ def reconcile_skills(self, project_path: Optional[Path] = None) -> DeploymentResult:
164
+ """
165
+ Reconcile skill deployment with configuration.
166
+
167
+ This includes:
168
+ 1. Skills in skills.enabled list
169
+ 2. Skills required by enabled agents (if auto_detect_dependencies=True)
170
+
171
+ Args:
172
+ project_path: Project directory (default: current directory)
173
+
174
+ Returns:
175
+ DeploymentResult with reconciliation summary
176
+ """
177
+ project_path = project_path or Path.cwd()
178
+ cache_dir = self.path_manager.get_cache_dir() / "skills"
179
+ deploy_dir = project_path / ".claude" / "skills"
180
+
181
+ # Get configured skills (explicit + agent dependencies)
182
+ configured_skills = set(self.config.skills.enabled)
183
+
184
+ if self.config.skills.auto_detect_dependencies:
185
+ # Add skills required by enabled agents
186
+ agent_skill_deps = self._get_agent_skill_dependencies(
187
+ self.config.agents.enabled
188
+ )
189
+ configured_skills.update(agent_skill_deps)
190
+
191
+ # Get current state
192
+ state = ReconciliationState(
193
+ configured=configured_skills,
194
+ deployed=self._list_deployed_skills(deploy_dir),
195
+ cached=self._list_cached_skills(cache_dir),
196
+ )
197
+
198
+ result = DeploymentResult(deployed=[], removed=[], unchanged=[], errors=[])
199
+
200
+ # Deploy missing skills
201
+ for skill_id in state.to_deploy:
202
+ if skill_id not in state.cached:
203
+ error_msg = (
204
+ f"Skill '{skill_id}' not found in cache. Check skill sources."
205
+ )
206
+ logger.warning(error_msg)
207
+ result.errors.append(error_msg)
208
+ continue
209
+
210
+ try:
211
+ self._deploy_skill(skill_id, cache_dir, deploy_dir)
212
+ result.deployed.append(skill_id)
213
+ logger.info(f"Deployed skill: {skill_id}")
214
+ except Exception as e:
215
+ error_msg = f"Failed to deploy skill '{skill_id}': {e}"
216
+ logger.error(error_msg)
217
+ result.errors.append(error_msg)
218
+
219
+ # Remove unneeded skills (only MPM skills)
220
+ for skill_id in state.to_remove:
221
+ try:
222
+ if self._is_mpm_skill(deploy_dir, skill_id):
223
+ self._remove_skill(skill_id, deploy_dir)
224
+ result.removed.append(skill_id)
225
+ logger.info(f"Removed skill: {skill_id}")
226
+ else:
227
+ logger.debug(f"Skipping removal of user skill: {skill_id}")
228
+ result.unchanged.append(skill_id)
229
+ except Exception as e:
230
+ error_msg = f"Failed to remove skill '{skill_id}': {e}"
231
+ logger.error(error_msg)
232
+ result.errors.append(error_msg)
233
+
234
+ # Track unchanged skills
235
+ result.unchanged.extend(list(state.unchanged))
236
+
237
+ return result
238
+
239
+ def _get_agent_state(
240
+ self, cache_dir: Path, deploy_dir: Path
241
+ ) -> ReconciliationState:
242
+ """Get current agent deployment state."""
243
+ return ReconciliationState(
244
+ configured=set(self.config.agents.enabled),
245
+ deployed=self._list_deployed_agents(deploy_dir),
246
+ cached=self._list_cached_agents(cache_dir),
247
+ )
248
+
249
+ def _list_deployed_agents(self, deploy_dir: Path) -> Set[str]:
250
+ """List agent IDs currently deployed."""
251
+ if not deploy_dir.exists():
252
+ return set()
253
+
254
+ agent_ids = set()
255
+ for agent_file in deploy_dir.glob("*.md"):
256
+ # Extract agent ID from filename (remove .md extension)
257
+ agent_id = agent_file.stem
258
+ agent_ids.add(agent_id)
259
+
260
+ return agent_ids
261
+
262
+ def _list_cached_agents(self, cache_dir: Path) -> Set[str]:
263
+ """List agent IDs available in cache."""
264
+ if not cache_dir.exists():
265
+ return set()
266
+
267
+ agent_ids = set()
268
+ for agent_file in cache_dir.glob("**/*.md"):
269
+ # Extract agent ID from filename
270
+ agent_id = agent_file.stem
271
+ agent_ids.add(agent_id)
272
+
273
+ return agent_ids
274
+
275
+ def _list_deployed_skills(self, deploy_dir: Path) -> Set[str]:
276
+ """List skill IDs currently deployed."""
277
+ if not deploy_dir.exists():
278
+ return set()
279
+
280
+ skill_ids = set()
281
+ for skill_file in deploy_dir.glob("*.md"):
282
+ skill_id = skill_file.stem
283
+ skill_ids.add(skill_id)
284
+
285
+ return skill_ids
286
+
287
+ def _list_cached_skills(self, cache_dir: Path) -> Set[str]:
288
+ """List skill IDs available in cache."""
289
+ if not cache_dir.exists():
290
+ return set()
291
+
292
+ skill_ids = set()
293
+ for skill_file in cache_dir.glob("**/*.md"):
294
+ skill_id = skill_file.stem
295
+ skill_ids.add(skill_id)
296
+
297
+ return skill_ids
298
+
299
+ def _deploy_agent(self, agent_id: str, cache_dir: Path, deploy_dir: Path) -> None:
300
+ """Deploy agent from cache to project directory."""
301
+ # Find agent file in cache
302
+ agent_file = self._find_file_in_cache(agent_id, cache_dir, "*.md")
303
+ if not agent_file:
304
+ raise FileNotFoundError(f"Agent file for '{agent_id}' not found in cache")
305
+
306
+ # Ensure deploy directory exists
307
+ deploy_dir.mkdir(parents=True, exist_ok=True)
308
+
309
+ # Copy agent file to deployment directory
310
+ dest_file = deploy_dir / agent_file.name
311
+ shutil.copy2(agent_file, dest_file)
312
+
313
+ def _deploy_skill(self, skill_id: str, cache_dir: Path, deploy_dir: Path) -> None:
314
+ """Deploy skill from cache to project directory."""
315
+ # Find skill file in cache
316
+ skill_file = self._find_file_in_cache(skill_id, cache_dir, "*.md")
317
+ if not skill_file:
318
+ raise FileNotFoundError(f"Skill file for '{skill_id}' not found in cache")
319
+
320
+ # Ensure deploy directory exists
321
+ deploy_dir.mkdir(parents=True, exist_ok=True)
322
+
323
+ # Copy skill file to deployment directory
324
+ dest_file = deploy_dir / skill_file.name
325
+ shutil.copy2(skill_file, dest_file)
326
+
327
+ def _remove_agent(self, agent_id: str, deploy_dir: Path) -> None:
328
+ """Remove deployed agent."""
329
+ agent_file = deploy_dir / f"{agent_id}.md"
330
+ if agent_file.exists():
331
+ agent_file.unlink()
332
+
333
+ def _remove_skill(self, skill_id: str, deploy_dir: Path) -> None:
334
+ """Remove deployed skill."""
335
+ skill_file = deploy_dir / f"{skill_id}.md"
336
+ if skill_file.exists():
337
+ skill_file.unlink()
338
+
339
+ def _is_mpm_agent(self, deploy_dir: Path, agent_id: str) -> bool:
340
+ """Check if agent is managed by MPM (not user-created)."""
341
+ agent_file = deploy_dir / f"{agent_id}.md"
342
+ if not agent_file.exists():
343
+ return False
344
+
345
+ try:
346
+ content = agent_file.read_text(encoding="utf-8")
347
+ # Check for MPM author markers
348
+ mpm_markers = [
349
+ "author: claude-mpm",
350
+ "author: 'claude-mpm'",
351
+ "author: anthropic",
352
+ ]
353
+ return any(marker in content.lower() for marker in mpm_markers)
354
+ except Exception as e:
355
+ logger.warning(f"Failed to check MPM marker for {agent_id}: {e}")
356
+ return False
357
+
358
+ def _is_mpm_skill(self, deploy_dir: Path, skill_id: str) -> bool:
359
+ """Check if skill is managed by MPM (not user-created)."""
360
+ skill_file = deploy_dir / f"{skill_id}.md"
361
+ if not skill_file.exists():
362
+ return False
363
+
364
+ try:
365
+ content = skill_file.read_text(encoding="utf-8")
366
+ # Check for MPM author markers
367
+ mpm_markers = [
368
+ "author: claude-mpm",
369
+ "author: 'claude-mpm'",
370
+ "author: anthropic",
371
+ ]
372
+ return any(marker in content.lower() for marker in mpm_markers)
373
+ except Exception as e:
374
+ logger.warning(f"Failed to check MPM marker for {skill_id}: {e}")
375
+ return False
376
+
377
+ def _find_file_in_cache(
378
+ self, item_id: str, cache_dir: Path, pattern: str
379
+ ) -> Optional[Path]:
380
+ """Find file in cache directory by ID pattern."""
381
+ # Try exact match first
382
+ exact_match = cache_dir / f"{item_id}.md"
383
+ if exact_match.exists():
384
+ return exact_match
385
+
386
+ # Search recursively
387
+ for file_path in cache_dir.glob(f"**/{item_id}.md"):
388
+ return file_path
389
+
390
+ return None
391
+
392
+ def _get_agent_skill_dependencies(self, agent_ids: List[str]) -> Set[str]:
393
+ """
394
+ Get skill dependencies for enabled agents.
395
+
396
+ This reads agent frontmatter to find required skills.
397
+
398
+ Args:
399
+ agent_ids: List of enabled agent IDs
400
+
401
+ Returns:
402
+ Set of skill IDs required by these agents
403
+ """
404
+ skill_deps = set()
405
+
406
+ # Get deployed agents directory
407
+ project_path = Path.cwd()
408
+ agents_dir = project_path / ".claude" / "agents"
409
+
410
+ if not agents_dir.exists():
411
+ logger.debug("No agents directory found, cannot extract skill dependencies")
412
+ return skill_deps
413
+
414
+ for agent_id in agent_ids:
415
+ agent_file = agents_dir / f"{agent_id}.md"
416
+ if not agent_file.exists():
417
+ logger.debug(
418
+ f"Agent file not found for {agent_id}, skipping skill dependency extraction"
419
+ )
420
+ continue
421
+
422
+ try:
423
+ # Parse frontmatter to get skills list
424
+ skills = self._parse_agent_skills_from_frontmatter(agent_file)
425
+ if skills:
426
+ logger.debug(f"Agent {agent_id} requires skills: {skills}")
427
+ skill_deps.update(skills)
428
+ except Exception as e:
429
+ logger.warning(f"Failed to parse skills from {agent_id}: {e}")
430
+
431
+ return skill_deps
432
+
433
+ def _parse_agent_skills_from_frontmatter(self, agent_file: Path) -> List[str]:
434
+ """
435
+ Parse skills list from agent frontmatter.
436
+
437
+ Expected frontmatter format:
438
+ ---
439
+ name: Python Engineer
440
+ skills:
441
+ - pytest
442
+ - git-workflow
443
+ ---
444
+
445
+ Args:
446
+ agent_file: Path to agent .md file
447
+
448
+ Returns:
449
+ List of skill IDs from frontmatter (empty if none found)
450
+ """
451
+ try:
452
+ import yaml
453
+ except ImportError:
454
+ logger.warning("PyYAML not installed, cannot parse agent frontmatter")
455
+ return []
456
+
457
+ try:
458
+ content = agent_file.read_text(encoding="utf-8")
459
+
460
+ # Check for frontmatter delimiters
461
+ if not content.startswith("---"):
462
+ return []
463
+
464
+ # Find end of frontmatter
465
+ end_marker = content.find("\n---\n", 4)
466
+ if end_marker == -1:
467
+ end_marker = content.find("\n---\r\n", 4)
468
+
469
+ if end_marker == -1:
470
+ logger.debug(
471
+ f"No valid frontmatter end marker found in {agent_file.name}"
472
+ )
473
+ return []
474
+
475
+ # Extract frontmatter YAML
476
+ frontmatter_yaml = content[4:end_marker]
477
+
478
+ # Parse YAML
479
+ frontmatter = yaml.safe_load(frontmatter_yaml)
480
+
481
+ if not frontmatter or not isinstance(frontmatter, dict):
482
+ return []
483
+
484
+ # Get skills list
485
+ skills = frontmatter.get("skills", [])
486
+ if isinstance(skills, list):
487
+ return [str(skill) for skill in skills]
488
+ logger.debug(
489
+ f"Skills field in {agent_file.name} is not a list: {type(skills)}"
490
+ )
491
+ return []
492
+
493
+ except yaml.YAMLError as e:
494
+ logger.warning(
495
+ f"Failed to parse YAML frontmatter in {agent_file.name}: {e}"
496
+ )
497
+ return []
498
+ except Exception as e:
499
+ logger.warning(
500
+ f"Unexpected error parsing frontmatter in {agent_file.name}: {e}"
501
+ )
502
+ return []
503
+
504
+ def get_reconciliation_view(
505
+ self, project_path: Optional[Path] = None
506
+ ) -> Dict[str, ReconciliationState]:
507
+ """
508
+ Get reconciliation view for agents and skills.
509
+
510
+ Args:
511
+ project_path: Project directory
512
+
513
+ Returns:
514
+ Dictionary with 'agents' and 'skills' reconciliation states
515
+ """
516
+ project_path = project_path or Path.cwd()
517
+
518
+ # Get agent state
519
+ agent_cache = self.path_manager.get_cache_dir() / "agents"
520
+ agent_deploy = project_path / ".claude" / "agents"
521
+ agent_state = self._get_agent_state(agent_cache, agent_deploy)
522
+
523
+ # Get skill state
524
+ skill_cache = self.path_manager.get_cache_dir() / "skills"
525
+ skill_deploy = project_path / ".claude" / "skills"
526
+
527
+ configured_skills = set(self.config.skills.enabled)
528
+ if self.config.skills.auto_detect_dependencies:
529
+ configured_skills.update(
530
+ self._get_agent_skill_dependencies(self.config.agents.enabled)
531
+ )
532
+
533
+ skill_state = ReconciliationState(
534
+ configured=configured_skills,
535
+ deployed=self._list_deployed_skills(skill_deploy),
536
+ cached=self._list_cached_skills(skill_cache),
537
+ )
538
+
539
+ return {"agents": agent_state, "skills": skill_state}
@@ -0,0 +1,138 @@
1
+ """
2
+ Startup Reconciliation Hook
3
+
4
+ This module provides a hook for performing agent/skill reconciliation
5
+ during application startup, ensuring deployed state matches configuration.
6
+
7
+ Usage:
8
+ from claude_mpm.services.agents.deployment.startup_reconciliation import (
9
+ perform_startup_reconciliation
10
+ )
11
+
12
+ # In your startup code
13
+ perform_startup_reconciliation()
14
+ """
15
+
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from claude_mpm.core.logging_utils import get_logger
20
+ from claude_mpm.core.unified_config import UnifiedConfig
21
+
22
+ from .deployment_reconciler import DeploymentReconciler, DeploymentResult
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ def perform_startup_reconciliation(
28
+ project_path: Optional[Path] = None,
29
+ config: Optional[UnifiedConfig] = None,
30
+ silent: bool = False,
31
+ ) -> tuple[DeploymentResult, DeploymentResult]:
32
+ """
33
+ Perform agent and skill reconciliation during startup.
34
+
35
+ This ensures the deployed state (.claude/agents, .claude/skills) matches
36
+ the configuration (agents.enabled, skills.enabled lists).
37
+
38
+ Args:
39
+ project_path: Project directory (default: current directory)
40
+ config: Configuration instance (auto-loads if None)
41
+ silent: Suppress info logging (only errors)
42
+
43
+ Returns:
44
+ Tuple of (agent_result, skill_result)
45
+ """
46
+ project_path = project_path or Path.cwd()
47
+
48
+ # Load config if not provided
49
+ if config is None:
50
+ config = UnifiedConfig()
51
+
52
+ # Initialize reconciler
53
+ reconciler = DeploymentReconciler(config)
54
+
55
+ if not silent:
56
+ logger.info("Performing startup reconciliation...")
57
+
58
+ # Reconcile agents
59
+ agent_result = reconciler.reconcile_agents(project_path)
60
+
61
+ if agent_result.deployed and not silent:
62
+ logger.info(f"Deployed agents: {', '.join(agent_result.deployed)}")
63
+ if agent_result.removed and not silent:
64
+ logger.info(f"Removed agents: {', '.join(agent_result.removed)}")
65
+ if agent_result.errors:
66
+ for error in agent_result.errors:
67
+ logger.error(f"Agent reconciliation error: {error}")
68
+
69
+ # Reconcile skills
70
+ skill_result = reconciler.reconcile_skills(project_path)
71
+
72
+ if skill_result.deployed and not silent:
73
+ logger.info(f"Deployed skills: {', '.join(skill_result.deployed)}")
74
+ if skill_result.removed and not silent:
75
+ logger.info(f"Removed skills: {', '.join(skill_result.removed)}")
76
+ if skill_result.errors:
77
+ for error in skill_result.errors:
78
+ logger.error(f"Skill reconciliation error: {error}")
79
+
80
+ if not silent:
81
+ total_errors = len(agent_result.errors) + len(skill_result.errors)
82
+ if total_errors == 0:
83
+ logger.info("Startup reconciliation complete")
84
+ else:
85
+ logger.warning(
86
+ f"Startup reconciliation complete with {total_errors} errors"
87
+ )
88
+
89
+ return agent_result, skill_result
90
+
91
+
92
+ def check_reconciliation_needed(
93
+ project_path: Optional[Path] = None, config: Optional[UnifiedConfig] = None
94
+ ) -> bool:
95
+ """
96
+ Check if reconciliation is needed (without performing it).
97
+
98
+ Args:
99
+ project_path: Project directory
100
+ config: Configuration instance
101
+
102
+ Returns:
103
+ True if reconciliation would make changes
104
+ """
105
+ project_path = project_path or Path.cwd()
106
+
107
+ if config is None:
108
+ config = UnifiedConfig()
109
+
110
+ reconciler = DeploymentReconciler(config)
111
+ view = reconciler.get_reconciliation_view(project_path)
112
+
113
+ agent_state = view["agents"]
114
+ skill_state = view["skills"]
115
+
116
+ # Check if any changes needed
117
+ return (
118
+ len(agent_state.to_deploy) > 0
119
+ or len(agent_state.to_remove) > 0
120
+ or len(skill_state.to_deploy) > 0
121
+ or len(skill_state.to_remove) > 0
122
+ )
123
+
124
+
125
+ # Example integration in startup code:
126
+ #
127
+ # from claude_mpm.services.agents.deployment.startup_reconciliation import (
128
+ # perform_startup_reconciliation,
129
+ # check_reconciliation_needed
130
+ # )
131
+ #
132
+ # def startup():
133
+ # # Check if reconciliation needed
134
+ # if check_reconciliation_needed():
135
+ # logger.info("Reconciliation needed, performing...")
136
+ # perform_startup_reconciliation()
137
+ # else:
138
+ # logger.debug("No reconciliation needed")
@@ -768,8 +768,10 @@ class AgentDependencyLoader:
768
768
  )
769
769
 
770
770
  if is_uv_tool:
771
- cmd = ["uv", "pip", "install"]
772
- logger.debug("Using 'uv pip install' for UV tool environment")
771
+ cmd = ["uv", "pip", "install", "--python", sys.executable]
772
+ logger.debug(
773
+ f"Using 'uv pip install --python {sys.executable}' for UV tool environment"
774
+ )
773
775
  else:
774
776
  cmd = [sys.executable, "-m", "pip", "install"]
775
777