claude-mpm 5.4.64__py3-none-any.whl → 5.4.96__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 (163) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md +405 -0
  3. claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +66 -241
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +107 -1928
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +82 -686
  6. claude_mpm/cli/__init__.py +5 -1
  7. claude_mpm/cli/commands/agents.py +2 -4
  8. claude_mpm/cli/commands/agents_reconcile.py +197 -0
  9. claude_mpm/cli/commands/autotodos.py +526 -0
  10. claude_mpm/cli/commands/configure.py +620 -21
  11. claude_mpm/cli/commands/monitor.py +2 -2
  12. claude_mpm/cli/commands/mpm_init/core.py +2 -2
  13. claude_mpm/cli/commands/skills.py +166 -14
  14. claude_mpm/cli/executor.py +89 -0
  15. claude_mpm/cli/interactive/__init__.py +10 -0
  16. claude_mpm/cli/interactive/agent_wizard.py +30 -50
  17. claude_mpm/cli/interactive/questionary_styles.py +65 -0
  18. claude_mpm/cli/interactive/skill_selector.py +481 -0
  19. claude_mpm/cli/parsers/base_parser.py +59 -1
  20. claude_mpm/cli/startup.py +202 -367
  21. claude_mpm/cli/startup_display.py +72 -5
  22. claude_mpm/cli/startup_logging.py +2 -2
  23. claude_mpm/commands/mpm-session-resume.md +1 -1
  24. claude_mpm/constants.py +1 -0
  25. claude_mpm/core/claude_runner.py +2 -2
  26. claude_mpm/core/hook_manager.py +51 -3
  27. claude_mpm/core/interactive_session.py +7 -7
  28. claude_mpm/core/output_style_manager.py +21 -13
  29. claude_mpm/core/unified_config.py +50 -8
  30. claude_mpm/core/unified_paths.py +30 -13
  31. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  32. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  33. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cs_tUR18.js → 1WZnGYqX.js} +1 -1
  34. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CDuw-vjf.js → 67pF3qNn.js} +1 -1
  35. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bTOqqlTd.js → 6RxdMKe4.js} +1 -1
  36. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DwBR2MJi.js → 8cZrfX0h.js} +1 -1
  37. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{ZGh7QtNv.js → 9a6T2nm-.js} +1 -1
  38. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D9lljYKQ.js → B443AUzu.js} +1 -1
  39. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{RJiighC3.js → B8AwtY2H.js} +1 -1
  40. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uuIeMWc-.js → BF15LAsF.js} +1 -1
  41. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{D3k0OPJN.js → BRcwIQNr.js} +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CyWMqx4W.js → BV6nKitt.js} +1 -1
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CiIAseT4.js → BViJ8lZt.js} +5 -5
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CBBdVcY8.js → BcQ-Q0FE.js} +1 -1
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BovzEFCE.js → Bpyvgze_.js} +1 -1
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{eNVUfhuA.js → C3rbW_a-.js} +1 -1
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{GYwsonyD.js → C8WYN38h.js} +1 -1
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BIF9m_hv.js → C9I8FlXH.js} +1 -1
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B0uc0UOD.js → CIQcWgO2.js} +3 -3
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Be7GpZd6.js → CIctN7YN.js} +1 -1
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Bh0LDWpI.js → CKrS_JZW.js} +2 -2
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DUrLdbGD.js → CR6P9C4A.js} +1 -1
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7xVLGWV.js → CRRR9MD_.js} +1 -1
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dhb8PKl3.js → CSXtMOf0.js} +1 -1
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BPYeabCQ.js → CT-sbxSk.js} +1 -1
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{sQeU3Y1z.js → CWm6DJsp.js} +1 -1
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CnA0NrzZ.js → CpqQ1Kzn.js} +1 -1
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4B-KCzX.js → D2nGpDRe.js} +1 -1
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DGkLK5U1.js → D9iCMida.js} +1 -1
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{BofRWZRR.js → D9ykgMoY.js} +1 -1
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DmxopI1J.js → DL2Ldur1.js} +1 -1
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C30mlcqg.js → DPfltzjH.js} +1 -1
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Vzk33B_K.js → DR8nis88.js} +2 -2
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DI7hHRFL.js → DUliQN2b.js} +1 -1
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C4JcI4KD.js → DXlhR01x.js} +1 -1
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{bT1r9zLR.js → D_lyTybS.js} +1 -1
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DZX00Y4g.js → DngoTTgh.js} +1 -1
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzZX-COe.js → DqkmHtDC.js} +1 -1
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{B7RN905-.js → DsDh8EYs.js} +1 -1
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DLVjFsZ3.js → DypDmXgd.js} +1 -1
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{iEWssX7S.js → IPYC-LnN.js} +1 -1
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DaimHw_p.js → JpevfAFt.js} +1 -1
  77. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DY1XQ8fi.js → R8CEIRAd.js} +1 -1
  78. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Dle-35c7.js → Zxy7qc-l.js} +2 -2
  79. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  80. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{C_Usid8X.js → qtd3IeO4.js} +2 -2
  81. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CzeYkLYB.js → ulBFON_C.js} +2 -2
  82. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{Cfqx1Qun.js → wQVh1CoA.js} +1 -1
  83. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/{app.D6-I5TpK.js → app.Dr7t0z2J.js} +2 -2
  84. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  85. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.m1gL8KXf.js → 0.RgBboRvH.js} +1 -1
  86. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{1.CgNOuw-d.js → 1.DG-KkbDf.js} +1 -1
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  88. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  89. claude_mpm/dashboard/static/svelte-build/index.html +9 -9
  90. claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
  91. claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
  92. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +486 -0
  97. claude_mpm/hooks/claude_hooks/event_handlers.py +216 -11
  98. claude_mpm/hooks/claude_hooks/hook_handler.py +28 -4
  99. claude_mpm/hooks/claude_hooks/response_tracking.py +3 -1
  100. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  101. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  102. claude_mpm/hooks/claude_hooks/services/connection_manager.py +20 -0
  103. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +30 -6
  104. claude_mpm/hooks/session_resume_hook.py +85 -1
  105. claude_mpm/init.py +1 -1
  106. claude_mpm/services/agents/cache_git_manager.py +1 -1
  107. claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
  108. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +3 -0
  109. claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
  110. claude_mpm/services/agents/startup_sync.py +5 -2
  111. claude_mpm/services/cli/__init__.py +3 -0
  112. claude_mpm/services/cli/incremental_pause_manager.py +561 -0
  113. claude_mpm/services/cli/session_resume_helper.py +10 -2
  114. claude_mpm/services/delegation_detector.py +175 -0
  115. claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
  116. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  117. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  118. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  119. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  120. claude_mpm/services/diagnostics/models.py +14 -1
  121. claude_mpm/services/event_log.py +317 -0
  122. claude_mpm/services/infrastructure/__init__.py +4 -0
  123. claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
  124. claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
  125. claude_mpm/services/monitor/daemon_manager.py +15 -4
  126. claude_mpm/services/monitor/management/lifecycle.py +8 -2
  127. claude_mpm/services/monitor/server.py +106 -16
  128. claude_mpm/services/pm_skills_deployer.py +177 -83
  129. claude_mpm/services/skills/git_skill_source_manager.py +5 -1
  130. claude_mpm/services/skills/selective_skill_deployer.py +114 -26
  131. claude_mpm/services/socketio/handlers/hook.py +14 -7
  132. claude_mpm/services/socketio/server/main.py +12 -4
  133. claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
  134. claude_mpm/skills/bundled/pm/mpm-bug-reporting/SKILL.md +248 -0
  135. claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
  136. claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
  137. claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
  138. claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
  139. claude_mpm/skills/skill_manager.py +4 -4
  140. claude_mpm/utils/agent_dependency_loader.py +103 -4
  141. claude_mpm/utils/robust_installer.py +45 -24
  142. claude_mpm-5.4.96.dist-info/METADATA +377 -0
  143. {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/RECORD +153 -131
  144. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +0 -1
  145. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +0 -1
  146. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +0 -1
  147. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +0 -24
  148. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +0 -1
  149. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +0 -1
  150. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +0 -323
  151. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +0 -1
  152. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +0 -1
  153. claude_mpm-5.4.64.dist-info/METADATA +0 -999
  154. /claude_mpm/skills/bundled/pm/{pm-delegation-patterns → mpm-delegation-patterns}/SKILL.md +0 -0
  155. /claude_mpm/skills/bundled/pm/{pm-git-file-tracking → mpm-git-file-tracking}/SKILL.md +0 -0
  156. /claude_mpm/skills/bundled/pm/{pm-pr-workflow → mpm-pr-workflow}/SKILL.md +0 -0
  157. /claude_mpm/skills/bundled/pm/{pm-ticketing-integration → mpm-ticketing-integration}/SKILL.md +0 -0
  158. /claude_mpm/skills/bundled/pm/{pm-verification-protocols → mpm-verification-protocols}/SKILL.md +0 -0
  159. {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/WHEEL +0 -0
  160. {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/entry_points.txt +0 -0
  161. {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/licenses/LICENSE +0 -0
  162. {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  163. {claude_mpm-5.4.64.dist-info → claude_mpm-5.4.96.dist-info}/top_level.txt +0 -0
claude_mpm/cli/startup.py CHANGED
@@ -191,7 +191,8 @@ def should_skip_background_services(args, processed_argv):
191
191
  skip_commands = ["--version", "-v", "--help", "-h"]
192
192
  return any(cmd in (processed_argv or sys.argv[1:]) for cmd in skip_commands) or (
193
193
  hasattr(args, "command")
194
- and args.command in ["info", "doctor", "config", "mcp", "configure"]
194
+ and args.command
195
+ in ["info", "doctor", "config", "mcp", "configure", "hook-errors", "autotodos"]
195
196
  )
196
197
 
197
198
 
@@ -234,7 +235,7 @@ def deploy_bundled_skills():
234
235
  if not skills_config.get("auto_deploy", True):
235
236
  # Auto-deploy disabled, skip silently
236
237
  return
237
- except Exception:
238
+ except Exception: # nosec B110
238
239
  # If config loading fails, assume auto-deploy is enabled (default)
239
240
  pass
240
241
 
@@ -308,68 +309,60 @@ def deploy_output_style_on_startup():
308
309
  communication without emojis and exclamation points. Styles are project-specific
309
310
  to allow different projects to have different communication styles.
310
311
 
311
- DESIGN DECISION: This is non-blocking and idempotent. Deploys to project-level
312
- directory (.claude/settings/output-styles/) instead of user-level to maintain
313
- project isolation.
312
+ DESIGN DECISION: This is non-blocking and idempotent. Deploys to user-level
313
+ directory (~/.claude/output-styles/) which is the official Claude Code location
314
+ for custom output styles.
314
315
 
315
- Deploys two styles:
316
- - claude-mpm-style.md (professional mode)
316
+ Deploys all styles:
317
+ - claude-mpm.md (professional mode)
317
318
  - claude-mpm-teacher.md (teaching mode)
319
+ - claude-mpm-founders.md (founders mode)
318
320
  """
319
321
  try:
320
- import shutil
321
- from pathlib import Path
322
+ from ..core.output_style_manager import OutputStyleManager
322
323
 
323
- # Source files (in framework package)
324
- package_dir = Path(__file__).parent.parent / "agents"
325
- professional_source = package_dir / "CLAUDE_MPM_OUTPUT_STYLE.md"
326
- teacher_source = package_dir / "CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md"
327
-
328
- # Target directory (USER-LEVEL for global availability)
329
- # Claude Code reads output styles from ~/.claude/settings/output-styles/
330
- user_home = Path.home()
331
- output_styles_dir = user_home / ".claude" / "settings" / "output-styles"
332
- professional_target = output_styles_dir / "claude-mpm-style.md"
333
- teacher_target = output_styles_dir / "claude-mpm-teacher.md"
334
-
335
- # Create directory if it doesn't exist
336
- output_styles_dir.mkdir(parents=True, exist_ok=True)
337
-
338
- # Check if already deployed AND up-to-date (compare sizes to detect changes)
339
- professional_up_to_date = (
340
- professional_target.exists()
341
- and professional_source.exists()
342
- and professional_target.stat().st_size == professional_source.stat().st_size
343
- )
344
- teacher_up_to_date = (
345
- teacher_target.exists()
346
- and teacher_source.exists()
347
- and teacher_target.stat().st_size == teacher_source.stat().st_size
348
- )
324
+ # Initialize the output style manager
325
+ manager = OutputStyleManager()
326
+
327
+ # Check if Claude Code version supports output styles (>= 1.0.83)
328
+ if not manager.supports_output_styles():
329
+ # Skip deployment for older versions
330
+ # The manager will fall back to injecting content directly
331
+ return
349
332
 
350
- if professional_up_to_date and teacher_up_to_date:
333
+ # Check if all styles are already deployed and up-to-date
334
+ all_up_to_date = True
335
+ for style_config in manager.styles.values():
336
+ source_path = style_config["source"]
337
+ target_path = style_config["target"]
338
+
339
+ if not (
340
+ target_path.exists()
341
+ and source_path.exists()
342
+ and target_path.stat().st_size == source_path.stat().st_size
343
+ ):
344
+ all_up_to_date = False
345
+ break
346
+
347
+ if all_up_to_date:
351
348
  # Show feedback that output styles are ready
352
349
  print("✓ Output styles ready", flush=True)
353
350
  return
354
351
 
355
- # Deploy both styles
356
- deployed_count = 0
357
- if professional_source.exists():
358
- shutil.copy2(professional_source, professional_target)
359
- deployed_count += 1
352
+ # Deploy all styles using the manager
353
+ results = manager.deploy_all_styles(activate_default=True)
360
354
 
361
- if teacher_source.exists():
362
- shutil.copy2(teacher_source, teacher_target)
363
- deployed_count += 1
355
+ # Count successful deployments
356
+ deployed_count = sum(1 for success in results.values() if success)
364
357
 
365
358
  if deployed_count > 0:
366
359
  print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
367
360
  else:
368
- # Source files missing - log but don't fail
361
+ # Deployment failed - log but don't fail startup
369
362
  from ..core.logger import get_logger
370
363
 
371
364
  logger = get_logger("cli")
372
- logger.debug("Output style source files not found")
365
+ logger.debug("Failed to deploy any output styles")
373
366
 
374
367
  except Exception as e:
375
368
  # Non-critical - log but don't fail startup
@@ -458,7 +451,7 @@ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) ->
458
451
  return removed_count
459
452
 
460
453
 
461
- def sync_remote_agents_on_startup():
454
+ def sync_remote_agents_on_startup(force_sync: bool = False):
462
455
  """
463
456
  Synchronize agent templates from remote sources on startup.
464
457
 
@@ -471,16 +464,16 @@ def sync_remote_agents_on_startup():
471
464
  block startup to ensure claude-mpm remains functional.
472
465
 
473
466
  Workflow:
474
- 1. Cleanup legacy agent cache directories (if any)
475
- 2. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
476
- 3. Deploy agents to ~/.claude/agents/ - Phase 2 progress bar
477
- 4. Cleanup orphaned agents (ours but no longer deployed) - Phase 3
467
+ 1. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
468
+ 2. Deploy agents to ~/.claude/agents/ - Phase 2 progress bar
469
+ 3. Cleanup orphaned agents (ours but no longer deployed) - Phase 3
470
+ 4. Cleanup legacy agent cache directories (after sync/deployment) - Phase 4
478
471
  5. Log deployment results
479
- """
480
- # Cleanup legacy cache directories first (before syncing)
481
- cleanup_legacy_agent_cache()
482
472
 
483
- # DEPRECATED: Legacy warning - replaced by automatic cleanup above
473
+ Args:
474
+ force_sync: Force download even if cache is fresh (bypasses ETag).
475
+ """
476
+ # DEPRECATED: Legacy warning - no-op function, kept for compatibility
484
477
  check_legacy_cache()
485
478
 
486
479
  try:
@@ -489,7 +482,6 @@ def sync_remote_agents_on_startup():
489
482
  from pathlib import Path
490
483
 
491
484
  from ..core.shared.config_loader import ConfigLoader
492
- from ..services.agents.deployment.agent_deployment import AgentDeploymentService
493
485
  from ..services.agents.startup_sync import sync_agents_on_startup
494
486
  from ..services.profile_manager import ProfileManager
495
487
  from ..utils.progress import ProgressBar
@@ -514,7 +506,7 @@ def sync_remote_agents_on_startup():
514
506
  )
515
507
 
516
508
  # Phase 1: Sync files from Git sources
517
- result = sync_agents_on_startup()
509
+ result = sync_agents_on_startup(force_refresh=force_sync)
518
510
 
519
511
  # Only proceed with deployment if sync was enabled and ran
520
512
  if result.get("enabled") and result.get("sources_synced", 0) > 0:
@@ -537,304 +529,89 @@ def sync_remote_agents_on_startup():
537
529
  logger.warning(f"Agent sync completed with {len(errors)} errors")
538
530
 
539
531
  # Phase 2: Deploy agents from cache to ~/.claude/agents/
540
- # This mirrors the skills deployment pattern (lines 371-407)
532
+ # Use reconciliation service to respect configuration.yaml settings
541
533
  try:
542
- # Initialize deployment service with profile-filtered configuration
543
- from ..core.config import Config
544
-
545
- deploy_config = None
546
- if active_profile and profile_manager.active_profile:
547
- # Create config with excluded agents based on profile
548
- # Get all agents that should be excluded (not in enabled list)
549
- from pathlib import Path
550
-
551
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
552
- if cache_dir.exists():
553
- # Find all agent files
554
- # Supports both flat cache and {owner}/{repo}/agents/ structure
555
- all_agent_files = [
556
- f
557
- for f in cache_dir.rglob("*.md")
558
- if "/agents/" in str(f)
559
- and f.stem.lower() != "base-agent"
560
- and f.name.lower()
561
- not in {"readme.md", "changelog.md", "contributing.md"}
562
- ]
563
-
564
- # Build exclusion list for agents not in profile
565
- excluded_agents = []
566
- for agent_file in all_agent_files:
567
- agent_name = agent_file.stem
568
- if not profile_manager.is_agent_enabled(agent_name):
569
- excluded_agents.append(agent_name)
570
-
571
- if excluded_agents:
572
- # Get singleton config and update with profile settings
573
- # BUGFIX: Config is a singleton that ignores dict parameter if already initialized.
574
- # Creating Config({...}) doesn't store excluded_agents - use set() instead.
575
- deploy_config = Config()
576
- deploy_config.set(
577
- "agent_deployment.excluded_agents", excluded_agents
578
- )
579
- deploy_config.set(
580
- "agent_deployment.filter_non_mpm_agents", False
581
- )
582
- deploy_config.set("agent_deployment.case_sensitive", False)
583
- deploy_config.set(
584
- "agent_deployment.exclude_dependencies", False
585
- )
586
- logger.info(
587
- f"Profile '{active_profile}': Excluding {len(excluded_agents)} agents from deployment"
588
- )
589
-
590
- deployment_service = AgentDeploymentService(config=deploy_config)
591
-
592
- # Count agents in cache to show accurate progress
593
534
  from pathlib import Path
594
535
 
595
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
596
- agent_count = 0
597
-
598
- if cache_dir.exists():
599
- # BUGFIX (cache-count-inflation): Clean up stale cache files
600
- # from old repositories before counting to prevent inflated counts.
601
- # Issue: Old caches like bobmatnyc/claude-mpm-agents/agents/
602
- # were counted alongside current agents, inflating count
603
- # from 44 to 85.
604
- #
605
- # Solution: Remove files with nested /agents/ paths
606
- # (e.g., cache/agents/user/repo/agents/...)
607
- # Keep only current agents (e.g., cache/agents/engineer/...)
608
- removed_count = 0
609
- stale_dirs = set()
610
-
611
- for md_file in cache_dir.rglob("*.md"):
612
- # Stale cache files have multiple /agents/ in their path RELATIVE to cache_dir
613
- # Current: cache/agents/bobmatnyc/claude-mpm-agents/agents/engineer/...
614
- # (1 occurrence in relative path: /agents/)
615
- # Old flat: cache/agents/engineer/...
616
- # (0 occurrences in relative path - no repo structure)
617
- # The issue: str(md_file).count("/agents/") counts BOTH cache/agents/ AND repo/agents/
618
- # Fix: Count /agents/ in path RELATIVE to cache_dir (after cache/agents/)
619
- relative_path = str(md_file.relative_to(cache_dir))
620
- if relative_path.count("/agents/") > 1:
621
- # Track parent directory for cleanup
622
- # Extract subdirectory under cache/agents/
623
- # (e.g., "bobmatnyc")
624
- parts = md_file.parts
625
- cache_agents_idx = parts.index("agents")
626
- if cache_agents_idx + 1 < len(parts):
627
- stale_subdir = parts[cache_agents_idx + 1]
628
- # Only remove if it's not a known category directory
629
- if stale_subdir not in [
630
- "engineer",
631
- "ops",
632
- "qa",
633
- "universal",
634
- "documentation",
635
- "claude-mpm",
636
- "security",
637
- ]:
638
- stale_dirs.add(cache_dir / stale_subdir)
639
-
640
- md_file.unlink()
641
- removed_count += 1
642
-
643
- # Remove empty stale directories
644
- for stale_dir in stale_dirs:
645
- if stale_dir.exists() and stale_dir.is_dir():
646
- try:
647
- # Remove directory and all contents
648
- import shutil
649
-
650
- shutil.rmtree(stale_dir)
651
- except Exception:
652
- pass # Ignore cleanup errors
653
-
654
- if removed_count > 0:
655
- from loguru import logger
656
-
657
- logger.info(
658
- f"Cleaned up {removed_count} stale cache files "
659
- f"from old repositories"
660
- )
536
+ from ..core.unified_config import UnifiedConfig
537
+ from ..services.agents.deployment.startup_reconciliation import (
538
+ perform_startup_reconciliation,
539
+ )
661
540
 
662
- # Count MD files in cache (agent markdown files from
663
- # current repos)
664
- # BUGFIX: Only count files in agent directories,
665
- # not docs/templates/READMEs
666
- # Valid agent paths must contain "/agents/" exactly ONCE
667
- # (current structure)
668
- # Exclude PM templates, BASE-AGENT, and documentation files
669
- pm_templates = {
670
- "base-agent.md",
671
- "circuit_breakers.md",
672
- "pm_examples.md",
673
- "pm_red_flags.md",
674
- "research_gate_examples.md",
675
- "response_format.md",
676
- "ticket_completeness_examples.md",
677
- "validation_templates.md",
678
- "git_file_tracking.md",
679
- }
680
- # Documentation files to exclude (by filename)
681
- doc_files = {
682
- "readme.md",
683
- "changelog.md",
684
- "contributing.md",
685
- "implementation-summary.md",
686
- "reorganization-plan.md",
687
- "auto-deploy-index.md",
688
- }
689
-
690
- # Find all markdown files (after cleanup)
691
- all_md_files = list(cache_dir.rglob("*.md"))
692
-
693
- # Filter to only agent files:
694
- # 1. Must have "/agents/" in path (current structure supports
695
- # both flat and {owner}/{repo}/agents/ patterns)
696
- # 2. Must not be in PM templates or doc files
697
- # 3. Exclude BASE-AGENT.md which is not a deployable agent
698
- # 4. Exclude build artifacts (dist/, build/, .cache/)
699
- # to prevent double-counting
700
- agent_files = [
701
- f
702
- for f in all_md_files
703
- if (
704
- # Must be in an agent directory
705
- # Supports: cache/agents/{category}/... (flat)
706
- # Supports: cache/agents/{owner}/{repo}/agents/{category}/... (GitHub sync)
707
- "/agents/" in str(f)
708
- # Exclude PM templates, doc files, and BASE-AGENT
709
- and f.name.lower() not in pm_templates
710
- and f.name.lower() not in doc_files
711
- and f.name.lower() != "base-agent.md"
712
- # Exclude build artifacts (prevents double-counting
713
- # source + built files)
714
- and not any(
715
- part in str(f).split("/")
716
- for part in ["dist", "build", ".cache"]
717
- )
718
- )
719
- ]
720
- agent_count = len(agent_files)
721
-
722
- if agent_count > 0:
723
- # Deploy agents to project-level directory where Claude Code expects them
724
- deploy_target = Path.cwd() / ".claude" / "agents"
725
- deployment_result = deployment_service.deploy_agents(
726
- target_dir=deploy_target,
727
- force_rebuild=False, # Only deploy if versions differ
728
- deployment_mode="update", # Version-aware updates
729
- config=deploy_config, # Pass config to respect profile filtering
541
+ # Load configuration
542
+ unified_config = UnifiedConfig()
543
+
544
+ # Override with profile settings if active
545
+ if active_profile and profile_manager.active_profile:
546
+ # Get enabled agents from profile (returns Set[str])
547
+ profile_enabled_agents = (
548
+ profile_manager.active_profile.get_enabled_agents()
549
+ )
550
+ # Update config with profile's enabled list (convert Set to List)
551
+ unified_config.agents.enabled = list(profile_enabled_agents)
552
+ logger.info(
553
+ f"Profile '{active_profile}': Using {len(profile_enabled_agents)} enabled agents"
730
554
  )
731
555
 
732
- # Get actual counts from deployment result (reflects configured agents)
733
- deployed = len(deployment_result.get("deployed", []))
734
- updated = len(deployment_result.get("updated", []))
735
- skipped = len(deployment_result.get("skipped", []))
736
- total_configured = deployed + updated + skipped
737
-
738
- # FALLBACK: If deployment result doesn't track skipped agents (async path),
739
- # count existing agents in target directory as "already deployed"
740
- # This ensures accurate reporting when agents are already up-to-date
741
- if total_configured == 0 and deploy_target.exists():
742
- existing_agents = list(deploy_target.glob("*.md"))
743
- # Filter out non-agent files (e.g., README.md, INSTRUCTIONS.md)
744
- agent_count_in_target = len(
745
- [
746
- f
747
- for f in existing_agents
748
- if not f.name.startswith(("README", "INSTRUCTIONS"))
749
- ]
750
- )
751
- if agent_count_in_target > 0:
752
- # All agents already deployed - count them as skipped
753
- skipped = agent_count_in_target
754
- total_configured = agent_count_in_target
556
+ # Perform reconciliation to deploy configured agents
557
+ project_path = Path.cwd()
558
+ agent_result, _skill_result = perform_startup_reconciliation(
559
+ project_path=project_path, config=unified_config, silent=False
560
+ )
561
+
562
+ # Display results with progress bar
563
+ total_operations = (
564
+ len(agent_result.deployed)
565
+ + len(agent_result.removed)
566
+ + len(agent_result.unchanged)
567
+ )
755
568
 
756
- # Create progress bar with actual configured agent count (not raw file count)
569
+ if total_operations > 0:
757
570
  deploy_progress = ProgressBar(
758
- total=total_configured if total_configured > 0 else 1,
571
+ total=total_operations,
759
572
  prefix="Deploying agents",
760
573
  show_percentage=True,
761
574
  show_counter=True,
762
575
  )
763
-
764
- # Update progress bar to completion
765
- deploy_progress.update(
766
- total_configured if total_configured > 0 else 1
767
- )
768
-
769
- # Cleanup orphaned agents (ours but no longer deployed)
770
- # Get list of deployed agent filenames (what should remain)
771
- deployed_filenames = []
772
- for agent_name in deployment_result.get("deployed", []):
773
- deployed_filenames.append(f"{agent_name}.md")
774
- for agent_name in deployment_result.get("updated", []):
775
- deployed_filenames.append(f"{agent_name}.md")
776
- for agent_name in deployment_result.get("skipped", []):
777
- deployed_filenames.append(f"{agent_name}.md")
778
-
779
- # Run cleanup and get count of removed agents
780
- removed = _cleanup_orphaned_agents(
781
- deploy_target, deployed_filenames
576
+ deploy_progress.update(total_operations)
577
+
578
+ # Build summary message
579
+ deployed = len(agent_result.deployed)
580
+ removed = len(agent_result.removed)
581
+ unchanged = len(agent_result.unchanged)
582
+
583
+ summary_parts = []
584
+ if deployed > 0:
585
+ summary_parts.append(f"{deployed} new")
586
+ if removed > 0:
587
+ summary_parts.append(f"{removed} removed")
588
+ if unchanged > 0:
589
+ summary_parts.append(f"{unchanged} unchanged")
590
+
591
+ summary = f"Complete: {', '.join(summary_parts)}"
592
+ deploy_progress.finish(summary)
593
+
594
+ # Display errors if any
595
+ if agent_result.errors:
596
+ logger.warning(
597
+ f"Agent deployment completed with {len(agent_result.errors)} errors"
782
598
  )
599
+ print("\n⚠️ Agent Deployment Errors:")
600
+ max_errors_to_show = 10
601
+ errors_to_display = agent_result.errors[:max_errors_to_show]
783
602
 
784
- # Show total configured agents (deployed + updated + already existing)
785
- # Include cache count for context and removed count if any
786
- if deployed > 0 or updated > 0:
787
- if removed > 0:
788
- deploy_progress.finish(
789
- f"Complete: {deployed} new, {updated} updated, {skipped} unchanged, "
790
- f"{removed} removed ({total_configured} configured from {agent_count} files in cache)"
791
- )
792
- else:
793
- deploy_progress.finish(
794
- f"Complete: {deployed} new, {updated} updated, {skipped} unchanged "
795
- f"({total_configured} configured from {agent_count} files in cache)"
796
- )
797
- elif removed > 0:
798
- deploy_progress.finish(
799
- f"Complete: {total_configured} agents deployed, "
800
- f"{removed} removed ({agent_count} files in cache)"
801
- )
802
- else:
803
- deploy_progress.finish(
804
- f"Complete: {total_configured} agents deployed "
805
- f"({agent_count} files in cache)"
806
- )
807
-
808
- # Display deployment errors to user (not just logs)
809
- deploy_errors = deployment_result.get("errors", [])
810
- if deploy_errors:
811
- # Log for debugging
812
- logger.warning(
813
- f"Agent deployment completed with {len(deploy_errors)} errors: {deploy_errors}"
814
- )
815
-
816
- # Display errors to user with clear formatting
817
- print("\n⚠️ Agent Deployment Errors:")
603
+ for error in errors_to_display:
604
+ print(f" - {error}")
818
605
 
819
- # Show first 10 errors to avoid overwhelming output
820
- max_errors_to_show = 10
821
- errors_to_display = deploy_errors[:max_errors_to_show]
606
+ if len(agent_result.errors) > max_errors_to_show:
607
+ remaining = len(agent_result.errors) - max_errors_to_show
608
+ print(f" ... and {remaining} more error(s)")
822
609
 
823
- for error in errors_to_display:
824
- # Format error message for readability
825
- # Errors typically come as strings like "agent.md: Error message"
826
- print(f" - {error}")
827
-
828
- # If more errors exist, show count
829
- if len(deploy_errors) > max_errors_to_show:
830
- remaining = len(deploy_errors) - max_errors_to_show
831
- print(f" ... and {remaining} more error(s)")
832
-
833
- # Show summary message
834
- print(
835
- f"\n❌ Failed to deploy {len(deploy_errors)} agent(s). Please check the error messages above."
836
- )
837
- print(" Run with --verbose for detailed error information.\n")
610
+ print(
611
+ f"\n❌ Failed to deploy {len(agent_result.errors)} agent(s). "
612
+ "Please check the error messages above."
613
+ )
614
+ print(" Run with --verbose for detailed error information.\n")
838
615
 
839
616
  except Exception as e:
840
617
  # Deployment failure shouldn't block startup
@@ -843,6 +620,11 @@ def sync_remote_agents_on_startup():
843
620
  logger = get_logger("cli")
844
621
  logger.warning(f"Failed to deploy agents from cache: {e}")
845
622
 
623
+ # Phase 4: Cleanup legacy agent cache directories (after sync/deployment)
624
+ # CRITICAL: This must run AFTER sync completes because sync may recreate
625
+ # legacy directories. Running cleanup here ensures they're removed.
626
+ cleanup_legacy_agent_cache()
627
+
846
628
  except Exception as e:
847
629
  # Non-critical - log but don't fail startup
848
630
  from ..core.logger import get_logger
@@ -851,8 +633,14 @@ def sync_remote_agents_on_startup():
851
633
  logger.debug(f"Failed to sync remote agents: {e}")
852
634
  # Continue execution - agent sync failure shouldn't block startup
853
635
 
636
+ # Cleanup legacy cache even if sync failed
637
+ try:
638
+ cleanup_legacy_agent_cache()
639
+ except Exception: # nosec B110
640
+ pass # Ignore cleanup errors
641
+
854
642
 
855
- def sync_remote_skills_on_startup():
643
+ def sync_remote_skills_on_startup(force_sync: bool = False):
856
644
  """
857
645
  Synchronize skill templates from remote sources on startup.
858
646
 
@@ -870,6 +658,9 @@ def sync_remote_skills_on_startup():
870
658
  4. Apply profile filtering if active
871
659
  5. Deploy resolved skills to ~/.claude/skills/ - Phase 2 progress bar
872
660
  6. Log deployment results with source indication
661
+
662
+ Args:
663
+ force_sync: Force download even if cache is fresh (bypasses ETag).
873
664
  """
874
665
  try:
875
666
  from pathlib import Path
@@ -975,7 +766,7 @@ def sync_remote_skills_on_startup():
975
766
 
976
767
  # Sync all sources with progress callback
977
768
  results = manager.sync_all_sources(
978
- force=False, progress_callback=sync_progress.update
769
+ force=force_sync, progress_callback=sync_progress.update
979
770
  )
980
771
 
981
772
  # Finish sync progress bar with clear breakdown
@@ -1073,9 +864,7 @@ def sync_remote_skills_on_startup():
1073
864
  total_skill_count = len(all_skills)
1074
865
 
1075
866
  # Determine skill count based on resolution
1076
- skill_count = (
1077
- len(skills_to_deploy) if skills_to_deploy else total_skill_count
1078
- )
867
+ skill_count = len(skills_to_deploy) if skills_to_deploy else total_skill_count
1079
868
 
1080
869
  if skill_count > 0:
1081
870
  # Deploy skills with resolved filter
@@ -1086,11 +875,13 @@ def sync_remote_skills_on_startup():
1086
875
  # Deploy to project-local directory with cleanup
1087
876
  deployment_result = manager.deploy_skills(
1088
877
  target_dir=Path.cwd() / ".claude" / "skills",
1089
- force=False,
878
+ force=force_sync,
1090
879
  # CRITICAL FIX: Empty list should mean "deploy no skills", not "deploy all"
1091
880
  # When skills_to_deploy is [], we want skill_filter=set() NOT skill_filter=None
1092
881
  # None means "no filtering" (deploy all), empty set means "filter to nothing"
1093
- skill_filter=set(skills_to_deploy) if skills_to_deploy is not None else None,
882
+ skill_filter=set(skills_to_deploy)
883
+ if skills_to_deploy is not None
884
+ else None,
1094
885
  )
1095
886
 
1096
887
  # REMOVED: User-level deployment (lines 1068-1074)
@@ -1349,33 +1140,70 @@ def show_skill_summary():
1349
1140
 
1350
1141
 
1351
1142
  def verify_and_show_pm_skills():
1352
- """Verify PM skills and display status.
1143
+ """Verify PM skills and display status with enhanced validation.
1144
+
1145
+ WHY: PM skills are CRITICAL for PM agent operation. PM must KNOW if
1146
+ framework knowledge is unavailable at startup. Enhanced validation
1147
+ checks all required skills exist, are not corrupted, and auto-repairs
1148
+ if needed.
1353
1149
 
1354
- WHY: PM skills are essential for PM agent operation.
1355
- Shows deployment status and auto-deploys if missing.
1150
+ Shows deployment status:
1151
+ - "✓ PM skills: 8/8 verified" if all required skills are valid
1152
+ - "⚠ PM skills: 2 missing, auto-repairing..." if issues detected
1153
+ - Non-blocking but visible warning if auto-repair fails
1356
1154
  """
1357
1155
  try:
1358
1156
  from pathlib import Path
1359
1157
 
1360
- from ..services.pm_skills_deployer import PMSkillsDeployerService
1158
+ from ..services.pm_skills_deployer import (
1159
+ REQUIRED_PM_SKILLS,
1160
+ PMSkillsDeployerService,
1161
+ )
1361
1162
 
1362
1163
  deployer = PMSkillsDeployerService()
1363
1164
  project_dir = Path.cwd()
1364
1165
 
1365
- result = deployer.verify_pm_skills(project_dir)
1166
+ # Verify with auto-repair enabled
1167
+ result = deployer.verify_pm_skills(project_dir, auto_repair=True)
1366
1168
 
1367
1169
  if result.verified:
1368
- # Show verified status
1369
- print(f"✓ PM skills: {result.skill_count} verified", flush=True)
1170
+ # Show verified status with count
1171
+ total_required = len(REQUIRED_PM_SKILLS)
1172
+ print(
1173
+ f"✓ PM skills: {total_required}/{total_required} verified", flush=True
1174
+ )
1370
1175
  else:
1371
- # Auto-deploy if missing
1372
- print("Deploying PM skills...", end="", flush=True)
1373
- deploy_result = deployer.deploy_pm_skills(project_dir)
1374
- if deploy_result.success:
1375
- total = len(deploy_result.deployed) + len(deploy_result.skipped)
1376
- print(f"\r✓ PM skills: {total} deployed" + " " * 20, flush=True)
1176
+ # Show warning with details
1177
+ missing_count = len(result.missing_skills)
1178
+ corrupted_count = len(result.corrupted_skills)
1179
+
1180
+ # Build status message
1181
+ issues = []
1182
+ if missing_count > 0:
1183
+ issues.append(f"{missing_count} missing")
1184
+ if corrupted_count > 0:
1185
+ issues.append(f"{corrupted_count} corrupted")
1186
+
1187
+ status = ", ".join(issues)
1188
+
1189
+ # Check if auto-repair was attempted
1190
+ if "Auto-repaired" in result.message:
1191
+ # Auto-repair succeeded
1192
+ total_required = len(REQUIRED_PM_SKILLS)
1193
+ print(
1194
+ f"✓ PM skills: {total_required}/{total_required} verified (auto-repaired)",
1195
+ flush=True,
1196
+ )
1377
1197
  else:
1378
- print("\r⚠ PM skills: deployment failed" + " " * 20, flush=True)
1198
+ # Auto-repair failed or not attempted
1199
+ print(f"⚠ PM skills: {status}", flush=True)
1200
+
1201
+ # Log warnings for debugging
1202
+ from ..core.logger import get_logger
1203
+
1204
+ logger = get_logger("cli")
1205
+ for warning in result.warnings:
1206
+ logger.warning(f"PM skills: {warning}")
1379
1207
 
1380
1208
  except ImportError:
1381
1209
  # PM skills deployer not available - skip silently
@@ -1410,7 +1238,7 @@ def auto_install_chrome_devtools_on_startup():
1410
1238
  if not chrome_devtools_config.get("auto_install", True):
1411
1239
  # Auto-install disabled, skip silently
1412
1240
  return
1413
- except Exception:
1241
+ except Exception: # nosec B110
1414
1242
  # If config loading fails, assume auto-install is enabled (default)
1415
1243
  pass
1416
1244
 
@@ -1428,7 +1256,7 @@ def auto_install_chrome_devtools_on_startup():
1428
1256
  # Continue execution - chrome-devtools installation failure shouldn't block startup
1429
1257
 
1430
1258
 
1431
- def run_background_services():
1259
+ def run_background_services(force_sync: bool = False):
1432
1260
  """
1433
1261
  Initialize all background services on startup.
1434
1262
 
@@ -1439,6 +1267,9 @@ def run_background_services():
1439
1267
  explicitly requests them via agent-manager commands. This prevents unwanted
1440
1268
  file creation in project .claude/ directories.
1441
1269
  See: SystemInstructionsDeployer and agent_deployment.py line 504-509
1270
+
1271
+ Args:
1272
+ force_sync: Force download even if cache is fresh (bypasses ETag).
1442
1273
  """
1443
1274
  # Sync hooks early to ensure up-to-date configuration
1444
1275
  # RATIONALE: Hooks should be synced before other services to fix stale configs
@@ -1449,7 +1280,9 @@ def run_background_services():
1449
1280
  check_mcp_auto_configuration()
1450
1281
  verify_mcp_gateway_startup()
1451
1282
  check_for_updates_async()
1452
- sync_remote_agents_on_startup() # Sync agents from remote sources
1283
+ sync_remote_agents_on_startup(
1284
+ force_sync=force_sync
1285
+ ) # Sync agents from remote sources
1453
1286
  show_agent_summary() # Display agent counts after deployment
1454
1287
 
1455
1288
  # Skills deployment order (precedence: remote > bundled)
@@ -1458,7 +1291,9 @@ def run_background_services():
1458
1291
  # 3. Discover and link runtime skills (user-added skills)
1459
1292
  # This ensures remote skills take precedence over bundled skills when names conflict
1460
1293
  deploy_bundled_skills() # Base layer: package-bundled skills
1461
- sync_remote_skills_on_startup() # Override layer: Git-based skills (takes precedence)
1294
+ sync_remote_skills_on_startup(
1295
+ force_sync=force_sync
1296
+ ) # Override layer: Git-based skills (takes precedence)
1462
1297
  discover_and_link_runtime_skills() # Discovery: user-added skills
1463
1298
  show_skill_summary() # Display skill counts after deployment
1464
1299
  verify_and_show_pm_skills() # PM skills verification and status
@@ -1658,7 +1493,7 @@ def verify_mcp_gateway_startup():
1658
1493
  loop.run_until_complete(
1659
1494
  asyncio.gather(*pending, return_exceptions=True)
1660
1495
  )
1661
- except Exception:
1496
+ except Exception: # nosec B110
1662
1497
  pass # Ignore cleanup errors
1663
1498
  finally:
1664
1499
  loop.close()
@@ -1752,7 +1587,7 @@ def check_for_updates_async():
1752
1587
 
1753
1588
  logger = get_logger("upgrade_check")
1754
1589
  logger.debug(f"Update check failed (non-critical): {e}")
1755
- except Exception:
1590
+ except Exception: # nosec B110
1756
1591
  pass # Avoid any errors in error handling
1757
1592
  finally:
1758
1593
  # Properly clean up event loop
@@ -1767,7 +1602,7 @@ def check_for_updates_async():
1767
1602
  loop.run_until_complete(
1768
1603
  asyncio.gather(*pending, return_exceptions=True)
1769
1604
  )
1770
- except Exception:
1605
+ except Exception: # nosec B110
1771
1606
  pass # Ignore cleanup errors
1772
1607
  finally:
1773
1608
  loop.close()