claude-mpm 5.1.9__py3-none-any.whl → 5.4.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (162) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +85 -0
  4. claude_mpm/agents/agent_loader.py +13 -44
  5. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  6. claude_mpm/cli/__main__.py +4 -0
  7. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  8. claude_mpm/cli/commands/auto_configure.py +210 -25
  9. claude_mpm/cli/commands/config.py +88 -2
  10. claude_mpm/cli/commands/configure.py +1097 -158
  11. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  12. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  13. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  14. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  15. claude_mpm/cli/commands/skills.py +21 -2
  16. claude_mpm/cli/commands/summarize.py +413 -0
  17. claude_mpm/cli/executor.py +11 -3
  18. claude_mpm/cli/parsers/base_parser.py +5 -0
  19. claude_mpm/cli/parsers/config_parser.py +153 -83
  20. claude_mpm/cli/parsers/skills_parser.py +3 -2
  21. claude_mpm/cli/startup.py +333 -89
  22. claude_mpm/commands/mpm-config.md +266 -0
  23. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  24. claude_mpm/config/agent_sources.py +27 -0
  25. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  26. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  27. claude_mpm/core/framework_loader.py +4 -2
  28. claude_mpm/core/logger.py +13 -0
  29. claude_mpm/core/socketio_pool.py +3 -3
  30. claude_mpm/core/unified_agent_registry.py +5 -15
  31. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  32. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -78
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  34. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  35. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  36. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  37. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  38. claude_mpm/hooks/memory_integration_hook.py +46 -1
  39. claude_mpm/init.py +0 -19
  40. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  41. claude_mpm/scripts/launch_monitor.py +93 -13
  42. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  43. claude_mpm/services/agents/agent_review_service.py +280 -0
  44. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  45. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  46. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  47. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
  48. claude_mpm/services/agents/git_source_manager.py +34 -0
  49. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  50. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  51. claude_mpm/services/agents/toolchain_detector.py +10 -6
  52. claude_mpm/services/analysis/__init__.py +11 -1
  53. claude_mpm/services/analysis/clone_detector.py +1030 -0
  54. claude_mpm/services/command_deployment_service.py +71 -10
  55. claude_mpm/services/event_bus/config.py +3 -1
  56. claude_mpm/services/git/git_operations_service.py +93 -8
  57. claude_mpm/services/monitor/daemon.py +9 -2
  58. claude_mpm/services/monitor/daemon_manager.py +39 -3
  59. claude_mpm/services/monitor/server.py +225 -19
  60. claude_mpm/services/self_upgrade_service.py +120 -12
  61. claude_mpm/services/skills/__init__.py +3 -0
  62. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  63. claude_mpm/services/skills/selective_skill_deployer.py +230 -0
  64. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  65. claude_mpm/services/skills_deployer.py +64 -3
  66. claude_mpm/services/socketio/event_normalizer.py +15 -1
  67. claude_mpm/services/socketio/server/core.py +160 -21
  68. claude_mpm/services/version_control/git_operations.py +103 -0
  69. claude_mpm/utils/agent_filters.py +17 -44
  70. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/METADATA +47 -84
  71. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/RECORD +76 -150
  72. claude_mpm-5.4.14.dist-info/entry_points.txt +5 -0
  73. claude_mpm-5.4.14.dist-info/licenses/LICENSE +94 -0
  74. claude_mpm-5.4.14.dist-info/licenses/LICENSE-FAQ.md +153 -0
  75. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  76. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  77. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  78. claude_mpm/agents/BASE_OPS.md +0 -219
  79. claude_mpm/agents/BASE_PM.md +0 -480
  80. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  81. claude_mpm/agents/BASE_QA.md +0 -167
  82. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  83. claude_mpm/agents/base_agent.json +0 -31
  84. claude_mpm/agents/base_agent_loader.py +0 -601
  85. claude_mpm/cli/ticket_cli.py +0 -35
  86. claude_mpm/commands/mpm-config-view.md +0 -150
  87. claude_mpm/dashboard/analysis_runner.py +0 -455
  88. claude_mpm/dashboard/index.html +0 -13
  89. claude_mpm/dashboard/open_dashboard.py +0 -66
  90. claude_mpm/dashboard/static/css/activity.css +0 -1958
  91. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  92. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  93. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  94. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  95. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  96. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  97. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  98. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  99. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  100. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  101. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  102. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  103. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  104. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  105. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  106. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  107. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  108. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  109. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  110. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  111. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  112. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  113. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  114. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  115. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  116. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  117. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  118. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  119. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  120. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  121. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  122. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  123. claude_mpm/dashboard/templates/code_simple.html +0 -153
  124. claude_mpm/dashboard/templates/index.html +0 -606
  125. claude_mpm/dashboard/test_dashboard.html +0 -372
  126. claude_mpm/scripts/mcp_server.py +0 -75
  127. claude_mpm/scripts/mcp_wrapper.py +0 -39
  128. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  129. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  130. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  131. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  132. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  133. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  134. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  135. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  136. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  137. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  138. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  139. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  140. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  141. claude_mpm/services/mcp_gateway/main.py +0 -589
  142. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  143. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  144. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  145. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  146. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  147. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  148. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  149. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  150. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  151. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  152. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  153. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  154. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  155. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  156. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  157. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  158. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  159. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  160. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  161. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/WHEEL +0 -0
  162. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/top_level.txt +0 -0
claude_mpm/cli/startup.py CHANGED
@@ -14,6 +14,52 @@ import warnings
14
14
  from pathlib import Path
15
15
 
16
16
 
17
+ def sync_hooks_on_startup(quiet: bool = False) -> bool:
18
+ """Ensure hooks are up-to-date on startup.
19
+
20
+ WHY: Users can have stale hook configurations in settings.json that cause errors.
21
+ Reinstalling hooks ensures the hook format matches the current code.
22
+
23
+ DESIGN DECISION: Shows brief status message on success for user awareness.
24
+ Failures are logged but don't prevent startup to ensure claude-mpm remains functional.
25
+
26
+ Args:
27
+ quiet: If True, suppress all output (used internally)
28
+
29
+ Returns:
30
+ bool: True if hooks were synced successfully, False otherwise
31
+ """
32
+ try:
33
+ from ..hooks.claude_hooks.installer import HookInstaller
34
+
35
+ installer = HookInstaller()
36
+
37
+ # Show brief status (hooks sync is fast)
38
+ if not quiet:
39
+ print("Syncing Claude Code hooks...", end=" ", flush=True)
40
+
41
+ # Reinstall hooks (force=True ensures update)
42
+ success = installer.install_hooks(force=True)
43
+
44
+ if not quiet:
45
+ if success:
46
+ print("✓")
47
+ else:
48
+ print("(skipped)")
49
+
50
+ return success
51
+
52
+ except Exception as e:
53
+ if not quiet:
54
+ print("(error)")
55
+ # Log but don't fail startup
56
+ from ..core.logger import get_logger
57
+
58
+ logger = get_logger("startup")
59
+ logger.warning(f"Hook sync failed (non-fatal): {e}")
60
+ return False
61
+
62
+
17
63
  def check_legacy_cache() -> None:
18
64
  """Check for legacy cache/agents/ directory and warn user.
19
65
 
@@ -221,78 +267,157 @@ def discover_and_link_runtime_skills():
221
267
 
222
268
  def deploy_output_style_on_startup():
223
269
  """
224
- Deploy claude-mpm output style to Claude Code on CLI startup.
270
+ Deploy claude-mpm output styles to PROJECT-LEVEL directory on CLI startup.
225
271
 
226
- WHY: Automatically deploy and activate the output style to ensure consistent,
227
- professional communication without emojis and exclamation points. This ensures
228
- the style is available even when using Claude Code directly (not via chat command).
272
+ WHY: Automatically deploy output styles to ensure consistent, professional
273
+ communication without emojis and exclamation points. Styles are project-specific
274
+ to allow different projects to have different communication styles.
229
275
 
230
- DESIGN DECISION: This is non-blocking and idempotent. It uses OutputStyleManager
231
- which handles version detection, file deployment, and settings activation.
232
- Only works for Claude Code >= 1.0.83.
276
+ DESIGN DECISION: This is non-blocking and idempotent. Deploys to project-level
277
+ directory (.claude/settings/output-styles/) instead of user-level to maintain
278
+ project isolation.
279
+
280
+ Deploys two styles:
281
+ - claude-mpm-style.md (professional mode)
282
+ - claude-mpm-teacher.md (teaching mode)
233
283
  """
234
284
  try:
285
+ import shutil
235
286
  from pathlib import Path
236
287
 
237
- from ..core.output_style_manager import OutputStyleManager
238
-
239
- # Create OutputStyleManager instance
240
- output_style_manager = OutputStyleManager()
241
-
242
- # Check if Claude Code supports output styles
243
- if not output_style_manager.supports_output_styles():
244
- # Silently skip - version too old or Claude not installed
245
- return
246
-
247
- # Check if already deployed and active
248
- settings_file = Path.home() / ".claude" / "settings.json"
249
- output_style_file = Path.home() / ".claude" / "output-styles" / "claude-mpm.md"
250
-
251
- already_configured = False
252
- if settings_file.exists() and output_style_file.exists():
253
- try:
254
- import json
255
-
256
- # Check if file has content (bug fix: was skipping empty files)
257
- if output_style_file.stat().st_size == 0:
258
- # File is empty, need to redeploy with content
259
- pass # Fall through to deployment below
260
- else:
261
- # File has content, check if already active
262
- settings = json.loads(settings_file.read_text())
263
- if settings.get("activeOutputStyle") == "claude-mpm":
264
- # Already deployed and active with content
265
- already_configured = True
266
- except Exception:
267
- pass # Continue with deployment if we can't read settings
288
+ # Source files (in framework package)
289
+ package_dir = Path(__file__).parent.parent / "agents"
290
+ professional_source = package_dir / "CLAUDE_MPM_OUTPUT_STYLE.md"
291
+ teacher_source = package_dir / "CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md"
292
+
293
+ # Target directory (PROJECT-LEVEL, not user-level)
294
+ project_dir = Path.cwd()
295
+ output_styles_dir = project_dir / ".claude" / "settings" / "output-styles"
296
+ professional_target = output_styles_dir / "claude-mpm-style.md"
297
+ teacher_target = output_styles_dir / "claude-mpm-teacher.md"
298
+
299
+ # Create directory if it doesn't exist
300
+ output_styles_dir.mkdir(parents=True, exist_ok=True)
301
+
302
+ # Check if already deployed (both files exist and have content)
303
+ already_deployed = (
304
+ professional_target.exists()
305
+ and teacher_target.exists()
306
+ and professional_target.stat().st_size > 0
307
+ and teacher_target.stat().st_size > 0
308
+ )
268
309
 
269
- if already_configured:
270
- # Show feedback that output style is ready
271
- print("✓ Output style configured", flush=True)
310
+ if already_deployed:
311
+ # Show feedback that output styles are ready
312
+ print("✓ Output styles ready", flush=True)
272
313
  return
273
314
 
274
- # Read OUTPUT_STYLE.md content
275
- output_style_path = Path(__file__).parent.parent / "agents" / "OUTPUT_STYLE.md"
315
+ # Deploy both styles
316
+ deployed_count = 0
317
+ if professional_source.exists():
318
+ shutil.copy2(professional_source, professional_target)
319
+ deployed_count += 1
276
320
 
277
- if not output_style_path.exists():
278
- # No output style file to deploy
279
- return
321
+ if teacher_source.exists():
322
+ shutil.copy2(teacher_source, teacher_target)
323
+ deployed_count += 1
280
324
 
281
- output_style_content = output_style_path.read_text()
325
+ if deployed_count > 0:
326
+ print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
327
+ else:
328
+ # Source files missing - log but don't fail
329
+ from ..core.logger import get_logger
282
330
 
283
- # Deploy the output style (deploys file and activates it)
284
- output_style_manager.deploy_output_style(output_style_content)
285
- print("✓ Output style configured", flush=True)
331
+ logger = get_logger("cli")
332
+ logger.debug("Output style source files not found")
286
333
 
287
334
  except Exception as e:
288
335
  # Non-critical - log but don't fail startup
289
336
  from ..core.logger import get_logger
290
337
 
291
338
  logger = get_logger("cli")
292
- logger.debug(f"Failed to deploy output style: {e}")
339
+ logger.debug(f"Failed to deploy output styles: {e}")
293
340
  # Continue execution - output style deployment shouldn't block startup
294
341
 
295
342
 
343
+ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) -> int:
344
+ """Remove agents that are managed by claude-mpm but no longer deployed.
345
+
346
+ WHY: When agent configurations change, old agents should be removed to avoid
347
+ confusion and stale agent references. Only removes claude-mpm managed agents,
348
+ leaving user-created agents untouched.
349
+
350
+ SAFETY: Only removes files with claude-mpm ownership markers in frontmatter.
351
+ Files without frontmatter or without ownership indicators are preserved.
352
+
353
+ Args:
354
+ deploy_target: Path to .claude/agents/ directory
355
+ deployed_agents: List of agent filenames that should remain
356
+
357
+ Returns:
358
+ Number of agents removed
359
+ """
360
+ import re
361
+
362
+ import yaml
363
+
364
+ from ..core.logger import get_logger
365
+
366
+ logger = get_logger("cli")
367
+ removed_count = 0
368
+ deployed_set = set(deployed_agents)
369
+
370
+ if not deploy_target.exists():
371
+ return 0
372
+
373
+ # Scan all .md files in agents directory
374
+ for agent_file in deploy_target.glob("*.md"):
375
+ # Skip hidden files
376
+ if agent_file.name.startswith("."):
377
+ continue
378
+
379
+ # Skip if this agent should remain deployed
380
+ if agent_file.name in deployed_set:
381
+ continue
382
+
383
+ # Check if this is a claude-mpm managed agent
384
+ try:
385
+ content = agent_file.read_text(encoding="utf-8")
386
+
387
+ # Parse YAML frontmatter
388
+ if content.startswith("---"):
389
+ match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
390
+ if match:
391
+ frontmatter = yaml.safe_load(match.group(1))
392
+
393
+ # Check ownership indicators
394
+ is_ours = False
395
+ if frontmatter:
396
+ author = frontmatter.get("author", "")
397
+ source = frontmatter.get("source", "")
398
+ agent_id = frontmatter.get("agent_id", "")
399
+
400
+ # It's ours if it has any of these markers
401
+ if (
402
+ "Claude MPM" in str(author)
403
+ or source == "remote"
404
+ or agent_id
405
+ ):
406
+ is_ours = True
407
+
408
+ if is_ours:
409
+ # Safe to remove - it's our agent but not deployed
410
+ agent_file.unlink()
411
+ removed_count += 1
412
+ logger.info(f"Removed orphaned agent: {agent_file.name}")
413
+
414
+ except Exception as e:
415
+ logger.debug(f"Could not check agent {agent_file.name}: {e}")
416
+ # Don't remove if we can't verify ownership
417
+
418
+ return removed_count
419
+
420
+
296
421
  def sync_remote_agents_on_startup():
297
422
  """
298
423
  Synchronize agent templates from remote sources on startup.
@@ -308,7 +433,8 @@ def sync_remote_agents_on_startup():
308
433
  Workflow:
309
434
  1. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
310
435
  2. Deploy agents to ~/.claude/agents/ - Phase 2 progress bar
311
- 3. Log deployment results
436
+ 3. Cleanup orphaned agents (ours but no longer deployed) - Phase 3
437
+ 4. Log deployment results
312
438
  """
313
439
  # Check for legacy cache and warn user if found
314
440
  check_legacy_cache()
@@ -386,6 +512,7 @@ def sync_remote_agents_on_startup():
386
512
  # 1. Must have "/agents/" in path (from git repos)
387
513
  # 2. Must not be in PM templates or doc files
388
514
  # 3. Exclude BASE-AGENT.md which is not a deployable agent
515
+ # 4. Exclude build artifacts (dist/, build/, .cache/) to prevent double-counting
389
516
  agent_files = [
390
517
  f
391
518
  for f in all_md_files
@@ -396,44 +523,98 @@ def sync_remote_agents_on_startup():
396
523
  and f.name.lower() not in pm_templates
397
524
  and f.name.lower() not in doc_files
398
525
  and f.name.lower() != "base-agent.md"
526
+ # Exclude build artifacts (prevents double-counting source + built files)
527
+ and not any(
528
+ part in str(f).split("/")
529
+ for part in ["dist", "build", ".cache"]
530
+ )
399
531
  )
400
532
  ]
401
533
  agent_count = len(agent_files)
402
534
 
403
535
  if agent_count > 0:
404
- # Create progress bar for deployment phase
405
- deploy_progress = ProgressBar(
406
- total=agent_count,
407
- prefix="Deploying agents",
408
- show_percentage=True,
409
- show_counter=True,
410
- )
411
-
412
- # Deploy agents with progress callback
413
- deploy_target = Path.home() / ".claude" / "agents"
536
+ # Deploy agents to project-level directory where Claude Code expects them
537
+ deploy_target = Path.cwd() / ".claude" / "agents"
414
538
  deployment_result = deployment_service.deploy_agents(
415
539
  target_dir=deploy_target,
416
540
  force_rebuild=False, # Only deploy if versions differ
417
541
  deployment_mode="update", # Version-aware updates
418
542
  )
419
543
 
420
- # Update progress bar (single increment since deploy_agents is batch)
421
- deploy_progress.update(agent_count)
422
-
423
- # Finish deployment progress bar
544
+ # Get actual counts from deployment result (reflects configured agents)
424
545
  deployed = len(deployment_result.get("deployed", []))
425
546
  updated = len(deployment_result.get("updated", []))
426
547
  skipped = len(deployment_result.get("skipped", []))
427
- total_available = deployed + updated + skipped
548
+ total_configured = deployed + updated + skipped
549
+
550
+ # FALLBACK: If deployment result doesn't track skipped agents (async path),
551
+ # count existing agents in target directory as "already deployed"
552
+ # This ensures accurate reporting when agents are already up-to-date
553
+ if total_configured == 0 and deploy_target.exists():
554
+ existing_agents = list(deploy_target.glob("*.md"))
555
+ # Filter out non-agent files (e.g., README.md, INSTRUCTIONS.md)
556
+ agent_count_in_target = len(
557
+ [
558
+ f
559
+ for f in existing_agents
560
+ if not f.name.startswith(("README", "INSTRUCTIONS"))
561
+ ]
562
+ )
563
+ if agent_count_in_target > 0:
564
+ # All agents already deployed - count them as skipped
565
+ skipped = agent_count_in_target
566
+ total_configured = agent_count_in_target
567
+
568
+ # Create progress bar with actual configured agent count (not raw file count)
569
+ deploy_progress = ProgressBar(
570
+ total=total_configured if total_configured > 0 else 1,
571
+ prefix="Deploying agents",
572
+ show_percentage=True,
573
+ show_counter=True,
574
+ )
575
+
576
+ # Update progress bar to completion
577
+ deploy_progress.update(
578
+ total_configured if total_configured > 0 else 1
579
+ )
580
+
581
+ # Cleanup orphaned agents (ours but no longer deployed)
582
+ # Get list of deployed agent filenames (what should remain)
583
+ deployed_filenames = []
584
+ for agent_name in deployment_result.get("deployed", []):
585
+ deployed_filenames.append(f"{agent_name}.md")
586
+ for agent_name in deployment_result.get("updated", []):
587
+ deployed_filenames.append(f"{agent_name}.md")
588
+ for agent_name in deployment_result.get("skipped", []):
589
+ deployed_filenames.append(f"{agent_name}.md")
590
+
591
+ # Run cleanup and get count of removed agents
592
+ removed = _cleanup_orphaned_agents(
593
+ deploy_target, deployed_filenames
594
+ )
428
595
 
429
- # Show total available agents (deployed + updated + already existing)
596
+ # Show total configured agents (deployed + updated + already existing)
597
+ # Include repo count for context and removed count if any
430
598
  if deployed > 0 or updated > 0:
599
+ if removed > 0:
600
+ deploy_progress.finish(
601
+ f"Complete: {deployed} new, {updated} updated, {skipped} unchanged, "
602
+ f"{removed} removed ({total_configured} configured from {agent_count} in repo)"
603
+ )
604
+ else:
605
+ deploy_progress.finish(
606
+ f"Complete: {deployed} new, {updated} updated, {skipped} unchanged "
607
+ f"({total_configured} configured from {agent_count} in repo)"
608
+ )
609
+ elif removed > 0:
431
610
  deploy_progress.finish(
432
- f"Complete: {deployed} deployed, {updated} updated, {skipped} already present ({total_available} total)"
611
+ f"Complete: {total_configured} agents ready - all unchanged, "
612
+ f"{removed} removed ({agent_count} available in repo)"
433
613
  )
434
614
  else:
435
615
  deploy_progress.finish(
436
- f"Complete: {total_available} agents ready (all up-to-date)"
616
+ f"Complete: {total_configured} agents ready - all unchanged "
617
+ f"({agent_count} available in repo)"
437
618
  )
438
619
 
439
620
  # Display deployment errors to user (not just logs)
@@ -523,6 +704,8 @@ def sync_remote_skills_on_startup():
523
704
 
524
705
  # Discover total file count across all sources
525
706
  total_file_count = 0
707
+ total_skill_dirs = 0 # Count actual skill directories (folders with SKILL.md)
708
+
526
709
  for source in enabled_sources:
527
710
  try:
528
711
  # Parse GitHub URL
@@ -546,15 +729,26 @@ def sync_remote_skills_on_startup():
546
729
  ]
547
730
  total_file_count += len(relevant_files)
548
731
 
732
+ # Count skill directories (unique directories containing SKILL.md)
733
+ skill_dirs = set()
734
+ for f in all_files:
735
+ if f.endswith("/SKILL.md"):
736
+ # Extract directory path
737
+ skill_dir = "/".join(f.split("/")[:-1])
738
+ skill_dirs.add(skill_dir)
739
+ total_skill_dirs += len(skill_dirs)
740
+
549
741
  except Exception as e:
550
742
  logger.debug(f"Failed to discover files for {source.id}: {e}")
551
743
  # Use estimate if discovery fails
552
744
  total_file_count += 150
745
+ total_skill_dirs += 50 # Estimate ~50 skills
553
746
 
554
747
  # Create progress bar for sync phase with actual file count
748
+ # Note: We sync files (md, json, etc.), but will deploy skill directories
555
749
  sync_progress = ProgressBar(
556
750
  total=total_file_count if total_file_count > 0 else 1,
557
- prefix="Syncing skills",
751
+ prefix="Syncing skill files",
558
752
  show_percentage=True,
559
753
  show_counter=True,
560
754
  )
@@ -571,51 +765,96 @@ def sync_remote_skills_on_startup():
571
765
 
572
766
  if cached > 0:
573
767
  sync_progress.finish(
574
- f"Complete: {downloaded} downloaded, {cached} cached ({total_files} total)"
768
+ f"Complete: {downloaded} downloaded, {cached} cached ({total_files} files, {total_skill_dirs} skills)"
575
769
  )
576
770
  else:
577
771
  # All new downloads (first sync)
578
- sync_progress.finish(f"Complete: {downloaded} files downloaded")
772
+ sync_progress.finish(
773
+ f"Complete: {downloaded} files downloaded ({total_skill_dirs} skills)"
774
+ )
579
775
 
580
776
  # Phase 2: Deploy skills to ~/.claude/skills/
581
777
  # This flattens nested Git structure (e.g., collaboration/parallel-agents/SKILL.md)
582
778
  # into flat deployment (e.g., collaboration-dispatching-parallel-agents/SKILL.md)
583
779
  if results["synced_count"] > 0:
584
- # Get all skills to determine deployment count
780
+ # Get required skills from deployed agents (selective deployment)
781
+ from ..services.skills.selective_skill_deployer import (
782
+ get_required_skills_from_agents,
783
+ )
784
+
785
+ agents_dir = Path.cwd() / ".claude" / "agents"
786
+ required_skills = get_required_skills_from_agents(agents_dir)
787
+
788
+ # Get all skills to determine counts
585
789
  all_skills = manager.get_all_skills()
586
- skill_count = len(all_skills)
790
+ total_skill_count = len(all_skills)
587
791
 
588
- if skill_count > 0:
589
- # Create progress bar for deployment phase
590
- deploy_progress = ProgressBar(
591
- total=skill_count,
592
- prefix="Deploying skill directories",
593
- show_percentage=True,
594
- show_counter=True,
595
- )
792
+ # Determine skill count based on whether we have agent requirements
793
+ if required_skills:
794
+ # Selective deployment: only skills required by deployed agents
795
+ skill_count = len(required_skills)
796
+ else:
797
+ # No agent requirements found - deploy all skills
798
+ skill_count = total_skill_count
596
799
 
597
- # Deploy skills with progress callback
800
+ if skill_count > 0:
801
+ # Deploy skills with selective filter (if agent requirements exist)
598
802
  # Deploy to project directory (like agents), not user directory
599
803
  deployment_result = manager.deploy_skills(
600
804
  target_dir=Path.cwd() / ".claude" / "skills",
601
805
  force=False,
602
- progress_callback=deploy_progress.update,
806
+ skill_filter=required_skills if required_skills else None,
603
807
  )
604
808
 
605
- # Finish deployment progress bar
809
+ # Get actual counts from deployment result
606
810
  deployed = deployment_result.get("deployed_count", 0)
607
811
  skipped = deployment_result.get("skipped_count", 0)
812
+ filtered = deployment_result.get("filtered_count", 0)
608
813
  total_available = deployed + skipped
609
814
 
815
+ # Only show progress bar if there are skills to deploy
816
+ if total_available > 0:
817
+ deploy_progress = ProgressBar(
818
+ total=total_available,
819
+ prefix="Deploying skill directories",
820
+ show_percentage=True,
821
+ show_counter=True,
822
+ )
823
+ # Update progress bar to completion
824
+ deploy_progress.update(total_available)
825
+ else:
826
+ # No skills to deploy - create dummy progress for message only
827
+ deploy_progress = ProgressBar(
828
+ total=1,
829
+ prefix="Deploying skill directories",
830
+ show_percentage=False,
831
+ show_counter=False,
832
+ )
833
+ deploy_progress.update(1)
834
+
610
835
  # Show total available skills (deployed + already existing)
611
- # This is more user-friendly than just showing newly deployed count
836
+ # Include filtered count if selective deployment was used
837
+ # Note: total_skill_count is from the repo, total_available is what's deployed/needed
612
838
  if deployed > 0:
839
+ if filtered > 0:
840
+ deploy_progress.finish(
841
+ f"Complete: {deployed} new, {skipped} unchanged "
842
+ f"({total_available} required by agents, {filtered} available in repo)"
843
+ )
844
+ else:
845
+ deploy_progress.finish(
846
+ f"Complete: {deployed} new, {skipped} unchanged "
847
+ f"({total_available} skills deployed from {total_skill_count} in repo)"
848
+ )
849
+ elif filtered > 0:
850
+ # Skills filtered means agents require fewer skills than available
613
851
  deploy_progress.finish(
614
- f"Complete: {deployed} deployed, {skipped} already present ({total_available} total)"
852
+ f"Agents require no skills ({total_skill_count} available in repo)"
615
853
  )
616
854
  else:
617
855
  deploy_progress.finish(
618
- f"Complete: {total_available} skills ready (all up-to-date)"
856
+ f"Complete: {total_available} skills deployed for agents "
857
+ f"({total_skill_count} available in repo)"
619
858
  )
620
859
 
621
860
  # Log deployment errors if any
@@ -656,6 +895,11 @@ def run_background_services():
656
895
  file creation in project .claude/ directories.
657
896
  See: SystemInstructionsDeployer and agent_deployment.py line 504-509
658
897
  """
898
+ # Sync hooks early to ensure up-to-date configuration
899
+ # RATIONALE: Hooks should be synced before other services to fix stale configs
900
+ # This is fast (<100ms) and non-blocking, so it doesn't delay startup
901
+ sync_hooks_on_startup() # Shows "Syncing Claude Code hooks... ✓"
902
+
659
903
  initialize_project_registry()
660
904
  check_mcp_auto_configuration()
661
905
  verify_mcp_gateway_startup()