claude-mpm 5.1.8__py3-none-any.whl → 5.4.22__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/__init__.py +4 -0
- claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
- claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/frontmatter_validator.py +68 -0
- claude_mpm/agents/templates/circuit-breakers.md +138 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +8 -17
- claude_mpm/cli/commands/agents.py +169 -31
- claude_mpm/cli/commands/auto_configure.py +210 -25
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1111 -161
- claude_mpm/cli/commands/configure_agent_display.py +15 -6
- claude_mpm/cli/commands/mpm_init/core.py +160 -46
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +11 -3
- claude_mpm/cli/parsers/agents_parser.py +54 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +550 -94
- claude_mpm/commands/mpm-config.md +265 -0
- claude_mpm/commands/mpm-help.md +14 -95
- claude_mpm/commands/mpm-organize.md +500 -0
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
- claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +0 -19
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +188 -12
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +531 -55
- claude_mpm/services/agents/git_source_manager.py +34 -0
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
- claude_mpm/services/agents/toolchain_detector.py +10 -6
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +81 -10
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +93 -8
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +32 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +17 -44
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +86 -176
- claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_ENGINEER.md +0 -658
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent.json +0 -31
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/commands/mpm-ticket-organize.md +0 -304
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- 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/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/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- claude_mpm-5.1.8.dist-info/entry_points.txt +0 -10
- claude_mpm-5.1.8.dist-info/licenses/LICENSE +0 -21
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
- {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
|
@@ -23,10 +23,14 @@ class CommandDeploymentService(BaseService):
|
|
|
23
23
|
DEPRECATED_COMMANDS = [
|
|
24
24
|
"mpm-agents.md", # Replaced by mpm-agents-list.md
|
|
25
25
|
"mpm-auto-configure.md", # Replaced by mpm-agents-auto-configure.md
|
|
26
|
-
"mpm-config.md", # Replaced by mpm-config
|
|
27
|
-
"mpm-organize.md", # Replaced by mpm-ticket-organize.md
|
|
26
|
+
"mpm-config-view.md", # Replaced by mpm-config.md
|
|
28
27
|
"mpm-resume.md", # Replaced by mpm-session-resume.md
|
|
29
28
|
"mpm-ticket.md", # Replaced by mpm-ticket-view.md
|
|
29
|
+
# Removed - consolidated into /mpm-configure
|
|
30
|
+
"mpm-agents-list.md", # Consolidated into /mpm-configure
|
|
31
|
+
"mpm-agents-detect.md", # Consolidated into /mpm-configure
|
|
32
|
+
"mpm-agents-auto-configure.md", # Consolidated into /mpm-configure
|
|
33
|
+
"mpm-agents-recommend.md", # Consolidated into /mpm-configure
|
|
30
34
|
]
|
|
31
35
|
|
|
32
36
|
def __init__(self):
|
|
@@ -315,10 +319,14 @@ class CommandDeploymentService(BaseService):
|
|
|
315
319
|
replacement_map = {
|
|
316
320
|
"mpm-agents.md": "mpm-agents-list.md",
|
|
317
321
|
"mpm-auto-configure.md": "mpm-agents-auto-configure.md",
|
|
318
|
-
"mpm-config.md": "mpm-config
|
|
319
|
-
"mpm-organize.md": "mpm-ticket-organize.md",
|
|
322
|
+
"mpm-config-view.md": "mpm-config.md",
|
|
320
323
|
"mpm-resume.md": "mpm-session-resume.md",
|
|
321
324
|
"mpm-ticket.md": "mpm-ticket-view.md",
|
|
325
|
+
# Removed commands - consolidated into /mpm-configure
|
|
326
|
+
"mpm-agents-list.md": "mpm-configure (use /mpm-configure)",
|
|
327
|
+
"mpm-agents-detect.md": "mpm-configure (use /mpm-configure)",
|
|
328
|
+
"mpm-agents-auto-configure.md": "mpm-configure (use /mpm-configure)",
|
|
329
|
+
"mpm-agents-recommend.md": "mpm-configure (use /mpm-configure)",
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
for deprecated_cmd in self.DEPRECATED_COMMANDS:
|
|
@@ -345,13 +353,71 @@ class CommandDeploymentService(BaseService):
|
|
|
345
353
|
|
|
346
354
|
return removed
|
|
347
355
|
|
|
356
|
+
def remove_stale_commands(self) -> int:
|
|
357
|
+
"""Remove stale MPM commands that no longer exist in source.
|
|
358
|
+
|
|
359
|
+
This method cleans up deployed commands that have been deleted or renamed
|
|
360
|
+
in the source directory. It's called automatically on startup to ensure
|
|
361
|
+
deployed commands stay in sync with source.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Number of stale files removed
|
|
365
|
+
"""
|
|
366
|
+
if not self.target_dir.exists():
|
|
367
|
+
self.logger.debug(
|
|
368
|
+
f"Target directory does not exist: {self.target_dir}, skipping stale command cleanup"
|
|
369
|
+
)
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
if not self.source_dir.exists():
|
|
373
|
+
self.logger.warning(
|
|
374
|
+
f"Source directory does not exist: {self.source_dir}, cannot detect stale commands"
|
|
375
|
+
)
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
# Get current source commands (ground truth)
|
|
379
|
+
source_commands = {f.name for f in self.source_dir.glob("mpm*.md")}
|
|
380
|
+
|
|
381
|
+
# Get deployed commands
|
|
382
|
+
deployed_commands = {f.name for f in self.target_dir.glob("mpm*.md")}
|
|
383
|
+
|
|
384
|
+
# Find stale commands (deployed but not in source, excluding deprecated)
|
|
385
|
+
stale_commands = (
|
|
386
|
+
deployed_commands - source_commands - set(self.DEPRECATED_COMMANDS)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if not stale_commands:
|
|
390
|
+
self.logger.debug("No stale commands found to remove")
|
|
391
|
+
return 0
|
|
392
|
+
|
|
393
|
+
removed = 0
|
|
394
|
+
self.logger.info(f"Cleaning up {len(stale_commands)} stale command(s)...")
|
|
395
|
+
|
|
396
|
+
for stale_cmd in stale_commands:
|
|
397
|
+
stale_file = self.target_dir / stale_cmd
|
|
398
|
+
try:
|
|
399
|
+
stale_file.unlink()
|
|
400
|
+
self.logger.info(
|
|
401
|
+
f"Removed stale command: {stale_cmd} (no longer in source)"
|
|
402
|
+
)
|
|
403
|
+
removed += 1
|
|
404
|
+
except Exception as e:
|
|
405
|
+
# Log error but don't fail startup - this is non-critical
|
|
406
|
+
self.logger.warning(f"Failed to remove stale command {stale_cmd}: {e}")
|
|
407
|
+
|
|
408
|
+
if removed > 0:
|
|
409
|
+
self.logger.info(f"Removed {removed} stale command(s)")
|
|
410
|
+
|
|
411
|
+
return removed
|
|
412
|
+
|
|
348
413
|
|
|
349
414
|
def deploy_commands_on_startup(force: bool = False) -> None:
|
|
350
415
|
"""Convenience function to deploy commands during startup.
|
|
351
416
|
|
|
352
417
|
This function:
|
|
353
418
|
1. Removes deprecated commands that have been replaced
|
|
354
|
-
2.
|
|
419
|
+
2. Removes stale commands that no longer exist in source
|
|
420
|
+
3. Deploys current command files
|
|
355
421
|
|
|
356
422
|
Args:
|
|
357
423
|
force: Force deployment even if files exist
|
|
@@ -359,12 +425,17 @@ def deploy_commands_on_startup(force: bool = False) -> None:
|
|
|
359
425
|
service = CommandDeploymentService()
|
|
360
426
|
logger = get_logger("startup")
|
|
361
427
|
|
|
362
|
-
# Clean up deprecated commands
|
|
363
|
-
|
|
364
|
-
if
|
|
365
|
-
logger.info(f"Cleaned up {
|
|
428
|
+
# Clean up deprecated commands FIRST (known old commands)
|
|
429
|
+
deprecated_count = service.remove_deprecated_commands()
|
|
430
|
+
if deprecated_count > 0:
|
|
431
|
+
logger.info(f"Cleaned up {deprecated_count} deprecated command(s)")
|
|
432
|
+
|
|
433
|
+
# Clean up stale commands SECOND (deployed but not in source anymore)
|
|
434
|
+
stale_count = service.remove_stale_commands()
|
|
435
|
+
if stale_count > 0:
|
|
436
|
+
logger.info(f"Cleaned up {stale_count} stale command(s)")
|
|
366
437
|
|
|
367
|
-
# Deploy current commands
|
|
438
|
+
# Deploy current commands LAST
|
|
368
439
|
result = service.deploy_commands(force=force)
|
|
369
440
|
|
|
370
441
|
if result["deployed"]:
|
|
@@ -52,9 +52,11 @@ class EventBusConfig:
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
# Relay configuration
|
|
55
|
+
# DirectSocketIORelay disabled by default - events already emit via direct sio.emit()
|
|
56
|
+
# Enable with CLAUDE_MPM_RELAY_ENABLED=true if needed for external consumers
|
|
55
57
|
relay_enabled: bool = field(
|
|
56
58
|
default_factory=lambda: os.environ.get(
|
|
57
|
-
"CLAUDE_MPM_RELAY_ENABLED", "
|
|
59
|
+
"CLAUDE_MPM_RELAY_ENABLED", "false"
|
|
58
60
|
).lower()
|
|
59
61
|
== "true"
|
|
60
62
|
)
|
|
@@ -18,10 +18,13 @@ Example:
|
|
|
18
18
|
... service.commit(Path("~/.claude-mpm/cache/remote-agents"), "feat: improve research agent memory handling")
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
import logging
|
|
21
22
|
import subprocess
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import List, Optional, Tuple
|
|
24
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
# Custom Exceptions
|
|
27
30
|
class GitOperationError(Exception):
|
|
@@ -314,7 +317,12 @@ class GitOperationsService:
|
|
|
314
317
|
|
|
315
318
|
def pull(self, repo_path: Path, branch: str = "main") -> bool:
|
|
316
319
|
"""
|
|
317
|
-
Pull latest changes from remote.
|
|
320
|
+
Pull latest changes from remote with automatic divergent branch recovery.
|
|
321
|
+
|
|
322
|
+
For cache/sync repositories, automatically handles divergent branches by:
|
|
323
|
+
1. First attempting git pull --rebase (cleaner history)
|
|
324
|
+
2. If rebase fails or conflicts exist, hard reset to match remote exactly
|
|
325
|
+
(cache repos should mirror remote state, local changes are discarded)
|
|
318
326
|
|
|
319
327
|
Args:
|
|
320
328
|
repo_path: Repository path
|
|
@@ -325,21 +333,98 @@ class GitOperationsService:
|
|
|
325
333
|
|
|
326
334
|
Raises:
|
|
327
335
|
GitOperationError: If pull fails
|
|
328
|
-
GitConflictError: If merge conflicts detected
|
|
336
|
+
GitConflictError: If merge conflicts detected and cannot be auto-resolved
|
|
329
337
|
"""
|
|
330
338
|
self._validate_repo(repo_path)
|
|
331
339
|
|
|
340
|
+
# First, try standard pull
|
|
332
341
|
returncode, _stdout, stderr = self._run_git_command(
|
|
333
342
|
["git", "pull", "origin", branch], cwd=repo_path
|
|
334
343
|
)
|
|
335
344
|
|
|
336
|
-
if returncode
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
if returncode == 0:
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
# Check if this is a divergent branch situation
|
|
349
|
+
is_divergent = any(
|
|
350
|
+
phrase in stderr.lower()
|
|
351
|
+
for phrase in [
|
|
352
|
+
"divergent branches",
|
|
353
|
+
"need to specify how to reconcile",
|
|
354
|
+
"have diverged",
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if is_divergent:
|
|
359
|
+
logger.warning(
|
|
360
|
+
f"Divergent branches detected in cache repository at {repo_path}. "
|
|
361
|
+
"Attempting automatic recovery with rebase..."
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Strategy 1: Try rebase for cleaner history
|
|
365
|
+
returncode, _stdout, stderr = self._run_git_command(
|
|
366
|
+
["git", "pull", "--rebase", "origin", branch], cwd=repo_path
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if returncode == 0:
|
|
370
|
+
logger.info(
|
|
371
|
+
f"Successfully resolved divergent branches with rebase for {branch}"
|
|
372
|
+
)
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
# Check if rebase had conflicts
|
|
376
|
+
has_rebase_conflict = any(
|
|
377
|
+
phrase in stderr.lower()
|
|
378
|
+
for phrase in ["conflict", "rebase in progress"]
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if has_rebase_conflict:
|
|
382
|
+
logger.warning(
|
|
383
|
+
"Rebase conflicts detected. Aborting rebase and resetting to remote state..."
|
|
341
384
|
)
|
|
342
|
-
|
|
385
|
+
# Abort the rebase
|
|
386
|
+
self._run_git_command(["git", "rebase", "--abort"], cwd=repo_path)
|
|
387
|
+
|
|
388
|
+
# Strategy 2: Hard reset to match remote exactly (cache repos should mirror remote)
|
|
389
|
+
logger.warning(
|
|
390
|
+
f"Discarding local changes and resetting to origin/{branch} "
|
|
391
|
+
"(cache repositories should match remote exactly)"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Fetch latest to ensure we have the remote state
|
|
395
|
+
returncode, _stdout, stderr = self._run_git_command(
|
|
396
|
+
["git", "fetch", "origin", branch], cwd=repo_path
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if returncode != 0:
|
|
400
|
+
raise GitOperationError(
|
|
401
|
+
f"Failed to fetch from remote during divergent branch recovery: {stderr}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Hard reset to remote branch
|
|
405
|
+
returncode, _stdout, stderr = self._run_git_command(
|
|
406
|
+
["git", "reset", "--hard", f"origin/{branch}"], cwd=repo_path
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if returncode != 0:
|
|
410
|
+
raise GitOperationError(
|
|
411
|
+
f"Failed to reset to origin/{branch} during divergent branch recovery: {stderr}"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
logger.info(
|
|
415
|
+
f"Successfully reset cache repository to origin/{branch} "
|
|
416
|
+
"(local changes discarded)"
|
|
417
|
+
)
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
# Check for merge conflicts (non-divergent case)
|
|
421
|
+
if "conflict" in stderr.lower():
|
|
422
|
+
raise GitConflictError(
|
|
423
|
+
f"Merge conflicts detected when pulling {branch}: {stderr}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Other pull errors
|
|
427
|
+
raise GitOperationError(f"Failed to pull {branch}: {stderr}")
|
|
343
428
|
|
|
344
429
|
return True
|
|
345
430
|
|
|
@@ -44,6 +44,7 @@ class UnifiedMonitorDaemon:
|
|
|
44
44
|
daemon_mode: bool = False,
|
|
45
45
|
pid_file: Optional[str] = None,
|
|
46
46
|
log_file: Optional[str] = None,
|
|
47
|
+
enable_hot_reload: bool = False,
|
|
47
48
|
):
|
|
48
49
|
"""Initialize the unified monitor daemon.
|
|
49
50
|
|
|
@@ -53,10 +54,12 @@ class UnifiedMonitorDaemon:
|
|
|
53
54
|
daemon_mode: Whether to run as background daemon
|
|
54
55
|
pid_file: Path to PID file for daemon mode
|
|
55
56
|
log_file: Path to log file for daemon mode
|
|
57
|
+
enable_hot_reload: Enable file watching and hot reload for development
|
|
56
58
|
"""
|
|
57
59
|
self.host = host
|
|
58
60
|
self.port = port
|
|
59
61
|
self.daemon_mode = daemon_mode
|
|
62
|
+
self.enable_hot_reload = enable_hot_reload
|
|
60
63
|
self.logger = get_logger(__name__)
|
|
61
64
|
|
|
62
65
|
# Use new consolidated DaemonManager for all daemon operations
|
|
@@ -75,7 +78,9 @@ class UnifiedMonitorDaemon:
|
|
|
75
78
|
)
|
|
76
79
|
|
|
77
80
|
# Core server
|
|
78
|
-
self.server = UnifiedMonitorServer(
|
|
81
|
+
self.server = UnifiedMonitorServer(
|
|
82
|
+
host=host, port=port, enable_hot_reload=enable_hot_reload
|
|
83
|
+
)
|
|
79
84
|
|
|
80
85
|
# Health monitoring
|
|
81
86
|
self.health_monitor = HealthMonitor(port=port)
|
|
@@ -510,7 +515,9 @@ class UnifiedMonitorDaemon:
|
|
|
510
515
|
|
|
511
516
|
# Recreate the server and health monitor after stop() sets them to None
|
|
512
517
|
self.logger.info(f"Recreating server components for {self.host}:{self.port}")
|
|
513
|
-
self.server = UnifiedMonitorServer(
|
|
518
|
+
self.server = UnifiedMonitorServer(
|
|
519
|
+
host=self.host, port=self.port, enable_hot_reload=self.enable_hot_reload
|
|
520
|
+
)
|
|
514
521
|
self.health_monitor = HealthMonitor(port=self.port)
|
|
515
522
|
|
|
516
523
|
# Reset the shutdown event for the new run
|
|
@@ -34,6 +34,11 @@ from typing import Optional, Tuple
|
|
|
34
34
|
from ...core.enums import OperationResult
|
|
35
35
|
from ...core.logging_config import get_logger
|
|
36
36
|
|
|
37
|
+
# Exit code constants for signal handling
|
|
38
|
+
EXIT_NORMAL = 0
|
|
39
|
+
EXIT_SIGKILL = 137 # 128 + SIGKILL(9) - forced termination
|
|
40
|
+
EXIT_SIGTERM = 143 # 128 + SIGTERM(15) - graceful shutdown
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
class DaemonManager:
|
|
39
44
|
"""Centralized manager for all daemon lifecycle operations.
|
|
@@ -556,7 +561,7 @@ class DaemonManager:
|
|
|
556
561
|
# Use subprocess for clean daemon startup (v4.2.40)
|
|
557
562
|
# This avoids fork() issues with Python threading
|
|
558
563
|
if self.use_subprocess_daemon():
|
|
559
|
-
return self.start_daemon_subprocess()
|
|
564
|
+
return self.start_daemon_subprocess(force_restart=force_restart)
|
|
560
565
|
# Fallback to traditional fork (kept for compatibility)
|
|
561
566
|
return self.daemonize()
|
|
562
567
|
|
|
@@ -574,12 +579,15 @@ class DaemonManager:
|
|
|
574
579
|
# Otherwise, use subprocess for monitor daemon to avoid threading issues
|
|
575
580
|
return True
|
|
576
581
|
|
|
577
|
-
def start_daemon_subprocess(self) -> bool:
|
|
582
|
+
def start_daemon_subprocess(self, force_restart: bool = False) -> bool:
|
|
578
583
|
"""Start daemon using subprocess.Popen for clean process isolation.
|
|
579
584
|
|
|
580
585
|
This avoids all the fork() + threading issues by starting the monitor
|
|
581
586
|
in a completely fresh process with no inherited threads or locks.
|
|
582
587
|
|
|
588
|
+
Args:
|
|
589
|
+
force_restart: Whether this is a force restart (helps interpret exit codes)
|
|
590
|
+
|
|
583
591
|
Returns:
|
|
584
592
|
True if daemon started successfully
|
|
585
593
|
"""
|
|
@@ -652,7 +660,35 @@ class DaemonManager:
|
|
|
652
660
|
# Check if process is still running
|
|
653
661
|
returncode = process.poll()
|
|
654
662
|
if returncode is not None:
|
|
655
|
-
# Process exited -
|
|
663
|
+
# Process exited - interpret exit code with context
|
|
664
|
+
# Exit codes 137 (SIGKILL) and 143 (SIGTERM) are common during daemon replacement
|
|
665
|
+
if returncode == EXIT_SIGKILL:
|
|
666
|
+
# SIGKILL - process was forcefully terminated
|
|
667
|
+
if force_restart:
|
|
668
|
+
# This is expected during force restart - old daemon was killed
|
|
669
|
+
self.logger.info(
|
|
670
|
+
f"Previous monitor instance replaced (exit {EXIT_SIGKILL}: SIGKILL during force restart)"
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
# Unexpected SIGKILL - something else killed our new daemon
|
|
674
|
+
self.logger.warning(
|
|
675
|
+
f"Monitor subprocess terminated unexpectedly (exit {EXIT_SIGKILL}: SIGKILL). "
|
|
676
|
+
f"Check {self.log_file} for details."
|
|
677
|
+
)
|
|
678
|
+
return False
|
|
679
|
+
if returncode == EXIT_SIGTERM:
|
|
680
|
+
# SIGTERM - graceful shutdown requested
|
|
681
|
+
self.logger.info(
|
|
682
|
+
f"Monitor subprocess cleanly terminated (exit {EXIT_SIGTERM}: SIGTERM, graceful shutdown)"
|
|
683
|
+
)
|
|
684
|
+
return False
|
|
685
|
+
if returncode == EXIT_NORMAL:
|
|
686
|
+
# Normal exit
|
|
687
|
+
self.logger.info(
|
|
688
|
+
f"Monitor subprocess exited normally (exit code {EXIT_NORMAL})"
|
|
689
|
+
)
|
|
690
|
+
return False
|
|
691
|
+
# Unexpected exit code - this IS an error
|
|
656
692
|
self.logger.error(
|
|
657
693
|
f"Monitor daemon subprocess exited prematurely with code {returncode}"
|
|
658
694
|
)
|