claude-mpm 4.20.3__py3-none-any.whl → 5.1.8__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_PM.md +35 -6
- claude_mpm/agents/OUTPUT_STYLE.md +3 -48
- claude_mpm/agents/PM_INSTRUCTIONS.md +1241 -667
- claude_mpm/agents/PM_INSTRUCTIONS_TEACH.md +1322 -0
- claude_mpm/agents/WORKFLOW.md +75 -2
- claude_mpm/agents/__init__.py +6 -0
- claude_mpm/agents/agent_loader.py +1 -4
- claude_mpm/agents/base_agent.json +6 -3
- claude_mpm/agents/base_agent_loader.py +10 -35
- claude_mpm/agents/frontmatter_validator.py +1 -1
- claude_mpm/agents/templates/circuit-breakers.md +1254 -0
- claude_mpm/agents/templates/context-management-examples.md +544 -0
- claude_mpm/agents/templates/{pm_red_flags.md → pm-red-flags.md} +89 -19
- claude_mpm/agents/templates/pr-workflow-examples.md +427 -0
- claude_mpm/agents/templates/research-gate-examples.md +669 -0
- claude_mpm/agents/templates/structured-questions-examples.md +615 -0
- claude_mpm/agents/templates/ticket-completeness-examples.md +139 -0
- claude_mpm/agents/templates/ticketing-examples.md +277 -0
- claude_mpm/cli/__init__.py +37 -2
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_source.py +774 -0
- claude_mpm/cli/commands/agent_state_manager.py +188 -30
- claude_mpm/cli/commands/agents.py +959 -36
- claude_mpm/cli/commands/agents_cleanup.py +210 -0
- claude_mpm/cli/commands/agents_discover.py +338 -0
- claude_mpm/cli/commands/aggregate.py +1 -1
- claude_mpm/cli/commands/analyze.py +3 -3
- claude_mpm/cli/commands/auto_configure.py +537 -239
- claude_mpm/cli/commands/cleanup.py +1 -1
- claude_mpm/cli/commands/config.py +7 -4
- claude_mpm/cli/commands/configure.py +924 -45
- claude_mpm/cli/commands/configure_agent_display.py +4 -4
- claude_mpm/cli/commands/configure_navigation.py +63 -46
- claude_mpm/cli/commands/debug.py +12 -12
- claude_mpm/cli/commands/doctor.py +10 -2
- claude_mpm/cli/commands/hook_errors.py +277 -0
- claude_mpm/cli/commands/local_deploy.py +1 -4
- claude_mpm/cli/commands/mcp_install_commands.py +1 -1
- claude_mpm/cli/commands/mpm_init/__init__.py +73 -0
- claude_mpm/cli/commands/mpm_init/core.py +573 -0
- claude_mpm/cli/commands/mpm_init/display.py +341 -0
- claude_mpm/cli/commands/mpm_init/git_activity.py +427 -0
- claude_mpm/cli/commands/mpm_init/modes.py +397 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +442 -0
- claude_mpm/cli/commands/mpm_init_cli.py +396 -0
- claude_mpm/cli/commands/mpm_init_handler.py +67 -1
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/run.py +125 -167
- claude_mpm/cli/commands/skill_source.py +694 -0
- claude_mpm/cli/commands/skills.py +835 -44
- claude_mpm/cli/executor.py +78 -3
- claude_mpm/cli/interactive/agent_wizard.py +1032 -47
- claude_mpm/cli/parsers/agent_source_parser.py +171 -0
- claude_mpm/cli/parsers/agents_parser.py +256 -4
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
- claude_mpm/cli/parsers/base_parser.py +53 -0
- claude_mpm/cli/parsers/config_parser.py +96 -43
- claude_mpm/cli/parsers/mpm_init_parser.py +42 -0
- claude_mpm/cli/parsers/skill_source_parser.py +169 -0
- claude_mpm/cli/parsers/skills_parser.py +145 -0
- claude_mpm/cli/parsers/source_parser.py +138 -0
- claude_mpm/cli/startup.py +564 -108
- claude_mpm/cli/startup_display.py +480 -0
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/cli_module/commands.py +1 -1
- claude_mpm/commands/{mpm-auto-configure.md → mpm-agents-auto-configure.md} +9 -0
- claude_mpm/commands/mpm-agents-detect.md +9 -0
- claude_mpm/commands/{mpm-agents.md → mpm-agents-list.md} +9 -0
- claude_mpm/commands/mpm-agents-recommend.md +9 -0
- claude_mpm/commands/{mpm-config.md → mpm-config-view.md} +9 -0
- claude_mpm/commands/mpm-doctor.md +9 -0
- claude_mpm/commands/mpm-help.md +17 -2
- claude_mpm/commands/mpm-init.md +28 -3
- claude_mpm/commands/mpm-monitor.md +9 -0
- claude_mpm/commands/mpm-postmortem.md +123 -0
- claude_mpm/commands/mpm-session-resume.md +381 -0
- claude_mpm/commands/mpm-status.md +9 -0
- claude_mpm/commands/{mpm-organize.md → mpm-ticket-organize.md} +9 -0
- claude_mpm/commands/mpm-ticket-view.md +552 -0
- claude_mpm/commands/mpm-version.md +9 -0
- claude_mpm/commands/mpm.md +11 -0
- claude_mpm/config/agent_presets.py +488 -0
- claude_mpm/config/agent_sources.py +325 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/config/skill_sources.py +590 -0
- claude_mpm/constants.py +13 -0
- claude_mpm/core/api_validator.py +1 -1
- claude_mpm/core/claude_runner.py +19 -35
- claude_mpm/core/config.py +24 -0
- claude_mpm/core/constants.py +1 -1
- claude_mpm/core/framework/__init__.py +3 -16
- claude_mpm/core/framework/loaders/file_loader.py +54 -101
- claude_mpm/core/framework/loaders/instruction_loader.py +25 -5
- claude_mpm/core/framework/processors/metadata_processor.py +1 -1
- claude_mpm/core/hook_error_memory.py +381 -0
- claude_mpm/core/hook_manager.py +41 -2
- claude_mpm/core/interactive_session.py +131 -10
- claude_mpm/core/interfaces.py +56 -1
- claude_mpm/core/logger.py +3 -1
- claude_mpm/core/oneshot_session.py +110 -8
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/shared/singleton_manager.py +11 -4
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/dashboard/static/css/activity.css +69 -69
- claude_mpm/dashboard/static/css/connection-status.css +10 -10
- claude_mpm/dashboard/static/css/dashboard.css +15 -15
- claude_mpm/dashboard/static/js/components/activity-tree.js +178 -178
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +101 -101
- claude_mpm/dashboard/static/js/components/agent-inference.js +31 -31
- claude_mpm/dashboard/static/js/components/build-tracker.js +59 -59
- claude_mpm/dashboard/static/js/components/code-simple.js +107 -107
- claude_mpm/dashboard/static/js/components/connection-debug.js +101 -101
- claude_mpm/dashboard/static/js/components/diff-viewer.js +113 -113
- claude_mpm/dashboard/static/js/components/event-viewer.js +12 -12
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +57 -57
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +74 -74
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +6 -6
- claude_mpm/dashboard/static/js/components/file-viewer.js +42 -42
- claude_mpm/dashboard/static/js/components/module-viewer.js +27 -27
- claude_mpm/dashboard/static/js/components/session-manager.js +14 -14
- claude_mpm/dashboard/static/js/components/socket-manager.js +1 -1
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +14 -14
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +110 -110
- claude_mpm/dashboard/static/js/components/working-directory.js +8 -8
- claude_mpm/dashboard/static/js/connection-manager.js +76 -76
- claude_mpm/dashboard/static/js/dashboard.js +76 -58
- claude_mpm/dashboard/static/js/extension-error-handler.js +22 -22
- claude_mpm/dashboard/static/js/socket-client.js +138 -121
- claude_mpm/dashboard/templates/code_simple.html +23 -23
- claude_mpm/dashboard/templates/index.html +18 -18
- claude_mpm/experimental/cli_enhancements.py +1 -5
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +3 -1
- claude_mpm/hooks/claude_hooks/hook_handler.py +24 -7
- claude_mpm/hooks/claude_hooks/installer.py +45 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/failure_learning/__init__.py +2 -8
- claude_mpm/hooks/failure_learning/failure_detection_hook.py +1 -6
- claude_mpm/hooks/failure_learning/fix_detection_hook.py +1 -6
- claude_mpm/hooks/failure_learning/learning_extraction_hook.py +1 -6
- claude_mpm/hooks/kuzu_response_hook.py +1 -5
- claude_mpm/hooks/templates/pre_tool_use_simple.py +78 -0
- claude_mpm/hooks/templates/pre_tool_use_template.py +323 -0
- claude_mpm/models/git_repository.py +198 -0
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/scripts/start_activity_logging.py +3 -1
- claude_mpm/services/agents/agent_builder.py +45 -9
- claude_mpm/services/agents/agent_preset_service.py +238 -0
- claude_mpm/services/agents/agent_selection_service.py +484 -0
- claude_mpm/services/agents/auto_deploy_index_parser.py +569 -0
- claude_mpm/services/agents/cache_git_manager.py +621 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +126 -2
- claude_mpm/services/agents/deployment/agent_discovery_service.py +105 -73
- claude_mpm/services/agents/deployment/agent_format_converter.py +1 -1
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +1 -5
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +3 -3
- claude_mpm/services/agents/deployment/agent_restore_handler.py +1 -4
- claude_mpm/services/agents/deployment/agent_template_builder.py +236 -15
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +101 -15
- claude_mpm/services/agents/deployment/async_agent_deployment.py +2 -1
- claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -3
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +115 -15
- claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +2 -2
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +1 -4
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +363 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +2 -2
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +168 -46
- claude_mpm/services/agents/deployment/validation/deployment_validator.py +2 -2
- claude_mpm/services/agents/git_source_manager.py +629 -0
- claude_mpm/services/agents/loading/framework_agent_loader.py +9 -12
- claude_mpm/services/agents/local_template_manager.py +50 -10
- claude_mpm/services/agents/single_tier_deployment_service.py +696 -0
- claude_mpm/services/agents/sources/__init__.py +13 -0
- claude_mpm/services/agents/sources/agent_sync_state.py +516 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +1087 -0
- claude_mpm/services/agents/startup_sync.py +239 -0
- claude_mpm/services/agents/toolchain_detector.py +474 -0
- claude_mpm/services/analysis/__init__.py +25 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/cli/session_pause_manager.py +504 -0
- claude_mpm/services/cli/session_resume_helper.py +36 -16
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/command_deployment_service.py +200 -6
- claude_mpm/services/core/base.py +31 -11
- claude_mpm/services/core/interfaces/__init__.py +1 -3
- claude_mpm/services/core/interfaces/health.py +1 -4
- claude_mpm/services/core/interfaces.py +56 -1
- claude_mpm/services/core/models/__init__.py +2 -11
- claude_mpm/services/core/models/agent_config.py +3 -0
- claude_mpm/services/core/models/process.py +4 -0
- claude_mpm/services/diagnostics/checks/__init__.py +4 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +0 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +577 -0
- claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
- claude_mpm/services/diagnostics/checks/mcp_check.py +0 -1
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
- claude_mpm/services/diagnostics/checks/monitor_check.py +0 -1
- claude_mpm/services/diagnostics/checks/skill_sources_check.py +587 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +9 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +40 -10
- claude_mpm/services/diagnostics/models.py +21 -0
- claude_mpm/services/event_bus/direct_relay.py +3 -3
- claude_mpm/services/event_bus/event_bus.py +36 -3
- claude_mpm/services/event_bus/relay.py +23 -7
- claude_mpm/services/events/consumers/logging.py +1 -2
- claude_mpm/services/git/__init__.py +21 -0
- claude_mpm/services/git/git_operations_service.py +494 -0
- claude_mpm/services/github/__init__.py +21 -0
- claude_mpm/services/github/github_cli_service.py +397 -0
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -5
- claude_mpm/services/infrastructure/monitoring/aggregator.py +1 -6
- claude_mpm/services/infrastructure/monitoring/resources.py +1 -1
- claude_mpm/services/instructions/__init__.py +9 -0
- claude_mpm/services/instructions/instruction_cache_service.py +374 -0
- claude_mpm/services/local_ops/__init__.py +5 -13
- claude_mpm/services/local_ops/health_checks/__init__.py +1 -3
- claude_mpm/services/local_ops/health_manager.py +1 -4
- claude_mpm/services/local_ops/process_manager.py +1 -1
- claude_mpm/services/local_ops/resource_monitor.py +2 -2
- claude_mpm/services/mcp_config_manager.py +75 -145
- claude_mpm/services/mcp_gateway/auto_configure.py +31 -25
- claude_mpm/services/mcp_gateway/config/configuration.py +1 -1
- claude_mpm/services/mcp_gateway/core/process_pool.py +41 -26
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +1 -6
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -2
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +1 -1
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +26 -21
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +6 -2
- claude_mpm/services/mcp_service_verifier.py +6 -3
- claude_mpm/services/memory/failure_tracker.py +19 -4
- claude_mpm/services/memory/optimizer.py +1 -1
- claude_mpm/services/model/model_router.py +8 -9
- claude_mpm/services/monitor/daemon.py +29 -9
- claude_mpm/services/monitor/daemon_manager.py +96 -19
- claude_mpm/services/monitor/server.py +2 -2
- claude_mpm/services/native_agent_converter.py +356 -0
- claude_mpm/services/port_manager.py +1 -1
- claude_mpm/services/pr/__init__.py +14 -0
- claude_mpm/services/pr/pr_template_service.py +329 -0
- claude_mpm/services/project/documentation_manager.py +2 -1
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/project/toolchain_analyzer.py +3 -1
- claude_mpm/services/runner_configuration_service.py +17 -3
- claude_mpm/services/self_upgrade_service.py +165 -7
- claude_mpm/services/session_management_service.py +16 -4
- claude_mpm/services/skills/__init__.py +18 -0
- claude_mpm/services/skills/git_skill_source_manager.py +1169 -0
- claude_mpm/services/skills/skill_discovery_service.py +568 -0
- claude_mpm/services/skills_config.py +547 -0
- claude_mpm/services/skills_deployer.py +955 -0
- claude_mpm/services/socketio/handlers/connection.py +1 -1
- claude_mpm/services/socketio/handlers/git.py +2 -2
- claude_mpm/services/socketio/server/core.py +1 -4
- claude_mpm/services/socketio/server/main.py +1 -3
- claude_mpm/services/system_instructions_service.py +1 -3
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +0 -3
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +0 -1
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +1 -1
- claude_mpm/services/unified/deployment_strategies/vercel.py +1 -5
- claude_mpm/services/unified/unified_deployment.py +1 -5
- claude_mpm/services/version_control/conflict_resolution.py +6 -4
- claude_mpm/services/visualization/__init__.py +1 -5
- claude_mpm/services/visualization/mermaid_generator.py +2 -3
- claude_mpm/skills/__init__.py +3 -3
- claude_mpm/skills/agent_skills_injector.py +42 -49
- claude_mpm/skills/bundled/infrastructure/env-manager/scripts/validate_env.py +576 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +17 -10
- claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +92 -39
- claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +13 -12
- claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +5 -3
- claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +19 -12
- claude_mpm/skills/bundled/performance-profiling.md +6 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +6 -6
- claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +13 -9
- claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +8 -8
- claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +37 -15
- claude_mpm/skills/skills_registry.py +44 -48
- claude_mpm/skills/skills_service.py +117 -108
- claude_mpm/templates/questions/__init__.py +38 -0
- claude_mpm/templates/questions/base.py +193 -0
- claude_mpm/templates/questions/pr_strategy.py +311 -0
- claude_mpm/templates/questions/project_init.py +385 -0
- claude_mpm/templates/questions/ticket_mgmt.py +394 -0
- claude_mpm/tools/__main__.py +8 -8
- claude_mpm/tools/code_tree_analyzer/__init__.py +45 -0
- claude_mpm/tools/code_tree_analyzer/analysis.py +299 -0
- claude_mpm/tools/code_tree_analyzer/cache.py +131 -0
- claude_mpm/tools/code_tree_analyzer/core.py +380 -0
- claude_mpm/tools/code_tree_analyzer/discovery.py +403 -0
- claude_mpm/tools/code_tree_analyzer/events.py +168 -0
- claude_mpm/tools/code_tree_analyzer/gitignore.py +308 -0
- claude_mpm/tools/code_tree_analyzer/models.py +39 -0
- claude_mpm/tools/code_tree_analyzer/multilang_analyzer.py +224 -0
- claude_mpm/tools/code_tree_analyzer/python_analyzer.py +284 -0
- claude_mpm/utils/agent_dependency_loader.py +80 -13
- claude_mpm/utils/agent_filters.py +288 -0
- claude_mpm/utils/dependency_cache.py +3 -1
- claude_mpm/utils/gitignore.py +244 -0
- claude_mpm/utils/log_cleanup.py +3 -3
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/progress.py +387 -0
- claude_mpm/utils/robust_installer.py +3 -5
- claude_mpm/utils/structured_questions.py +619 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/METADATA +496 -65
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/RECORD +328 -416
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -17
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +0 -3
- claude_mpm/agents/templates/agent-manager.json +0 -273
- claude_mpm/agents/templates/agentic-coder-optimizer.json +0 -248
- claude_mpm/agents/templates/api_qa.json +0 -180
- claude_mpm/agents/templates/circuit_breakers.md +0 -638
- claude_mpm/agents/templates/clerk-ops.json +0 -235
- claude_mpm/agents/templates/code_analyzer.json +0 -101
- claude_mpm/agents/templates/content-agent.json +0 -358
- claude_mpm/agents/templates/dart_engineer.json +0 -307
- claude_mpm/agents/templates/data_engineer.json +0 -225
- claude_mpm/agents/templates/documentation.json +0 -211
- claude_mpm/agents/templates/engineer.json +0 -210
- claude_mpm/agents/templates/gcp_ops_agent.json +0 -253
- claude_mpm/agents/templates/golang_engineer.json +0 -270
- claude_mpm/agents/templates/imagemagick.json +0 -264
- claude_mpm/agents/templates/java_engineer.json +0 -346
- claude_mpm/agents/templates/local_ops_agent.json +0 -1840
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +0 -39
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250901_010124_142.md +0 -400
- claude_mpm/agents/templates/memory_manager.json +0 -158
- claude_mpm/agents/templates/nextjs_engineer.json +0 -285
- claude_mpm/agents/templates/ops.json +0 -185
- claude_mpm/agents/templates/php-engineer.json +0 -281
- claude_mpm/agents/templates/product_owner.json +0 -338
- claude_mpm/agents/templates/project_organizer.json +0 -140
- claude_mpm/agents/templates/prompt-engineer.json +0 -737
- claude_mpm/agents/templates/python_engineer.json +0 -387
- claude_mpm/agents/templates/qa.json +0 -242
- claude_mpm/agents/templates/react_engineer.json +0 -238
- claude_mpm/agents/templates/refactoring_engineer.json +0 -276
- claude_mpm/agents/templates/research.json +0 -188
- claude_mpm/agents/templates/ruby-engineer.json +0 -280
- claude_mpm/agents/templates/rust_engineer.json +0 -275
- claude_mpm/agents/templates/security.json +0 -202
- claude_mpm/agents/templates/svelte-engineer.json +0 -225
- claude_mpm/agents/templates/ticketing.json +0 -177
- claude_mpm/agents/templates/typescript_engineer.json +0 -285
- claude_mpm/agents/templates/vercel_ops_agent.json +0 -412
- claude_mpm/agents/templates/version_control.json +0 -157
- claude_mpm/agents/templates/web_qa.json +0 -399
- claude_mpm/agents/templates/web_ui.json +0 -189
- claude_mpm/cli/commands/mpm_init.py +0 -2093
- claude_mpm/commands/mpm-tickets.md +0 -102
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +0 -1
- claude_mpm/dashboard/react/components/DataInspector/DataInspector.module.css +0 -188
- claude_mpm/dashboard/react/components/EventViewer/EventViewer.module.css +0 -156
- claude_mpm/dashboard/react/components/shared/ConnectionStatus.module.css +0 -38
- claude_mpm/dashboard/react/components/shared/FilterBar.module.css +0 -92
- claude_mpm/dashboard/static/archive/activity_dashboard_fixed.html +0 -248
- claude_mpm/dashboard/static/archive/activity_dashboard_test.html +0 -61
- claude_mpm/dashboard/static/archive/test_activity_connection.html +0 -179
- claude_mpm/dashboard/static/archive/test_claude_tree_tab.html +0 -68
- claude_mpm/dashboard/static/archive/test_dashboard.html +0 -409
- claude_mpm/dashboard/static/archive/test_dashboard_fixed.html +0 -519
- claude_mpm/dashboard/static/archive/test_dashboard_verification.html +0 -181
- claude_mpm/dashboard/static/archive/test_file_data.html +0 -315
- claude_mpm/dashboard/static/archive/test_file_tree_empty_state.html +0 -243
- claude_mpm/dashboard/static/archive/test_file_tree_fix.html +0 -234
- claude_mpm/dashboard/static/archive/test_file_tree_rename.html +0 -117
- claude_mpm/dashboard/static/archive/test_file_tree_tab.html +0 -115
- claude_mpm/dashboard/static/archive/test_file_viewer.html +0 -224
- claude_mpm/dashboard/static/archive/test_final_activity.html +0 -220
- claude_mpm/dashboard/static/archive/test_tab_fix.html +0 -139
- claude_mpm/dashboard/static/built/assets/events.DjpNxWNo.css +0 -1
- claude_mpm/dashboard/static/built/components/activity-tree.js +0 -2
- claude_mpm/dashboard/static/built/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/built/components/agent-inference.js +0 -2
- claude_mpm/dashboard/static/built/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/built/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/built/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/built/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/built/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/built/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/built/components/code-tree.js +0 -2
- claude_mpm/dashboard/static/built/components/code-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/built/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/built/components/event-processor.js +0 -2
- claude_mpm/dashboard/static/built/components/event-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/export-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/built/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +0 -2
- claude_mpm/dashboard/static/built/components/file-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/hud-library-loader.js +0 -2
- claude_mpm/dashboard/static/built/components/hud-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/hud-visualizer.js +0 -2
- claude_mpm/dashboard/static/built/components/module-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/nav-bar.js +0 -145
- claude_mpm/dashboard/static/built/components/page-structure.js +0 -429
- claude_mpm/dashboard/static/built/components/session-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/socket-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/unified-data-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/working-directory.js +0 -2
- claude_mpm/dashboard/static/built/connection-manager.js +0 -536
- claude_mpm/dashboard/static/built/dashboard.js +0 -2
- claude_mpm/dashboard/static/built/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/built/react/events.js +0 -30
- claude_mpm/dashboard/static/built/shared/dom-helpers.js +0 -396
- claude_mpm/dashboard/static/built/shared/event-bus.js +0 -330
- claude_mpm/dashboard/static/built/shared/event-filter-service.js +0 -540
- claude_mpm/dashboard/static/built/shared/logger.js +0 -385
- claude_mpm/dashboard/static/built/shared/page-structure.js +0 -249
- claude_mpm/dashboard/static/built/shared/tooltip-service.js +0 -253
- claude_mpm/dashboard/static/built/socket-client.js +0 -2
- claude_mpm/dashboard/static/built/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/dist/assets/events.DjpNxWNo.css +0 -1
- claude_mpm/dashboard/static/dist/components/activity-tree.js +0 -2
- claude_mpm/dashboard/static/dist/components/agent-inference.js +0 -2
- claude_mpm/dashboard/static/dist/components/code-tree.js +0 -2
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/event-processor.js +0 -2
- claude_mpm/dashboard/static/dist/components/event-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/export-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +0 -2
- claude_mpm/dashboard/static/dist/components/file-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/hud-library-loader.js +0 -2
- claude_mpm/dashboard/static/dist/components/hud-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/hud-visualizer.js +0 -2
- claude_mpm/dashboard/static/dist/components/module-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/session-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/socket-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/ui-state-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/working-directory.js +0 -2
- claude_mpm/dashboard/static/dist/dashboard.js +0 -2
- claude_mpm/dashboard/static/dist/react/events.js +0 -30
- claude_mpm/dashboard/static/dist/socket-client.js +0 -2
- claude_mpm/dashboard/static/events.html +0 -607
- claude_mpm/dashboard/static/index.html +0 -635
- claude_mpm/dashboard/static/js/shared/dom-helpers.js +0 -396
- claude_mpm/dashboard/static/js/shared/event-bus.js +0 -330
- claude_mpm/dashboard/static/js/shared/logger.js +0 -385
- claude_mpm/dashboard/static/js/shared/tooltip-service.js +0 -253
- claude_mpm/dashboard/static/js/stores/dashboard-store.js +0 -562
- claude_mpm/dashboard/static/legacy/activity.html +0 -736
- claude_mpm/dashboard/static/legacy/agents.html +0 -786
- claude_mpm/dashboard/static/legacy/files.html +0 -747
- claude_mpm/dashboard/static/legacy/tools.html +0 -831
- claude_mpm/dashboard/static/monitors.html +0 -431
- claude_mpm/dashboard/static/production/events.html +0 -659
- claude_mpm/dashboard/static/production/main.html +0 -698
- claude_mpm/dashboard/static/production/monitors.html +0 -483
- claude_mpm/dashboard/static/test-archive/dashboard.html +0 -635
- claude_mpm/dashboard/static/test-archive/debug-events.html +0 -147
- claude_mpm/dashboard/static/test-archive/test-navigation.html +0 -256
- claude_mpm/dashboard/static/test-archive/test-react-exports.html +0 -180
- claude_mpm/dashboard/static/test-archive/test_debug.html +0 -25
- claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +0 -75
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +0 -184
- claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +0 -107
- claude_mpm/skills/bundled/collaboration/requesting-code-review/code-reviewer.md +0 -146
- claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +0 -118
- claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +0 -177
- claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +0 -119
- claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +0 -148
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +0 -483
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +0 -452
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +0 -449
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +0 -411
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +0 -14
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +0 -58
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +0 -68
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +0 -69
- claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +0 -175
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +0 -213
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +0 -314
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +0 -227
- claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +0 -74
- claude_mpm/skills/bundled/main/internal-comms/SKILL.md +0 -32
- claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +0 -47
- claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +0 -65
- claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +0 -30
- claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +0 -16
- claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +0 -328
- claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +0 -602
- claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +0 -915
- claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +0 -916
- claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +0 -752
- claude_mpm/skills/bundled/main/skill-creator/SKILL.md +0 -209
- claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +0 -123
- claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +0 -145
- claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +0 -543
- claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +0 -741
- claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +0 -470
- claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +0 -458
- claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +0 -639
- claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +0 -304
- claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +0 -96
- claude_mpm/tools/code_tree_analyzer.py +0 -1825
- /claude_mpm/agents/templates/{git_file_tracking.md → git-file-tracking.md} +0 -0
- /claude_mpm/agents/templates/{pm_examples.md → pm-examples.md} +0 -0
- /claude_mpm/agents/templates/{response_format.md → response-format.md} +0 -0
- /claude_mpm/agents/templates/{validation_templates.md → validation-templates.md} +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/WHEEL +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
"""Git Source Sync Service for agent templates.
|
|
2
|
+
|
|
3
|
+
Syncs agent markdown files from remote Git repositories (GitHub) using
|
|
4
|
+
ETag-based caching and SQLite state tracking for efficient updates.
|
|
5
|
+
Implements Stage 1 of the three-stage sync algorithm:
|
|
6
|
+
- Check repository for updates using ETag headers
|
|
7
|
+
- Download agent files via raw.githubusercontent.com URLs
|
|
8
|
+
- Track content with SHA-256 hashes and sync history in SQLite
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from claude_mpm.core.file_utils import get_file_hash
|
|
21
|
+
from claude_mpm.services.agents.sources.agent_sync_state import AgentSyncState
|
|
22
|
+
from claude_mpm.utils.progress import create_progress_bar
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitSyncError(Exception):
|
|
28
|
+
"""Base exception for git sync errors."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NetworkError(GitSyncError):
|
|
32
|
+
"""Network/HTTP errors."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CacheError(GitSyncError):
|
|
36
|
+
"""Cache read/write errors."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ETagCache:
|
|
40
|
+
"""Manages ETag storage for efficient HTTP caching.
|
|
41
|
+
|
|
42
|
+
Design Decision: Simple JSON file-based cache for ETag storage
|
|
43
|
+
|
|
44
|
+
Rationale: ETags are small text strings that change infrequently.
|
|
45
|
+
JSON provides human-readable format for debugging and is sufficient
|
|
46
|
+
for this use case. Rejected SQLite as it adds complexity without
|
|
47
|
+
significant benefits for this simple key-value storage.
|
|
48
|
+
|
|
49
|
+
Trade-offs:
|
|
50
|
+
- Simplicity: JSON is simple and debuggable
|
|
51
|
+
- Performance: File I/O is fast enough for <100 ETags
|
|
52
|
+
- Scalability: Limited to ~1000s of ETags before performance degrades
|
|
53
|
+
|
|
54
|
+
Extension Points: Can be replaced with SQLite if ETag count exceeds
|
|
55
|
+
performance threshold (>1000 agents syncing).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, cache_file: Path):
|
|
59
|
+
"""Initialize ETag cache.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
cache_file: Path to JSON file storing ETags
|
|
63
|
+
"""
|
|
64
|
+
self._cache_file = cache_file
|
|
65
|
+
self._cache: Dict[str, Dict[str, Any]] = self._load_cache()
|
|
66
|
+
|
|
67
|
+
def get_etag(self, url: str) -> Optional[str]:
|
|
68
|
+
"""Retrieve stored ETag for URL.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
url: URL to look up ETag for
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
ETag string or None if not found
|
|
75
|
+
"""
|
|
76
|
+
entry = self._cache.get(url, {})
|
|
77
|
+
return entry.get("etag")
|
|
78
|
+
|
|
79
|
+
def set_etag(self, url: str, etag: str, file_size: Optional[int] = None):
|
|
80
|
+
"""Store ETag for URL.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
url: URL to store ETag for
|
|
84
|
+
etag: ETag value to store
|
|
85
|
+
file_size: Optional file size in bytes
|
|
86
|
+
"""
|
|
87
|
+
self._cache[url] = {
|
|
88
|
+
"etag": etag,
|
|
89
|
+
"last_modified": datetime.now(timezone.utc).isoformat(),
|
|
90
|
+
"file_size": file_size,
|
|
91
|
+
}
|
|
92
|
+
self._save_cache()
|
|
93
|
+
|
|
94
|
+
def _load_cache(self) -> Dict[str, Dict[str, Any]]:
|
|
95
|
+
"""Load ETag cache from JSON file.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Dictionary mapping URLs to ETag metadata
|
|
99
|
+
|
|
100
|
+
Error Handling:
|
|
101
|
+
- FileNotFoundError: Returns empty dict (first run)
|
|
102
|
+
- JSONDecodeError: Logs warning and returns empty dict
|
|
103
|
+
- PermissionError: Logs error and returns empty dict
|
|
104
|
+
"""
|
|
105
|
+
if not self._cache_file.exists():
|
|
106
|
+
return {}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
with self._cache_file.open() as f:
|
|
110
|
+
return json.load(f)
|
|
111
|
+
except json.JSONDecodeError:
|
|
112
|
+
logger.warning(f"Invalid ETag cache file: {self._cache_file}, resetting")
|
|
113
|
+
return {}
|
|
114
|
+
except PermissionError as e:
|
|
115
|
+
logger.error(f"Permission denied reading ETag cache: {e}")
|
|
116
|
+
return {}
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Error loading ETag cache: {e}")
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
def _save_cache(self):
|
|
122
|
+
"""Persist ETag cache to JSON file.
|
|
123
|
+
|
|
124
|
+
Error Handling:
|
|
125
|
+
- PermissionError: Logs error but doesn't raise (cache is optional)
|
|
126
|
+
- IOError: Logs error but doesn't raise (graceful degradation)
|
|
127
|
+
|
|
128
|
+
Failure Mode: If cache write fails, next sync will re-download
|
|
129
|
+
all files (inefficient but correct behavior).
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
# Ensure parent directory exists
|
|
133
|
+
self._cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
|
|
135
|
+
with self._cache_file.open("w") as f:
|
|
136
|
+
json.dump(self._cache, f, indent=2)
|
|
137
|
+
except PermissionError as e:
|
|
138
|
+
logger.error(f"Permission denied writing ETag cache: {e}")
|
|
139
|
+
except OSError as e:
|
|
140
|
+
logger.error(f"IO error writing ETag cache: {e}")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Error saving ETag cache: {e}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class GitSourceSyncService:
|
|
146
|
+
"""Service for syncing agent templates from remote Git repositories.
|
|
147
|
+
|
|
148
|
+
Design Decision: Use raw.githubusercontent.com URLs instead of Git API
|
|
149
|
+
|
|
150
|
+
Rationale: Raw URLs bypass GitHub API rate limits (60/hour unauthenticated,
|
|
151
|
+
5000/hour authenticated). For agent files, direct raw access is sufficient
|
|
152
|
+
and more reliable. Rejected Git API because it requires base64 decoding
|
|
153
|
+
and consumes rate limit unnecessarily.
|
|
154
|
+
|
|
155
|
+
Trade-offs:
|
|
156
|
+
- Performance: Raw URLs have no rate limit, instant access
|
|
157
|
+
- Simplicity: Direct HTTP GET, no JSON parsing or base64 decoding
|
|
158
|
+
- Discovery: Cannot auto-discover agent list (requires manifest or hardcoded)
|
|
159
|
+
- Metadata: No commit info, file size, or last modified date
|
|
160
|
+
|
|
161
|
+
Optimization Opportunities:
|
|
162
|
+
1. Async Downloads: Use aiohttp for parallel agent downloads
|
|
163
|
+
- Estimated speedup: 5-10x for initial sync (10 agents)
|
|
164
|
+
- Effort: 4-6 hours, medium complexity
|
|
165
|
+
- Threshold: Implement when agent count >20
|
|
166
|
+
|
|
167
|
+
2. Manifest File: Add agents.json to repository for auto-discovery
|
|
168
|
+
- Removes hardcoded agent list
|
|
169
|
+
- Effort: 2 hours
|
|
170
|
+
- Blocks: Requires repository write access
|
|
171
|
+
|
|
172
|
+
Performance:
|
|
173
|
+
- Time Complexity: O(n) where n = number of agents
|
|
174
|
+
- Space Complexity: O(n) for in-memory agent content during sync
|
|
175
|
+
- Expected Performance:
|
|
176
|
+
* First sync (10 agents): ~5-10 seconds
|
|
177
|
+
* Subsequent sync (no changes): ~1-2 seconds (ETag checks only)
|
|
178
|
+
* Partial update (2 of 10 changed): ~2-3 seconds
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
source_url: str = "https://raw.githubusercontent.com/bobmatnyc/claude-mpm-agents/main/agents",
|
|
184
|
+
cache_dir: Optional[Path] = None,
|
|
185
|
+
source_id: str = "github-remote",
|
|
186
|
+
):
|
|
187
|
+
"""Initialize Git source sync service.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
source_url: Base URL for raw files (without trailing slash)
|
|
191
|
+
cache_dir: Local cache directory (defaults to ~/.claude-mpm/cache/remote-agents/)
|
|
192
|
+
source_id: Unique identifier for this source (for multi-source support)
|
|
193
|
+
|
|
194
|
+
Design Decision: Cache to ~/.claude-mpm/cache/remote-agents/ (canonical location)
|
|
195
|
+
|
|
196
|
+
Rationale: Separates cached repository structure from deployed agents.
|
|
197
|
+
This allows preserving nested directory structure in cache while
|
|
198
|
+
flattening for deployment. Enables multiple deployment targets
|
|
199
|
+
(user, project) from single cache source.
|
|
200
|
+
|
|
201
|
+
Trade-offs:
|
|
202
|
+
- Storage: Uses 2x disk space (cache + deployment)
|
|
203
|
+
- Performance: Copy operation on deployment (~10ms for 50 agents)
|
|
204
|
+
- Flexibility: Supports project-specific deployments
|
|
205
|
+
- Migration: Requires one-time migration from old cache location
|
|
206
|
+
"""
|
|
207
|
+
self.source_url = source_url.rstrip("/")
|
|
208
|
+
self.source_id = source_id
|
|
209
|
+
|
|
210
|
+
# Setup cache directory (canonical: ~/.claude-mpm/cache/remote-agents/)
|
|
211
|
+
if cache_dir:
|
|
212
|
+
self.cache_dir = Path(cache_dir)
|
|
213
|
+
else:
|
|
214
|
+
# Default to ~/.claude-mpm/cache/remote-agents/ (canonical cache location)
|
|
215
|
+
home = Path.home()
|
|
216
|
+
self.cache_dir = home / ".claude-mpm" / "cache" / "remote-agents"
|
|
217
|
+
|
|
218
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
|
|
220
|
+
# Setup HTTP session with connection pooling
|
|
221
|
+
self.session = requests.Session()
|
|
222
|
+
self.session.headers["Accept"] = "text/plain"
|
|
223
|
+
|
|
224
|
+
# Initialize SQLite state tracking (NEW)
|
|
225
|
+
self.sync_state = AgentSyncState()
|
|
226
|
+
|
|
227
|
+
# Register this source
|
|
228
|
+
self.sync_state.register_source(
|
|
229
|
+
source_id=self.source_id, url=self.source_url, enabled=True
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Initialize ETag cache (DEPRECATED - kept for backward compatibility)
|
|
233
|
+
etag_cache_file = self.cache_dir / ".etag-cache.json"
|
|
234
|
+
self.etag_cache = ETagCache(etag_cache_file)
|
|
235
|
+
|
|
236
|
+
# Migrate old ETag cache to SQLite if it exists
|
|
237
|
+
if etag_cache_file.exists():
|
|
238
|
+
self._migrate_etag_cache(etag_cache_file)
|
|
239
|
+
|
|
240
|
+
# NEW: Initialize git manager for cache (Phase 1 integration)
|
|
241
|
+
from claude_mpm.services.agents.cache_git_manager import CacheGitManager
|
|
242
|
+
|
|
243
|
+
self.git_manager = CacheGitManager(self.cache_dir)
|
|
244
|
+
|
|
245
|
+
def sync_agents(
|
|
246
|
+
self,
|
|
247
|
+
force_refresh: bool = False,
|
|
248
|
+
show_progress: bool = True,
|
|
249
|
+
progress_prefix: str = "Syncing agents",
|
|
250
|
+
progress_suffix: str = "agents",
|
|
251
|
+
) -> Dict[str, Any]:
|
|
252
|
+
"""Sync agents from remote Git repository with SQLite state tracking.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
force_refresh: Force download even if cache is fresh (bypasses ETag)
|
|
256
|
+
show_progress: Show ASCII progress bar during sync (default: True, auto-detects TTY)
|
|
257
|
+
progress_prefix: Custom prefix for progress bar (default: "Syncing agents")
|
|
258
|
+
progress_suffix: Custom suffix for completion message (default: "agents")
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Dictionary with sync results:
|
|
262
|
+
{
|
|
263
|
+
"synced": ["agent1.md", "agent2.md"], # New downloads
|
|
264
|
+
"cached": ["agent3.md"], # ETag 304 responses
|
|
265
|
+
"failed": [], # Failed downloads
|
|
266
|
+
"total_downloaded": 2,
|
|
267
|
+
"cache_hits": 1
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Error Handling:
|
|
271
|
+
- Network errors: Individual agent failures don't stop sync
|
|
272
|
+
- Failed agents added to "failed" list
|
|
273
|
+
- Returns partial success if some agents sync successfully
|
|
274
|
+
"""
|
|
275
|
+
logger.info(f"Starting agent sync from {self.source_url}")
|
|
276
|
+
logger.debug(f"Cache directory: {self.cache_dir}")
|
|
277
|
+
logger.debug(f"Force refresh: {force_refresh}")
|
|
278
|
+
|
|
279
|
+
start_time = time.time()
|
|
280
|
+
|
|
281
|
+
# NEW: Pre-sync git operations (Phase 1 integration)
|
|
282
|
+
if self.git_manager.is_git_repo():
|
|
283
|
+
logger.debug("Cache is a git repository, checking for updates...")
|
|
284
|
+
|
|
285
|
+
# Warn about uncommitted changes
|
|
286
|
+
if self.git_manager.has_uncommitted_changes():
|
|
287
|
+
uncommitted_count = len(
|
|
288
|
+
self.git_manager.get_status().get("uncommitted", [])
|
|
289
|
+
)
|
|
290
|
+
logger.warning(
|
|
291
|
+
f"Cache has {uncommitted_count} uncommitted change(s). "
|
|
292
|
+
"These will be preserved, but consider committing them."
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Pull latest if online (non-blocking)
|
|
296
|
+
try:
|
|
297
|
+
success, msg = self.git_manager.pull_latest()
|
|
298
|
+
if success:
|
|
299
|
+
logger.info(f"✅ Git pull: {msg}")
|
|
300
|
+
else:
|
|
301
|
+
logger.warning(f"⚠️ Git pull failed: {msg}")
|
|
302
|
+
logger.info("Continuing with HTTP sync as fallback")
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(f"Git pull error (continuing with HTTP sync): {e}")
|
|
305
|
+
else:
|
|
306
|
+
logger.debug("Cache is not a git repository, skipping git operations")
|
|
307
|
+
|
|
308
|
+
results = {
|
|
309
|
+
"synced": [],
|
|
310
|
+
"cached": [],
|
|
311
|
+
"failed": [],
|
|
312
|
+
"total_downloaded": 0,
|
|
313
|
+
"cache_hits": 0,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Get list of agents to sync
|
|
317
|
+
agent_list = self._get_agent_list()
|
|
318
|
+
|
|
319
|
+
# Create progress bar if enabled
|
|
320
|
+
progress_bar = None
|
|
321
|
+
if show_progress:
|
|
322
|
+
progress_bar = create_progress_bar(
|
|
323
|
+
total=len(agent_list), prefix=progress_prefix
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
for idx, agent_filename in enumerate(agent_list, start=1):
|
|
327
|
+
try:
|
|
328
|
+
# Update progress bar with current file
|
|
329
|
+
if progress_bar:
|
|
330
|
+
progress_bar.update(idx, message=agent_filename)
|
|
331
|
+
|
|
332
|
+
url = f"{self.source_url}/{agent_filename}"
|
|
333
|
+
content, status = self._fetch_with_etag(url, force_refresh)
|
|
334
|
+
|
|
335
|
+
if status == 200:
|
|
336
|
+
# New content downloaded - save and track
|
|
337
|
+
self._save_to_cache(agent_filename, content)
|
|
338
|
+
|
|
339
|
+
# Track file with content hash in SQLite
|
|
340
|
+
cache_file = self.cache_dir / agent_filename
|
|
341
|
+
content_sha = get_file_hash(cache_file, algorithm="sha256")
|
|
342
|
+
if content_sha:
|
|
343
|
+
self.sync_state.track_file(
|
|
344
|
+
source_id=self.source_id,
|
|
345
|
+
file_path=agent_filename,
|
|
346
|
+
content_sha=content_sha,
|
|
347
|
+
local_path=str(cache_file),
|
|
348
|
+
file_size=len(content.encode("utf-8")),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
results["synced"].append(agent_filename)
|
|
352
|
+
results["total_downloaded"] += 1
|
|
353
|
+
logger.debug(f"Downloaded: {agent_filename}")
|
|
354
|
+
|
|
355
|
+
elif status == 304:
|
|
356
|
+
# Not modified - verify hash
|
|
357
|
+
cache_file = self.cache_dir / agent_filename
|
|
358
|
+
if cache_file.exists():
|
|
359
|
+
current_sha = get_file_hash(cache_file, algorithm="sha256")
|
|
360
|
+
if current_sha and self.sync_state.has_file_changed(
|
|
361
|
+
self.source_id, agent_filename, current_sha
|
|
362
|
+
):
|
|
363
|
+
# Hash mismatch - re-download
|
|
364
|
+
logger.warning(
|
|
365
|
+
f"Hash mismatch for {agent_filename}, re-downloading"
|
|
366
|
+
)
|
|
367
|
+
content, _ = self._fetch_with_etag(url, force_refresh=True)
|
|
368
|
+
if content:
|
|
369
|
+
self._save_to_cache(agent_filename, content)
|
|
370
|
+
# Re-calculate and track hash
|
|
371
|
+
new_sha = get_file_hash(cache_file, algorithm="sha256")
|
|
372
|
+
if new_sha:
|
|
373
|
+
self.sync_state.track_file(
|
|
374
|
+
source_id=self.source_id,
|
|
375
|
+
file_path=agent_filename,
|
|
376
|
+
content_sha=new_sha,
|
|
377
|
+
local_path=str(cache_file),
|
|
378
|
+
file_size=len(content.encode("utf-8")),
|
|
379
|
+
)
|
|
380
|
+
results["synced"].append(agent_filename)
|
|
381
|
+
results["total_downloaded"] += 1
|
|
382
|
+
else:
|
|
383
|
+
results["failed"].append(agent_filename)
|
|
384
|
+
else:
|
|
385
|
+
# Hash matches - true cache hit
|
|
386
|
+
results["cached"].append(agent_filename)
|
|
387
|
+
results["cache_hits"] += 1
|
|
388
|
+
logger.debug(f"Cache hit: {agent_filename}")
|
|
389
|
+
else:
|
|
390
|
+
# Cache file missing - re-download
|
|
391
|
+
logger.warning(
|
|
392
|
+
f"Cache file missing for {agent_filename}, re-downloading"
|
|
393
|
+
)
|
|
394
|
+
content, _ = self._fetch_with_etag(url, force_refresh=True)
|
|
395
|
+
if content:
|
|
396
|
+
self._save_to_cache(agent_filename, content)
|
|
397
|
+
# Track hash
|
|
398
|
+
current_sha = get_file_hash(cache_file, algorithm="sha256")
|
|
399
|
+
if current_sha:
|
|
400
|
+
self.sync_state.track_file(
|
|
401
|
+
source_id=self.source_id,
|
|
402
|
+
file_path=agent_filename,
|
|
403
|
+
content_sha=current_sha,
|
|
404
|
+
local_path=str(cache_file),
|
|
405
|
+
file_size=len(content.encode("utf-8")),
|
|
406
|
+
)
|
|
407
|
+
results["synced"].append(agent_filename)
|
|
408
|
+
results["total_downloaded"] += 1
|
|
409
|
+
else:
|
|
410
|
+
results["failed"].append(agent_filename)
|
|
411
|
+
|
|
412
|
+
else:
|
|
413
|
+
# Error status
|
|
414
|
+
logger.warning(f"Unexpected status {status} for {agent_filename}")
|
|
415
|
+
results["failed"].append(agent_filename)
|
|
416
|
+
|
|
417
|
+
except requests.RequestException as e:
|
|
418
|
+
logger.error(f"Network error downloading {agent_filename}: {e}")
|
|
419
|
+
results["failed"].append(agent_filename)
|
|
420
|
+
# Continue with other agents
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.error(f"Unexpected error for {agent_filename}: {e}")
|
|
423
|
+
results["failed"].append(agent_filename)
|
|
424
|
+
|
|
425
|
+
# Record sync result in history
|
|
426
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
427
|
+
status = (
|
|
428
|
+
"success"
|
|
429
|
+
if not results["failed"]
|
|
430
|
+
else ("partial" if results["synced"] or results["cached"] else "error")
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
self.sync_state.record_sync_result(
|
|
434
|
+
source_id=self.source_id,
|
|
435
|
+
status=status,
|
|
436
|
+
files_synced=results["total_downloaded"],
|
|
437
|
+
files_cached=results["cache_hits"],
|
|
438
|
+
files_failed=len(results["failed"]),
|
|
439
|
+
duration_ms=duration_ms,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Update source metadata
|
|
443
|
+
self.sync_state.update_source_sync_metadata(source_id=self.source_id)
|
|
444
|
+
|
|
445
|
+
# Finish progress bar with clear breakdown
|
|
446
|
+
if progress_bar:
|
|
447
|
+
downloaded = results["total_downloaded"]
|
|
448
|
+
cached = results["cache_hits"]
|
|
449
|
+
total = downloaded + cached
|
|
450
|
+
failed_count = len(results["failed"])
|
|
451
|
+
|
|
452
|
+
if failed_count > 0:
|
|
453
|
+
progress_bar.finish(
|
|
454
|
+
message=f"Complete: {downloaded} downloaded, {cached} cached, {failed_count} failed ({total} total)"
|
|
455
|
+
)
|
|
456
|
+
# Show breakdown to clarify only changed files were downloaded
|
|
457
|
+
elif cached > 0:
|
|
458
|
+
progress_bar.finish(
|
|
459
|
+
message=f"Complete: {downloaded} downloaded, {cached} cached ({total} total)"
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
# All new downloads (first sync)
|
|
463
|
+
progress_bar.finish(
|
|
464
|
+
message=f"Complete: {downloaded} {progress_suffix} downloaded"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Log summary
|
|
468
|
+
logger.info(
|
|
469
|
+
f"Sync complete: {results['total_downloaded']} downloaded, "
|
|
470
|
+
f"{results['cache_hits']} from cache, {len(results['failed'])} failed"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return results
|
|
474
|
+
|
|
475
|
+
def check_for_updates(self) -> Dict[str, bool]:
|
|
476
|
+
"""Check if remote repository has updates using ETag.
|
|
477
|
+
|
|
478
|
+
Uses HEAD requests to check ETags without downloading content.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Dictionary mapping agent filenames to update status:
|
|
482
|
+
{
|
|
483
|
+
"research.md": True, # Has updates
|
|
484
|
+
"engineer.md": False, # No updates (ETag matches)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
Performance: ~1-2 seconds for 10 agents (HEAD requests only)
|
|
488
|
+
"""
|
|
489
|
+
logger.info("Checking for agent updates")
|
|
490
|
+
updates = {}
|
|
491
|
+
|
|
492
|
+
agent_list = self._get_agent_list()
|
|
493
|
+
|
|
494
|
+
for agent_filename in agent_list:
|
|
495
|
+
try:
|
|
496
|
+
url = f"{self.source_url}/{agent_filename}"
|
|
497
|
+
cached_etag = self.etag_cache.get_etag(url)
|
|
498
|
+
|
|
499
|
+
# Use HEAD request to check ETag without downloading
|
|
500
|
+
response = self.session.head(url, timeout=30)
|
|
501
|
+
|
|
502
|
+
if response.status_code == 200:
|
|
503
|
+
remote_etag = response.headers.get("ETag")
|
|
504
|
+
has_update = remote_etag != cached_etag
|
|
505
|
+
updates[agent_filename] = has_update
|
|
506
|
+
|
|
507
|
+
if has_update:
|
|
508
|
+
logger.info(f"Update available: {agent_filename}")
|
|
509
|
+
else:
|
|
510
|
+
logger.warning(
|
|
511
|
+
f"Could not check {agent_filename}: HTTP {response.status_code}"
|
|
512
|
+
)
|
|
513
|
+
updates[agent_filename] = False
|
|
514
|
+
|
|
515
|
+
except requests.RequestException as e:
|
|
516
|
+
logger.error(f"Network error checking {agent_filename}: {e}")
|
|
517
|
+
updates[agent_filename] = False
|
|
518
|
+
|
|
519
|
+
return updates
|
|
520
|
+
|
|
521
|
+
def download_agent_file(self, filename: str) -> Optional[str]:
|
|
522
|
+
"""Download single agent file with ETag caching.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
filename: Agent filename (e.g., "research.md")
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Agent content as string, or None if download fails
|
|
529
|
+
|
|
530
|
+
Error Handling:
|
|
531
|
+
- Network errors: Returns None, logs error
|
|
532
|
+
- 404 Not Found: Returns None, logs warning
|
|
533
|
+
- Cache fallback: Attempts to load from cache on error
|
|
534
|
+
"""
|
|
535
|
+
url = f"{self.source_url}/{filename}"
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
content, status = self._fetch_with_etag(url)
|
|
539
|
+
|
|
540
|
+
if status == 200:
|
|
541
|
+
self._save_to_cache(filename, content)
|
|
542
|
+
return content
|
|
543
|
+
if status == 304:
|
|
544
|
+
# Load from cache
|
|
545
|
+
return self._load_from_cache(filename)
|
|
546
|
+
logger.warning(f"HTTP {status} for {filename}")
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
except requests.RequestException as e:
|
|
550
|
+
logger.error(f"Network error downloading {filename}: {e}")
|
|
551
|
+
# Try cache fallback
|
|
552
|
+
return self._load_from_cache(filename)
|
|
553
|
+
|
|
554
|
+
def _fetch_with_etag(
|
|
555
|
+
self, url: str, force_refresh: bool = False
|
|
556
|
+
) -> Tuple[Optional[str], int]:
|
|
557
|
+
"""Fetch URL with ETag caching.
|
|
558
|
+
|
|
559
|
+
Design Decision: Use If-None-Match header for conditional requests
|
|
560
|
+
|
|
561
|
+
Rationale: ETag-based caching is standard HTTP pattern that GitHub
|
|
562
|
+
supports. Reduces bandwidth by 95%+ for unchanged files. Alternative
|
|
563
|
+
was Last-Modified timestamps, but ETags are more reliable for Git
|
|
564
|
+
content (commit hash based).
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
url: URL to fetch
|
|
568
|
+
force_refresh: Skip ETag check and force download
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Tuple of (content, status_code) where:
|
|
572
|
+
- status_code 200: New content downloaded
|
|
573
|
+
- status_code 304: Not modified (use cached)
|
|
574
|
+
- content is None on 304
|
|
575
|
+
|
|
576
|
+
Error Handling:
|
|
577
|
+
- Timeout: 30 second timeout, raises requests.Timeout
|
|
578
|
+
- Connection errors: Raises requests.ConnectionError
|
|
579
|
+
- HTTP errors (4xx, 5xx): Returns (None, status_code)
|
|
580
|
+
"""
|
|
581
|
+
headers = {}
|
|
582
|
+
|
|
583
|
+
# Add ETag header if we have cached version and not forcing refresh
|
|
584
|
+
if not force_refresh:
|
|
585
|
+
cached_etag = self.etag_cache.get_etag(url)
|
|
586
|
+
if cached_etag:
|
|
587
|
+
headers["If-None-Match"] = cached_etag
|
|
588
|
+
|
|
589
|
+
response = self.session.get(url, headers=headers, timeout=30)
|
|
590
|
+
|
|
591
|
+
if response.status_code == 304:
|
|
592
|
+
# Not modified - use cached version
|
|
593
|
+
return None, 304
|
|
594
|
+
|
|
595
|
+
if response.status_code == 200:
|
|
596
|
+
# New content - update cache
|
|
597
|
+
content = response.text
|
|
598
|
+
etag = response.headers.get("ETag")
|
|
599
|
+
if etag:
|
|
600
|
+
file_size = len(content.encode("utf-8"))
|
|
601
|
+
self.etag_cache.set_etag(url, etag, file_size)
|
|
602
|
+
return content, 200
|
|
603
|
+
|
|
604
|
+
# Error status
|
|
605
|
+
return None, response.status_code
|
|
606
|
+
|
|
607
|
+
def _save_to_cache(self, filename: str, content: str):
|
|
608
|
+
"""Save agent file to cache (Phase 1: preserves nested directory structure).
|
|
609
|
+
|
|
610
|
+
Design Decision: Preserve nested directory structure in cache
|
|
611
|
+
|
|
612
|
+
Rationale: Cache mirrors remote repository structure, allowing
|
|
613
|
+
proper organization and future features (e.g., category browsing).
|
|
614
|
+
Deployment layer flattens to .claude-mpm/agents/ for backward
|
|
615
|
+
compatibility.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
filename: Agent file path (may include directories, e.g., "engineer/core/engineer.md")
|
|
619
|
+
content: File content
|
|
620
|
+
|
|
621
|
+
Error Handling:
|
|
622
|
+
- PermissionError: Logs error but doesn't raise
|
|
623
|
+
- IOError: Logs error but doesn't raise
|
|
624
|
+
|
|
625
|
+
Failure Mode: If cache write fails, agent is still synced in memory
|
|
626
|
+
but will need re-download on next sync (graceful degradation).
|
|
627
|
+
"""
|
|
628
|
+
try:
|
|
629
|
+
cache_file = self.cache_dir / filename
|
|
630
|
+
# Create parent directories for nested structure
|
|
631
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
632
|
+
cache_file.write_text(content, encoding="utf-8")
|
|
633
|
+
logger.debug(f"Saved to cache: {filename}")
|
|
634
|
+
except PermissionError as e:
|
|
635
|
+
logger.error(f"Permission denied writing {filename}: {e}")
|
|
636
|
+
except OSError as e:
|
|
637
|
+
logger.error(f"IO error writing {filename}: {e}")
|
|
638
|
+
except Exception as e:
|
|
639
|
+
logger.error(f"Error saving {filename} to cache: {e}")
|
|
640
|
+
|
|
641
|
+
def _load_from_cache(self, filename: str) -> Optional[str]:
|
|
642
|
+
"""Load agent file from cache.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
filename: Agent filename
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Cached content or None if not found
|
|
649
|
+
|
|
650
|
+
Error Handling:
|
|
651
|
+
- FileNotFoundError: Returns None (not in cache)
|
|
652
|
+
- PermissionError: Logs error, returns None
|
|
653
|
+
- IOError: Logs error, returns None
|
|
654
|
+
"""
|
|
655
|
+
cache_file = self.cache_dir / filename
|
|
656
|
+
|
|
657
|
+
if not cache_file.exists():
|
|
658
|
+
logger.debug(f"No cached version of {filename}")
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
try:
|
|
662
|
+
content = cache_file.read_text(encoding="utf-8")
|
|
663
|
+
logger.debug(f"Loaded from cache: {filename}")
|
|
664
|
+
return content
|
|
665
|
+
except PermissionError as e:
|
|
666
|
+
logger.error(f"Permission denied reading {filename}: {e}")
|
|
667
|
+
return None
|
|
668
|
+
except OSError as e:
|
|
669
|
+
logger.error(f"IO error reading {filename}: {e}")
|
|
670
|
+
return None
|
|
671
|
+
except Exception as e:
|
|
672
|
+
logger.error(f"Error loading {filename} from cache: {e}")
|
|
673
|
+
return None
|
|
674
|
+
|
|
675
|
+
def _get_agent_list(self) -> List[str]:
|
|
676
|
+
"""Get list of agent file paths to sync (including nested directories).
|
|
677
|
+
|
|
678
|
+
Design Decision: Use Git Tree API instead of Contents API (Phase 1 fix)
|
|
679
|
+
|
|
680
|
+
Rationale: Git Tree API with recursive=1 discovers entire repository
|
|
681
|
+
structure in a single request, solving the "1 agent discovered" issue.
|
|
682
|
+
Contents API only shows top-level files, missing nested directories.
|
|
683
|
+
|
|
684
|
+
Trade-offs:
|
|
685
|
+
- Performance: Single API call vs. 10-50+ recursive calls
|
|
686
|
+
- Rate Limits: 1 request vs. dozens (avoids 403 errors)
|
|
687
|
+
- Discovery: Finds ALL files in nested structure (50+ agents)
|
|
688
|
+
- API Complexity: Requires commit SHA lookup before tree fetch
|
|
689
|
+
|
|
690
|
+
Alternatives Considered:
|
|
691
|
+
1. Contents API with recursion: 50+ API calls, hits rate limits
|
|
692
|
+
2. Hardcoded nested paths: Misses new agents, unmaintainable
|
|
693
|
+
3. Manifest file: Requires repository write access
|
|
694
|
+
|
|
695
|
+
Error Handling:
|
|
696
|
+
- Network errors: Falls back to static list
|
|
697
|
+
- Rate limit exceeded: Falls back to static list
|
|
698
|
+
- JSON parse errors: Falls back to static list
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
List of agent file paths with directory structure
|
|
702
|
+
(e.g., ["research.md", "engineer/core/engineer.md", ...])
|
|
703
|
+
"""
|
|
704
|
+
# Extract repository info from source URL
|
|
705
|
+
# URL format: https://raw.githubusercontent.com/owner/repo/branch/path
|
|
706
|
+
try:
|
|
707
|
+
# Parse GitHub URL to extract owner/repo/branch
|
|
708
|
+
url_parts = self.source_url.replace(
|
|
709
|
+
"https://raw.githubusercontent.com/", ""
|
|
710
|
+
).split("/")
|
|
711
|
+
|
|
712
|
+
if len(url_parts) >= 3:
|
|
713
|
+
owner = url_parts[0]
|
|
714
|
+
repo = url_parts[1]
|
|
715
|
+
branch = url_parts[2]
|
|
716
|
+
base_path = "/".join(url_parts[3:]) if len(url_parts) > 3 else ""
|
|
717
|
+
|
|
718
|
+
logger.debug(
|
|
719
|
+
f"Discovering agents from {owner}/{repo}/{branch} via Git Tree API"
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Use Git Tree API for recursive discovery
|
|
723
|
+
agent_files = self._discover_agents_via_tree_api(
|
|
724
|
+
owner, repo, branch, base_path
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
if agent_files:
|
|
728
|
+
logger.info(
|
|
729
|
+
f"Discovered {len(agent_files)} agents via Git Tree API"
|
|
730
|
+
)
|
|
731
|
+
return sorted(agent_files)
|
|
732
|
+
|
|
733
|
+
logger.warning("No agent files found via Tree API, using fallback list")
|
|
734
|
+
|
|
735
|
+
except requests.RequestException as e:
|
|
736
|
+
logger.warning(
|
|
737
|
+
f"Network error fetching agent list from API: {e}, using fallback list"
|
|
738
|
+
)
|
|
739
|
+
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
740
|
+
logger.warning(
|
|
741
|
+
f"Error parsing GitHub API response: {e}, using fallback list"
|
|
742
|
+
)
|
|
743
|
+
except Exception as e:
|
|
744
|
+
logger.warning(
|
|
745
|
+
f"Unexpected error fetching agent list: {e}, using fallback list"
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Fallback to known agent list if API fails
|
|
749
|
+
logger.debug("Using fallback agent list")
|
|
750
|
+
return [
|
|
751
|
+
"research.md",
|
|
752
|
+
"engineer.md",
|
|
753
|
+
"qa.md",
|
|
754
|
+
"documentation.md",
|
|
755
|
+
"security.md",
|
|
756
|
+
"ops.md",
|
|
757
|
+
"ticketing.md",
|
|
758
|
+
"product_owner.md",
|
|
759
|
+
"version_control.md",
|
|
760
|
+
"project_organizer.md",
|
|
761
|
+
]
|
|
762
|
+
|
|
763
|
+
def _discover_agents_via_tree_api(
|
|
764
|
+
self, owner: str, repo: str, branch: str, base_path: str = ""
|
|
765
|
+
) -> List[str]:
|
|
766
|
+
"""Discover all agent files using GitHub Git Tree API with recursion.
|
|
767
|
+
|
|
768
|
+
Design Decision: Two-step Tree API pattern (commit SHA → tree)
|
|
769
|
+
|
|
770
|
+
Rationale: Git Tree API requires commit SHA, not branch name.
|
|
771
|
+
Step 1 resolves branch to SHA, Step 2 fetches recursive tree.
|
|
772
|
+
This pattern is standard for GitHub API and handles branch
|
|
773
|
+
references correctly.
|
|
774
|
+
|
|
775
|
+
Algorithm:
|
|
776
|
+
1. GET /repos/{owner}/{repo}/git/refs/heads/{branch} → commit SHA
|
|
777
|
+
2. GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1 → all files
|
|
778
|
+
3. Filter for .md/.json files in agents/ directory
|
|
779
|
+
4. Exclude README.md and .gitignore
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
owner: GitHub owner (e.g., "bobmatnyc")
|
|
783
|
+
repo: Repository name (e.g., "claude-mpm-agents")
|
|
784
|
+
branch: Branch name (e.g., "main")
|
|
785
|
+
base_path: Base path prefix to filter (e.g., "agents")
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
List of agent file paths relative to base_path
|
|
789
|
+
(e.g., ["research.md", "engineer/core/engineer.md"])
|
|
790
|
+
|
|
791
|
+
Error Handling:
|
|
792
|
+
- HTTP 404: Branch or repo not found, raises RequestException
|
|
793
|
+
- HTTP 403: Rate limit exceeded, raises RequestException
|
|
794
|
+
- Timeout: 30 second timeout, raises RequestException
|
|
795
|
+
- Empty tree: Returns empty list (logged as warning)
|
|
796
|
+
|
|
797
|
+
Performance:
|
|
798
|
+
- Time: ~500-800ms for 50+ agents (2 API calls)
|
|
799
|
+
- Rate Limit: Consumes 2 API calls per sync
|
|
800
|
+
- Scalability: Handles repositories with 1000s of files
|
|
801
|
+
"""
|
|
802
|
+
# Step 1: Get commit SHA for branch
|
|
803
|
+
refs_url = (
|
|
804
|
+
f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch}"
|
|
805
|
+
)
|
|
806
|
+
logger.debug(f"Fetching commit SHA from {refs_url}")
|
|
807
|
+
|
|
808
|
+
refs_response = self.session.get(
|
|
809
|
+
refs_url, headers={"Accept": "application/vnd.github+json"}, timeout=30
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
if refs_response.status_code == 403:
|
|
813
|
+
logger.warning(
|
|
814
|
+
"GitHub API rate limit exceeded (HTTP 403). "
|
|
815
|
+
"Consider setting GITHUB_TOKEN environment variable."
|
|
816
|
+
)
|
|
817
|
+
raise requests.RequestException("Rate limit exceeded")
|
|
818
|
+
|
|
819
|
+
refs_response.raise_for_status()
|
|
820
|
+
commit_sha = refs_response.json()["object"]["sha"]
|
|
821
|
+
logger.debug(f"Resolved {branch} to commit {commit_sha[:8]}")
|
|
822
|
+
|
|
823
|
+
# Step 2: Get recursive tree for commit
|
|
824
|
+
tree_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{commit_sha}"
|
|
825
|
+
params = {"recursive": "1"} # Recursively fetch all files
|
|
826
|
+
|
|
827
|
+
logger.debug(f"Fetching recursive tree from {tree_url}")
|
|
828
|
+
tree_response = self.session.get(
|
|
829
|
+
tree_url,
|
|
830
|
+
headers={"Accept": "application/vnd.github+json"},
|
|
831
|
+
params=params,
|
|
832
|
+
timeout=30,
|
|
833
|
+
)
|
|
834
|
+
tree_response.raise_for_status()
|
|
835
|
+
|
|
836
|
+
tree_data = tree_response.json()
|
|
837
|
+
all_items = tree_data.get("tree", [])
|
|
838
|
+
|
|
839
|
+
logger.debug(f"Tree API returned {len(all_items)} total items")
|
|
840
|
+
|
|
841
|
+
# Step 3: Filter for agent files
|
|
842
|
+
agent_files = []
|
|
843
|
+
for item in all_items:
|
|
844
|
+
# Only process files (blobs), not directories (trees)
|
|
845
|
+
if item["type"] != "blob":
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
path = item["path"]
|
|
849
|
+
|
|
850
|
+
# Filter for files in base_path (e.g., "agents/")
|
|
851
|
+
if base_path and not path.startswith(base_path + "/"):
|
|
852
|
+
continue
|
|
853
|
+
|
|
854
|
+
# Remove base_path prefix for relative paths
|
|
855
|
+
if base_path:
|
|
856
|
+
relative_path = path[len(base_path) + 1 :]
|
|
857
|
+
else:
|
|
858
|
+
relative_path = path
|
|
859
|
+
|
|
860
|
+
# Filter for .md or .json files, exclude README and .gitignore
|
|
861
|
+
if (
|
|
862
|
+
relative_path.endswith(".md") or relative_path.endswith(".json")
|
|
863
|
+
) and relative_path not in ["README.md", ".gitignore"]:
|
|
864
|
+
agent_files.append(relative_path)
|
|
865
|
+
|
|
866
|
+
logger.debug(f"Filtered to {len(agent_files)} agent files")
|
|
867
|
+
return agent_files
|
|
868
|
+
|
|
869
|
+
def _migrate_etag_cache(self, cache_file: Path):
|
|
870
|
+
"""Migrate old ETag cache to SQLite (one-time operation).
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
cache_file: Path to old JSON ETag cache file
|
|
874
|
+
|
|
875
|
+
Error Handling:
|
|
876
|
+
- Migration failures are logged but don't stop initialization
|
|
877
|
+
- Old cache is renamed to .migrated to prevent re-migration
|
|
878
|
+
"""
|
|
879
|
+
try:
|
|
880
|
+
with cache_file.open() as f:
|
|
881
|
+
old_cache = json.load(f)
|
|
882
|
+
|
|
883
|
+
logger.info(f"Migrating {len(old_cache)} ETag entries to SQLite...")
|
|
884
|
+
|
|
885
|
+
migrated = 0
|
|
886
|
+
for url, metadata in old_cache.items():
|
|
887
|
+
try:
|
|
888
|
+
etag = metadata.get("etag")
|
|
889
|
+
if etag:
|
|
890
|
+
# Store in new system
|
|
891
|
+
self.sync_state.update_source_sync_metadata(
|
|
892
|
+
source_id=self.source_id, etag=etag
|
|
893
|
+
)
|
|
894
|
+
migrated += 1
|
|
895
|
+
except Exception as e:
|
|
896
|
+
logger.error(f"Failed to migrate {url}: {e}")
|
|
897
|
+
|
|
898
|
+
# Rename old cache to prevent re-migration
|
|
899
|
+
backup_file = cache_file.with_suffix(".json.migrated")
|
|
900
|
+
cache_file.rename(backup_file)
|
|
901
|
+
|
|
902
|
+
logger.info(
|
|
903
|
+
f"ETag cache migration complete: {migrated} entries migrated, "
|
|
904
|
+
f"old cache backed up to {backup_file.name}"
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
except json.JSONDecodeError as e:
|
|
908
|
+
logger.error(f"Invalid JSON in ETag cache, skipping migration: {e}")
|
|
909
|
+
except Exception as e:
|
|
910
|
+
logger.error(f"Failed to migrate ETag cache: {e}")
|
|
911
|
+
|
|
912
|
+
def get_cached_agents_dir(self) -> Path:
|
|
913
|
+
"""Get directory containing cached agent files.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
Path to cache directory for integration with MultiSourceAgentDeploymentService
|
|
917
|
+
"""
|
|
918
|
+
return self.cache_dir
|
|
919
|
+
|
|
920
|
+
def deploy_agents_to_project(
|
|
921
|
+
self,
|
|
922
|
+
project_dir: Path,
|
|
923
|
+
agent_list: Optional[List[str]] = None,
|
|
924
|
+
force: bool = False,
|
|
925
|
+
) -> Dict[str, Any]:
|
|
926
|
+
"""Deploy agents from cache to project directory (Phase 1 deployment).
|
|
927
|
+
|
|
928
|
+
Design Decision: Copy from cache to project-specific deployment directory
|
|
929
|
+
|
|
930
|
+
Rationale: Separates syncing (cache) from deployment (project-local).
|
|
931
|
+
Allows multiple projects to use same cache with different agent
|
|
932
|
+
configurations. Flattens nested structure for backward compatibility.
|
|
933
|
+
|
|
934
|
+
Trade-offs:
|
|
935
|
+
- Storage: 2x disk usage (cache + deployments)
|
|
936
|
+
- Performance: Copy operation ~10ms for 50 agents
|
|
937
|
+
- Isolation: Each project has independent agent set
|
|
938
|
+
- Flexibility: Can deploy subset of cached agents per project
|
|
939
|
+
|
|
940
|
+
Algorithm:
|
|
941
|
+
1. Create deployment directory (.claude-mpm/agents/)
|
|
942
|
+
2. Discover cached agents if list not provided
|
|
943
|
+
3. For each agent, flatten path and copy to deployment
|
|
944
|
+
4. Track deployment results (new, updated, skipped)
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
project_dir: Project root directory (e.g., /path/to/project)
|
|
948
|
+
agent_list: Optional list of agent paths to deploy (uses all if None)
|
|
949
|
+
force: Force redeployment even if up-to-date
|
|
950
|
+
|
|
951
|
+
Returns:
|
|
952
|
+
Dictionary with deployment results:
|
|
953
|
+
{
|
|
954
|
+
"deployed": ["engineer.md"], # Newly deployed
|
|
955
|
+
"updated": ["research.md"], # Updated existing
|
|
956
|
+
"skipped": ["qa.md"], # Already up-to-date
|
|
957
|
+
"failed": ["broken.md"], # Copy failures
|
|
958
|
+
"deployment_dir": "/path/.claude-mpm/agents"
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
Error Handling:
|
|
962
|
+
- Missing cache files: Logged and added to "failed" list
|
|
963
|
+
- Permission errors: Individual failures don't stop deployment
|
|
964
|
+
- Directory creation: Creates deployment directory if missing
|
|
965
|
+
|
|
966
|
+
Example:
|
|
967
|
+
>>> service = GitSourceSyncService()
|
|
968
|
+
>>> service.sync_agents() # Sync to cache first
|
|
969
|
+
>>> result = service.deploy_agents_to_project(Path("/my/project"))
|
|
970
|
+
>>> print(f"Deployed {len(result['deployed'])} agents")
|
|
971
|
+
"""
|
|
972
|
+
import shutil
|
|
973
|
+
|
|
974
|
+
deployment_dir = project_dir / ".claude-mpm" / "agents"
|
|
975
|
+
deployment_dir.mkdir(parents=True, exist_ok=True)
|
|
976
|
+
|
|
977
|
+
results = {
|
|
978
|
+
"deployed": [],
|
|
979
|
+
"updated": [],
|
|
980
|
+
"skipped": [],
|
|
981
|
+
"failed": [],
|
|
982
|
+
"deployment_dir": str(deployment_dir),
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
# Get agents from cache or use provided list
|
|
986
|
+
if agent_list is None:
|
|
987
|
+
agent_list = self._discover_cached_agents()
|
|
988
|
+
|
|
989
|
+
logger.info(
|
|
990
|
+
f"Deploying {len(agent_list)} agents from cache to {deployment_dir}"
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
for agent_path in agent_list:
|
|
994
|
+
try:
|
|
995
|
+
cache_file = self.cache_dir / agent_path
|
|
996
|
+
|
|
997
|
+
if not cache_file.exists():
|
|
998
|
+
logger.warning(f"Cache file not found: {agent_path}")
|
|
999
|
+
results["failed"].append(agent_path)
|
|
1000
|
+
continue
|
|
1001
|
+
|
|
1002
|
+
# Flatten nested path for deployment (engineer/core/engineer.md → engineer.md)
|
|
1003
|
+
deploy_filename = Path(agent_path).name
|
|
1004
|
+
deploy_file = deployment_dir / deploy_filename
|
|
1005
|
+
|
|
1006
|
+
# Check if update needed (compare modification times)
|
|
1007
|
+
should_deploy = force
|
|
1008
|
+
was_existing = deploy_file.exists()
|
|
1009
|
+
|
|
1010
|
+
if not force and was_existing:
|
|
1011
|
+
cache_mtime = cache_file.stat().st_mtime
|
|
1012
|
+
deploy_mtime = deploy_file.stat().st_mtime
|
|
1013
|
+
should_deploy = cache_mtime > deploy_mtime
|
|
1014
|
+
|
|
1015
|
+
if not should_deploy and was_existing:
|
|
1016
|
+
results["skipped"].append(deploy_filename)
|
|
1017
|
+
logger.debug(f"Skipped (up-to-date): {deploy_filename}")
|
|
1018
|
+
continue
|
|
1019
|
+
|
|
1020
|
+
# Copy from cache to deployment
|
|
1021
|
+
shutil.copy2(cache_file, deploy_file)
|
|
1022
|
+
|
|
1023
|
+
# Track result
|
|
1024
|
+
if deploy_file.exists():
|
|
1025
|
+
if was_existing:
|
|
1026
|
+
results["updated"].append(deploy_filename)
|
|
1027
|
+
logger.info(f"Updated: {deploy_filename}")
|
|
1028
|
+
else:
|
|
1029
|
+
results["deployed"].append(deploy_filename)
|
|
1030
|
+
logger.info(f"Deployed: {deploy_filename}")
|
|
1031
|
+
else:
|
|
1032
|
+
results["failed"].append(deploy_filename)
|
|
1033
|
+
logger.error(f"Failed to deploy: {deploy_filename}")
|
|
1034
|
+
|
|
1035
|
+
except PermissionError as e:
|
|
1036
|
+
logger.error(f"Permission denied deploying {agent_path}: {e}")
|
|
1037
|
+
results["failed"].append(Path(agent_path).name)
|
|
1038
|
+
except OSError as e:
|
|
1039
|
+
logger.error(f"IO error deploying {agent_path}: {e}")
|
|
1040
|
+
results["failed"].append(Path(agent_path).name)
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
logger.error(f"Unexpected error deploying {agent_path}: {e}")
|
|
1043
|
+
results["failed"].append(Path(agent_path).name)
|
|
1044
|
+
|
|
1045
|
+
# Log summary
|
|
1046
|
+
total_success = len(results["deployed"]) + len(results["updated"])
|
|
1047
|
+
logger.info(
|
|
1048
|
+
f"Deployment complete: {total_success} deployed/updated, "
|
|
1049
|
+
f"{len(results['skipped'])} skipped, {len(results['failed'])} failed"
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
return results
|
|
1053
|
+
|
|
1054
|
+
def _discover_cached_agents(self) -> List[str]:
|
|
1055
|
+
"""Discover all agent files currently in cache.
|
|
1056
|
+
|
|
1057
|
+
Scans cache directory for .md and .json files, preserving
|
|
1058
|
+
nested directory structure in returned paths.
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
List of agent file paths relative to cache directory
|
|
1062
|
+
(e.g., ["research.md", "engineer/core/engineer.md"])
|
|
1063
|
+
|
|
1064
|
+
Algorithm:
|
|
1065
|
+
1. Walk cache directory recursively
|
|
1066
|
+
2. Find all .md and .json files
|
|
1067
|
+
3. Convert to paths relative to cache root
|
|
1068
|
+
4. Filter out README.md and .gitignore
|
|
1069
|
+
"""
|
|
1070
|
+
cached_agents = []
|
|
1071
|
+
|
|
1072
|
+
if not self.cache_dir.exists():
|
|
1073
|
+
logger.warning(f"Cache directory does not exist: {self.cache_dir}")
|
|
1074
|
+
return []
|
|
1075
|
+
|
|
1076
|
+
for file_path in self.cache_dir.rglob("*"):
|
|
1077
|
+
if file_path.is_file() and file_path.suffix in {".md", ".json"}:
|
|
1078
|
+
# Get relative path from cache directory
|
|
1079
|
+
relative_path = file_path.relative_to(self.cache_dir)
|
|
1080
|
+
relative_str = str(relative_path)
|
|
1081
|
+
|
|
1082
|
+
# Exclude README and .gitignore
|
|
1083
|
+
if relative_str not in ["README.md", ".gitignore"]:
|
|
1084
|
+
cached_agents.append(relative_str)
|
|
1085
|
+
|
|
1086
|
+
logger.debug(f"Discovered {len(cached_agents)} cached agents")
|
|
1087
|
+
return sorted(cached_agents)
|